From c7125266f6be4d70305cfb6f2fc19e110e7d83fb Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sat, 6 Jan 2024 04:02:09 +0700 Subject: [PATCH] fix(registries): retag image [EE-6456] (#10836) --- .../portainer/registries/queries/build-url.ts | 4 + .../repositories/queries/getRegistryBlobs.ts | 59 +++++++ .../repositories/queries/manifest.service.ts | 146 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 app/react/portainer/registries/repositories/queries/getRegistryBlobs.ts create mode 100644 app/react/portainer/registries/repositories/queries/manifest.service.ts diff --git a/app/react/portainer/registries/queries/build-url.ts b/app/react/portainer/registries/queries/build-url.ts index 266c865d5..3f76215bd 100644 --- a/app/react/portainer/registries/queries/build-url.ts +++ b/app/react/portainer/registries/queries/build-url.ts @@ -9,3 +9,7 @@ export function buildUrl(registryId: RegistryId) { return base; } + +export function buildProxyUrl(registryId: RegistryId) { + return `${buildUrl(registryId)}/v2`; +} diff --git a/app/react/portainer/registries/repositories/queries/getRegistryBlobs.ts b/app/react/portainer/registries/repositories/queries/getRegistryBlobs.ts new file mode 100644 index 000000000..26975337f --- /dev/null +++ b/app/react/portainer/registries/repositories/queries/getRegistryBlobs.ts @@ -0,0 +1,59 @@ +import axios, { parseAxiosError } from '@/portainer/services/axios'; + +import { EnvironmentId } from '../../../environments/types'; +import { RegistryId } from '../../types/registry'; +import { buildProxyUrl } from '../../queries/build-url'; + +/** + * TODO: This file is copy of an old angular service, need to migrate it to use axios + */ + +interface Params { + id: RegistryId; + repository: string; + digest: string; + endpointId?: EnvironmentId; +} + +function buildUrl(params: Params) { + return `${buildProxyUrl(params.id)}/${params.repository}/blobs/${ + params.digest + }`; +} + +export interface ImageConfigs { + architecture: string; + config: { + Env: string[]; + Entrypoint: string[]; + WorkingDir: string; + Labels: Record; + ArgsEscaped: boolean; + }; + created: string; + history: Array<{ + created: string; + created_by: string; + empty_layer?: boolean; + comment?: string; + }>; + os: string; + rootfs: { + type: string; + diff_ids: string[]; + }; + docker_version?: string; + container_config?: unknown; +} + +export async function getRegistryBlob(params: Params) { + try { + const { data } = await axios.get(buildUrl(params), { + params: { endpointId: params.endpointId }, + }); + + return typeof data === 'string' ? JSON.parse(data) : data; + } catch (err) { + throw parseAxiosError(err); + } +} diff --git a/app/react/portainer/registries/repositories/queries/manifest.service.ts b/app/react/portainer/registries/repositories/queries/manifest.service.ts new file mode 100644 index 000000000..3a15f2ea4 --- /dev/null +++ b/app/react/portainer/registries/repositories/queries/manifest.service.ts @@ -0,0 +1,146 @@ +import { Sha256 } from '@aws-crypto/sha256-js'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { EnvironmentId } from '@/react/portainer/environments/types'; + +import { RegistryId } from '../../types/registry'; +import { buildProxyUrl } from '../../queries/build-url'; + +/** + * TODO: This file is copy of an old angular service, need to migrate it to use axios + */ + +interface Params { + id: RegistryId; + repository: string; + tag: string; + endpointId?: EnvironmentId; +} + +function buildUrl({ id, repository, tag }: Params) { + return `${buildProxyUrl(id)}/${repository}/manifests/${tag}`; +} + +export interface ManifestV1 { + schemaVersion: number; + name: string; + tag: string; + architecture: string; + fsLayers: { + blobSum: string; + }[]; + history: { + v1Compatibility: string; + }[]; + signatures: { + header: { + jwk: { + crv: string; + kid: string; + kty: string; + x: string; + y: string; + }; + alg: string; + }; + signature: string; + protected: string; + }[]; +} + +export async function getTagManifestV1(params: Params) { + try { + const { data } = await axios.get(buildUrl(params), { + params: { endpointId: params.endpointId }, + headers: { + Accept: 'application/vnd.docker.distribution.manifest.v1+json', + 'Cache-Control': 'no-cache', + 'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT', + }, + }); + + return data; + } catch (err) { + throw parseAxiosError(err); + } +} + +export interface ManifestV2 { + schemaVersion: number; + mediaType: string; + config: { + mediaType: string; + size: number; + digest: string; + }; + layers: { + mediaType: string; + size: number; + digest: string; + }[]; +} + +export async function getTagManifestV2( + params: Params +): Promise { + try { + const { data: resultText, headers } = await axios.get( + buildUrl(params), + { + params: { endpointId: params.endpointId }, + headers: { + Accept: 'application/vnd.docker.distribution.manifest.v2+json', + 'Cache-Control': 'no-cache', + 'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT', + }, + // To support ECR we need text response + responseType: 'text', + // see https://github.com/axios/axios/issues/907#issuecomment-506924322 for text response + transformResponse: [(data) => data], + } + ); + + const result = JSON.parse(resultText); + // ECR does not return the digest header + result.digest = + headers['docker-content-digest'] || (await sha256(resultText)); + + return result; + } catch (err) { + throw parseAxiosError(err); + } +} + +export async function updateManifest(params: Params, data: unknown) { + try { + await axios.put(buildUrl(params), data, { + headers: { + 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json', + }, + params: { endpointId: params.endpointId }, + }); + } catch (err) { + throw parseAxiosError(err); + } +} + +export async function deleteManifest(params: { + id: RegistryId; + repository: string; + tag: string; + endpointId?: EnvironmentId; +}) { + try { + await axios.delete(buildUrl(params), { + params: { endpointId: params.endpointId }, + }); + } catch (err) { + throw parseAxiosError(err); + } +} + +function sha256(string: string) { + const hash = new Sha256(); + hash.update(string); + return hash.digest(); +}