From 5a20e819d2bfaf04a6b3141b07c4ee09187260ce Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:21:14 +0200 Subject: [PATCH 1/2] regctl: manifestGet Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/regclient/regctl.test.ts | 20 ++++++++++++++++++++ src/regclient/regctl.ts | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/__tests__/regclient/regctl.test.ts b/__tests__/regclient/regctl.test.ts index 94ea0b0..cd647b8 100644 --- a/__tests__/regclient/regctl.test.ts +++ b/__tests__/regclient/regctl.test.ts @@ -20,6 +20,26 @@ import * as semver from 'semver'; import {Exec} from '../../src/exec'; import {Regctl} from '../../src/regclient/regctl'; +describe('manifestGet', () => { + // prettier-ignore + test.each([ + ['moby/moby-bin:latest'], + ['crazymax/undock:latest'], + ['crazymax/diun:4.17.0'], + ])('given %p', async image => { + const regctl = new Regctl(); + const manifest = await regctl.manifestGet({ + image: image, + }); + console.log(`${image}: ${JSON.stringify(manifest, null, 2)}`); + expect(manifest).not.toBeNull(); + expect(manifest?.config).toBeDefined(); + expect(manifest?.config.digest).not.toEqual(''); + expect(manifest?.layers).toBeDefined(); + expect(manifest?.layers.length).toBeGreaterThan(0); + }); +}); + describe('isAvailable', () => { it('checks regctl is available', async () => { const execSpy = jest.spyOn(Exec, 'getExecOutput'); diff --git a/src/regclient/regctl.ts b/src/regclient/regctl.ts index 0c80808..2e2dc9f 100644 --- a/src/regclient/regctl.ts +++ b/src/regclient/regctl.ts @@ -19,10 +19,17 @@ import * as semver from 'semver'; import {Exec} from '../exec'; +import {Manifest} from '../types/oci/manifest'; + export interface RegctlOpts { binPath?: string; } +export interface RegctlManifestGetOpts { + image: string; + platform?: string; +} + export class Regctl { private readonly binPath: string; private _version: string; @@ -34,6 +41,18 @@ export class Regctl { this._versionOnce = false; } + public async manifestGet(opts: RegctlManifestGetOpts): Promise { + return await Exec.getExecOutput(this.binPath, ['manifest', 'get', opts.image, `--platform=${opts.platform ?? 'local'}`, `--format={{json .}}`], { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.trim()); + } + return JSON.parse(res.stdout.trim()); + }); + } + public async isAvailable(): Promise { const ok: boolean = await Exec.getExecOutput(this.binPath, [], { ignoreReturnCode: true, From 4dc0686a1f3f84611892b6044ca7f99999db1cdf Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:44:44 +0200 Subject: [PATCH 2/2] regctl: blobGet func Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/regclient/regctl.test.ts | 49 ++++++++++++++++++++++++++++-- src/regclient/regctl.ts | 18 +++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/__tests__/regclient/regctl.test.ts b/__tests__/regclient/regctl.test.ts index cd647b8..abb79ab 100644 --- a/__tests__/regclient/regctl.test.ts +++ b/__tests__/regclient/regctl.test.ts @@ -20,18 +20,19 @@ import * as semver from 'semver'; import {Exec} from '../../src/exec'; import {Regctl} from '../../src/regclient/regctl'; +import {Image} from '../../src/types/oci/config'; + describe('manifestGet', () => { // prettier-ignore test.each([ - ['moby/moby-bin:latest'], - ['crazymax/undock:latest'], + ['moby/moby-bin:28.1.0-rc.2'], ['crazymax/diun:4.17.0'], ])('given %p', async image => { const regctl = new Regctl(); const manifest = await regctl.manifestGet({ image: image, }); - console.log(`${image}: ${JSON.stringify(manifest, null, 2)}`); + console.log(`${image} manifest: ${JSON.stringify(manifest, null, 2)}`); expect(manifest).not.toBeNull(); expect(manifest?.config).toBeDefined(); expect(manifest?.config.digest).not.toEqual(''); @@ -40,6 +41,48 @@ describe('manifestGet', () => { }); }); +describe('blobGet', () => { + // prettier-ignore + test.each([ + ['moby/moby-bin', 'sha256:234fccbd13fde0ba978a19f728cbdc67e29bc76247ac560822bb6ae5236c0bf0'], + ['crazymax/diun', 'sha256:1e4881f66e0ec0f1710b837002107050bbbc0a231d8a42d7f422b56a139900bb'], + ])('given %p', async (repo, digest) => { + const regctl = new Regctl(); + const blob = await regctl.blobGet({ + repository: repo, + digest: digest + }); + expect(blob).toBeDefined(); + console.log(`${repo}:@${digest} blob: ${JSON.stringify(JSON.parse(blob), null, 2)}`); + }); +}); + +describe('image config', () => { + // prettier-ignore + test.each([ + ['moby/moby-bin:28.1.0-rc.2'], + ['crazymax/diun:4.17.0'], + ])('given %p', async image => { + const regctl = new Regctl(); + const manifest = await regctl.manifestGet({ + image: image, + }); + expect(manifest).not.toBeNull(); + expect(manifest?.config).toBeDefined(); + expect(manifest?.config.digest).not.toEqual(''); + const blob = await regctl.blobGet({ + repository: image, // image works as well + digest: manifest?.config.digest + }); + const imageConfig = JSON.parse(blob); + console.log(`${image} config: ${JSON.stringify(imageConfig, null, 2)}`); + expect(imageConfig).not.toBeNull(); + expect(imageConfig.config).toBeDefined(); + expect(imageConfig?.config?.Labels).toBeDefined(); + expect(Object.keys(imageConfig?.config?.Labels || {}).length).toBeGreaterThan(0); + }); +}); + describe('isAvailable', () => { it('checks regctl is available', async () => { const execSpy = jest.spyOn(Exec, 'getExecOutput'); diff --git a/src/regclient/regctl.ts b/src/regclient/regctl.ts index 2e2dc9f..b784d9d 100644 --- a/src/regclient/regctl.ts +++ b/src/regclient/regctl.ts @@ -25,6 +25,11 @@ export interface RegctlOpts { binPath?: string; } +export interface RegctlBlobGetOpts { + repository: string; + digest: string; +} + export interface RegctlManifestGetOpts { image: string; platform?: string; @@ -41,6 +46,19 @@ export class Regctl { this._versionOnce = false; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public async blobGet(opts: RegctlBlobGetOpts): Promise { + return await Exec.getExecOutput(this.binPath, ['blob', 'get', opts.repository, opts.digest], { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.trim()); + } + return res.stdout; + }); + } + public async manifestGet(opts: RegctlManifestGetOpts): Promise { return await Exec.getExecOutput(this.binPath, ['manifest', 'get', opts.image, `--platform=${opts.platform ?? 'local'}`, `--format={{json .}}`], { ignoreReturnCode: true,