refactor(registries): migrate tags table to react [EE-6452] (#10990)
parent
8913e75484
commit
bd271ec5a1
|
@ -4,10 +4,20 @@ import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable';
|
import { RepositoriesDatatable } from '@/react/portainer/registries/repositories/ListView/RepositoriesDatatable';
|
||||||
|
import { TagsDatatable } from '@/react/portainer/registries/repositories/ItemView/TagsDatatable/TagsDatatable';
|
||||||
|
|
||||||
export const registriesModule = angular
|
export const registriesModule = angular
|
||||||
.module('portainer.app.react.components.registries', [])
|
.module('portainer.app.react.components.registries', [])
|
||||||
.component(
|
.component(
|
||||||
'registryRepositoriesDatatable',
|
'registryRepositoriesDatatable',
|
||||||
r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset'])
|
r2a(withUIRouter(withReactQuery(RepositoriesDatatable)), ['dataset'])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'registriesRepositoryTagsDatatable',
|
||||||
|
r2a(withUIRouter(withReactQuery(TagsDatatable)), [
|
||||||
|
'dataset',
|
||||||
|
'advancedFeaturesAvailable',
|
||||||
|
'onRemove',
|
||||||
|
'onRetag',
|
||||||
|
])
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { TagIcon } from 'lucide-react';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
|
||||||
|
import { Datatable } from '@@/datatables';
|
||||||
|
import { useTableStateWithStorage } from '@@/datatables/useTableState';
|
||||||
|
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||||
|
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
||||||
|
|
||||||
|
import { Tag } from './types';
|
||||||
|
import { useColumns } from './columns/useColumns';
|
||||||
|
import { newNamesStore } from './useRetagState';
|
||||||
|
import { RepositoryTagViewModel } from './view-model';
|
||||||
|
|
||||||
|
export function TagsDatatable({
|
||||||
|
dataset,
|
||||||
|
advancedFeaturesAvailable,
|
||||||
|
onRemove,
|
||||||
|
onRetag,
|
||||||
|
}: {
|
||||||
|
dataset?: Tag[];
|
||||||
|
advancedFeaturesAvailable: boolean;
|
||||||
|
onRemove: (tags: Tag[]) => void;
|
||||||
|
onRetag: (tags: Record<string, RepositoryTagViewModel>) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const updatesState = useStore(newNamesStore);
|
||||||
|
|
||||||
|
const tableState = useTableStateWithStorage('registryRepositoryTags', 'name');
|
||||||
|
const columns = useColumns(advancedFeaturesAvailable);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Datatable
|
||||||
|
title="Tags"
|
||||||
|
titleIcon={TagIcon}
|
||||||
|
columns={columns}
|
||||||
|
dataset={dataset || []}
|
||||||
|
isLoading={!dataset}
|
||||||
|
settingsManager={tableState}
|
||||||
|
emptyContentLabel="No tags available."
|
||||||
|
renderTableActions={(selectedItems) =>
|
||||||
|
advancedFeaturesAvailable && (
|
||||||
|
<DeleteButton
|
||||||
|
confirmMessage="Are you sure you want to remove the selected tags?"
|
||||||
|
onConfirmed={() => onRemove(selectedItems)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getRowId={(tag) => tag.Name}
|
||||||
|
extendTableOptions={withMeta({
|
||||||
|
onUpdate: async () => {
|
||||||
|
await onRetag(updatesState.updates);
|
||||||
|
updatesState.clear();
|
||||||
|
},
|
||||||
|
table: 'registry-repository-tags',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { X, Check, TagIcon } from 'lucide-react';
|
||||||
|
import { Form, Formik } from 'formik';
|
||||||
|
import { useStore } from 'zustand';
|
||||||
|
import { object, string } from 'yup';
|
||||||
|
|
||||||
|
import { Button } from '@@/buttons';
|
||||||
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
import { Input } from '@@/form-components/Input';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
|
import { Tag } from '../types';
|
||||||
|
import { newNamesStore } from '../useRetagState';
|
||||||
|
import { getTableMeta } from '../meta';
|
||||||
|
|
||||||
|
import { helper } from './helper';
|
||||||
|
import { useDetails } from './buildCell';
|
||||||
|
|
||||||
|
export const actions = helper.display({
|
||||||
|
header: 'Actions',
|
||||||
|
cell: ActionsCell,
|
||||||
|
});
|
||||||
|
|
||||||
|
function ActionsCell({
|
||||||
|
table,
|
||||||
|
row: { original: item },
|
||||||
|
}: CellContext<Tag, unknown>) {
|
||||||
|
const meta = getTableMeta(table.options.meta);
|
||||||
|
const detailsQuery = useDetails(item.Name);
|
||||||
|
const state = useStore(newNamesStore);
|
||||||
|
|
||||||
|
const isEdit = state.updates[item.Name] !== undefined;
|
||||||
|
|
||||||
|
if (!detailsQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagDetails = detailsQuery.data;
|
||||||
|
|
||||||
|
if (!isEdit) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color="link"
|
||||||
|
icon={TagIcon}
|
||||||
|
onClick={() => state.setName(item.Name, tagDetails)}
|
||||||
|
>
|
||||||
|
Retag
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditTag
|
||||||
|
initialName={item.Name}
|
||||||
|
onCancel={() => state.setName(item.Name)}
|
||||||
|
onChange={(name) =>
|
||||||
|
state.setName(item.Name, {
|
||||||
|
...tagDetails,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onSubmit={() => meta.onUpdate()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = object().shape({
|
||||||
|
name: string()
|
||||||
|
.required()
|
||||||
|
.matches(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/),
|
||||||
|
});
|
||||||
|
|
||||||
|
function EditTag({
|
||||||
|
initialName,
|
||||||
|
onCancel,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
initialName: string;
|
||||||
|
onChange: (name: string) => void;
|
||||||
|
onCancel(): void;
|
||||||
|
onSubmit(): void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={{ name: initialName }}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
validationSchema={schema}
|
||||||
|
>
|
||||||
|
{({ values, errors, setFieldValue }) => (
|
||||||
|
<Form className="vertical-center">
|
||||||
|
<Tooltip message="'Tag can only contain alphanumeric (a-zA-Z0-9) and special _ . - characters. Tag must not start with . - characters.'" />
|
||||||
|
|
||||||
|
<Input
|
||||||
|
className="input-sm"
|
||||||
|
type="text"
|
||||||
|
value={values.name}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFieldValue('name', e.target.value);
|
||||||
|
onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errors.name && <FormError>{errors.name}</FormError>}
|
||||||
|
|
||||||
|
<Button color="none" icon={X} onClick={onCancel} />
|
||||||
|
<Button type="submit" color="none" icon={Check} />
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { useTagDetails } from '../../../queries/useTagDetails';
|
||||||
|
import { Tag } from '../types';
|
||||||
|
import { RepositoryTagViewModel } from '../view-model';
|
||||||
|
|
||||||
|
function useParams() {
|
||||||
|
const {
|
||||||
|
params: { endpointId, id, repository },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
|
const registryId = number(id);
|
||||||
|
|
||||||
|
if (!registryId) {
|
||||||
|
throw new Error('Missing registry id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repository || typeof repository !== 'string') {
|
||||||
|
throw new Error('Missing repository name');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
environmentId: number(endpointId),
|
||||||
|
registryId,
|
||||||
|
repository,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDetails<T = RepositoryTagViewModel>(
|
||||||
|
tag: string,
|
||||||
|
select?: (data: RepositoryTagViewModel) => T
|
||||||
|
) {
|
||||||
|
const params = useParams();
|
||||||
|
return useTagDetails(
|
||||||
|
{
|
||||||
|
tag,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
{ staleTime: 60 * 1000, select }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCell<T = RepositoryTagViewModel>(
|
||||||
|
select: (data: RepositoryTagViewModel) => T
|
||||||
|
) {
|
||||||
|
return function Cell({ row }: CellContext<Tag, unknown>) {
|
||||||
|
const detailsQuery = useDetails(row.original.Name, select);
|
||||||
|
|
||||||
|
return detailsQuery.data || '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function number(value: string | undefined) {
|
||||||
|
const num = parseInt(value || '', 10);
|
||||||
|
return Number.isNaN(num) ? undefined : num;
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Tag } from '../types';
|
||||||
|
|
||||||
|
export const helper = createColumnHelper<Tag>();
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { humanize } from '@/portainer/filters/filters';
|
||||||
|
import { trimSHA } from '@/docker/filters/utils';
|
||||||
|
|
||||||
|
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||||
|
|
||||||
|
import { Tag } from '../types';
|
||||||
|
|
||||||
|
import { helper } from './helper';
|
||||||
|
import { buildCell } from './buildCell';
|
||||||
|
import { actions } from './actions';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
buildNameColumn<Tag>(
|
||||||
|
'Name',
|
||||||
|
'portainer.registries.registry.repository.tag',
|
||||||
|
'tag',
|
||||||
|
(item) => item.Name
|
||||||
|
),
|
||||||
|
helper.display({
|
||||||
|
header: 'OS/Architecture',
|
||||||
|
cell: buildCell((model) => `${model.Os}/${model.Architecture}`),
|
||||||
|
}),
|
||||||
|
helper.display({
|
||||||
|
header: 'Image ID',
|
||||||
|
cell: buildCell((model) => trimSHA(model.ImageId)),
|
||||||
|
}),
|
||||||
|
helper.display({
|
||||||
|
header: 'Compressed Size',
|
||||||
|
cell: buildCell((model) => humanize(model.Size)),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function useColumns(advancedFeaturesAvailable: boolean) {
|
||||||
|
return useMemo(
|
||||||
|
() => _.compact([...columns, advancedFeaturesAvailable && actions]),
|
||||||
|
[advancedFeaturesAvailable]
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { ManifestV1, ManifestV2 } from '../../queries/manifest.service';
|
||||||
|
import { ImageConfigs } from '../../queries/getRegistryBlobs';
|
||||||
|
|
||||||
|
import { RepositoryTagViewModel } from './view-model';
|
||||||
|
|
||||||
|
function parseV1History(history: { v1Compatibility: string }[]) {
|
||||||
|
return _.map(history, (item) => JSON.parse(item.v1Compatibility));
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert image configs blob history to manifest v1 history
|
||||||
|
function parseImageConfigsHistory(imageConfigs: ImageConfigs, v2: ManifestV2) {
|
||||||
|
return _.map(imageConfigs.history.reverse(), (item) => ({
|
||||||
|
...item,
|
||||||
|
CreatedBy: item.created_by,
|
||||||
|
// below fields exist in manifest v1 history but not image configs blob
|
||||||
|
id: v2.config.digest,
|
||||||
|
created: imageConfigs.created,
|
||||||
|
docker_version: imageConfigs.docker_version,
|
||||||
|
os: imageConfigs.os,
|
||||||
|
architecture: imageConfigs.architecture,
|
||||||
|
config: imageConfigs.config,
|
||||||
|
container_config: imageConfigs.container_config,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function manifestsToTag({
|
||||||
|
v1,
|
||||||
|
v2,
|
||||||
|
imageConfigs,
|
||||||
|
}: {
|
||||||
|
v1?: ManifestV1;
|
||||||
|
v2: ManifestV2 & { digest: string };
|
||||||
|
imageConfigs?: ImageConfigs;
|
||||||
|
}) {
|
||||||
|
let history = [];
|
||||||
|
let name = '';
|
||||||
|
let os = '';
|
||||||
|
let arch = '';
|
||||||
|
|
||||||
|
if (imageConfigs) {
|
||||||
|
// use info from image configs blob when manifest v1 is not provided by registry
|
||||||
|
os = imageConfigs.os || '';
|
||||||
|
arch = imageConfigs.architecture || '';
|
||||||
|
history = parseImageConfigsHistory(imageConfigs, v2);
|
||||||
|
} else if (v1) {
|
||||||
|
// use info from manifest v1
|
||||||
|
history = parseV1History(v1.history);
|
||||||
|
name = v1.tag;
|
||||||
|
os = _.get(history, '[0].os', '');
|
||||||
|
arch = v1.architecture;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = v2.layers.reduce((size, b) => size + b.size, 0);
|
||||||
|
|
||||||
|
const imageId = v2.config.digest;
|
||||||
|
|
||||||
|
// v2.digest comes from
|
||||||
|
// 1. Docker-Content-Digest header from the v2 response, or
|
||||||
|
// 2. Calculated locally by sha256(v2-response-body)
|
||||||
|
const imageDigest = v2.digest;
|
||||||
|
|
||||||
|
return new RepositoryTagViewModel(
|
||||||
|
name,
|
||||||
|
os,
|
||||||
|
arch,
|
||||||
|
size,
|
||||||
|
imageDigest,
|
||||||
|
imageId,
|
||||||
|
v2,
|
||||||
|
history
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
interface TableMeta {
|
||||||
|
table: 'registry-repository-tags';
|
||||||
|
onUpdate(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTableMeta(meta: unknown): meta is TableMeta {
|
||||||
|
return (
|
||||||
|
!!meta &&
|
||||||
|
typeof meta === 'object' &&
|
||||||
|
'table' in meta &&
|
||||||
|
meta.table === 'registry-repository-tags'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTableMeta(meta: unknown): TableMeta {
|
||||||
|
if (!isTableMeta(meta)) {
|
||||||
|
throw new Error('Invalid table meta');
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface Tag {
|
||||||
|
Name: string;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { createStore } from 'zustand';
|
||||||
|
|
||||||
|
import { RepositoryTagViewModel } from './view-model';
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
updates: Record<string, RepositoryTagViewModel>;
|
||||||
|
setName(originalName: string, value?: RepositoryTagViewModel): void;
|
||||||
|
count: number;
|
||||||
|
getUpdate(originalName: string): RepositoryTagViewModel | undefined;
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const newNamesStore = createStore<Store>()((set, get) => ({
|
||||||
|
updates: {},
|
||||||
|
count: 0,
|
||||||
|
setName(originalName: string, value?: RepositoryTagViewModel) {
|
||||||
|
const { updates } = get();
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
delete updates[originalName];
|
||||||
|
} else {
|
||||||
|
updates[originalName] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ updates, count: Object.keys(updates).length });
|
||||||
|
},
|
||||||
|
getUpdate(originalName: string) {
|
||||||
|
const { updates } = get();
|
||||||
|
return updates[originalName];
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
set({ updates: {}, count: 0 });
|
||||||
|
},
|
||||||
|
}));
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ManifestV2 } from '../../queries/manifest.service';
|
||||||
|
|
||||||
|
export class RepositoryTagViewModel {
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
Os: string;
|
||||||
|
|
||||||
|
Architecture: string;
|
||||||
|
|
||||||
|
Size: number;
|
||||||
|
|
||||||
|
ImageDigest: string;
|
||||||
|
|
||||||
|
ImageId: string;
|
||||||
|
|
||||||
|
ManifestV2: ManifestV2 & { digest: string };
|
||||||
|
|
||||||
|
History: unknown[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
name: string,
|
||||||
|
os: string,
|
||||||
|
arch: string,
|
||||||
|
size: number,
|
||||||
|
imageDigest: string,
|
||||||
|
imageId: string,
|
||||||
|
v2: ManifestV2 & {
|
||||||
|
digest: string;
|
||||||
|
},
|
||||||
|
history: unknown[]
|
||||||
|
) {
|
||||||
|
this.Name = name;
|
||||||
|
this.Os = os || '';
|
||||||
|
this.Architecture = arch || '';
|
||||||
|
this.Size = size || 0;
|
||||||
|
this.ImageDigest = imageDigest || '';
|
||||||
|
this.ImageId = imageId || '';
|
||||||
|
this.ManifestV2 = v2 || {};
|
||||||
|
this.History = history || [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { queryKeys as registriesQueryKeys } from '../../queries/query-keys';
|
||||||
|
import { RegistryId } from '../../types/registry';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
base: (registryId: RegistryId) =>
|
||||||
|
[...registriesQueryKeys.item(registryId), 'repositories'] as const,
|
||||||
|
repository: (registryId: RegistryId, repository: string) =>
|
||||||
|
[...queryKeys.base(registryId), repository] as const,
|
||||||
|
repositoryTags: (registryId: RegistryId, repository: string) =>
|
||||||
|
[...queryKeys.repository(registryId, repository), 'tags'] as const,
|
||||||
|
repositoryTag: (registryId: RegistryId, repository: string, tag: string) =>
|
||||||
|
[...queryKeys.repository(registryId, repository), 'tags', tag] as const,
|
||||||
|
tagDetails: ({
|
||||||
|
registryId,
|
||||||
|
repository,
|
||||||
|
tag,
|
||||||
|
...params
|
||||||
|
}: {
|
||||||
|
repository: string;
|
||||||
|
environmentId?: EnvironmentId;
|
||||||
|
registryId: RegistryId;
|
||||||
|
tag: string;
|
||||||
|
}) =>
|
||||||
|
[
|
||||||
|
...queryKeys.repositoryTag(registryId, repository, tag),
|
||||||
|
'details',
|
||||||
|
params,
|
||||||
|
] as const,
|
||||||
|
};
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { Registry } from '@/react/portainer/registries/types/registry';
|
||||||
|
|
||||||
|
import { manifestsToTag } from '../ItemView/TagsDatatable/manifestsToTag';
|
||||||
|
import { RepositoryTagViewModel } from '../ItemView/TagsDatatable/view-model';
|
||||||
|
|
||||||
|
import { getTagManifestV1, getTagManifestV2 } from './manifest.service';
|
||||||
|
import { ImageConfigs, getRegistryBlob } from './getRegistryBlobs';
|
||||||
|
import { queryKeys } from './queryKeys';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
registryId: Registry['Id'];
|
||||||
|
repository: string;
|
||||||
|
environmentId?: Environment['Id'];
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTagDetails<T = RepositoryTagViewModel>(
|
||||||
|
params: Params,
|
||||||
|
{
|
||||||
|
staleTime = 0,
|
||||||
|
select,
|
||||||
|
}: { select?: (model: RepositoryTagViewModel) => T; staleTime?: number } = {}
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.tagDetails(params),
|
||||||
|
queryFn: () => getTagDetails(params),
|
||||||
|
staleTime,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTagDetails({
|
||||||
|
registryId,
|
||||||
|
environmentId,
|
||||||
|
repository,
|
||||||
|
tag,
|
||||||
|
}: Params) {
|
||||||
|
const params = {
|
||||||
|
id: registryId,
|
||||||
|
endpointId: environmentId,
|
||||||
|
repository,
|
||||||
|
tag,
|
||||||
|
};
|
||||||
|
const [v1, v2] = await Promise.all([
|
||||||
|
getTagManifestV1(params),
|
||||||
|
getTagManifestV2(params),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let useV1 = true;
|
||||||
|
let imageConfigs: ImageConfigs | undefined;
|
||||||
|
try {
|
||||||
|
imageConfigs = await getRegistryBlob({
|
||||||
|
digest: v2.config.digest,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
|
// prefer image configs than manifest v1
|
||||||
|
useV1 = false;
|
||||||
|
} catch (e) {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v1 && v1.schemaVersion === 2) {
|
||||||
|
// Registry returns manifest v2 while we request manifest v1
|
||||||
|
useV1 = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: { v2: typeof v2; v1?: typeof v1; imageConfigs?: ImageConfigs } = {
|
||||||
|
v2,
|
||||||
|
imageConfigs,
|
||||||
|
};
|
||||||
|
if (useV1) {
|
||||||
|
data.v1 = v1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagDetails = manifestsToTag(data);
|
||||||
|
tagDetails.Name = tagDetails.Name || tag;
|
||||||
|
return tagDetails;
|
||||||
|
}
|
Loading…
Reference in New Issue