diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index 8bb7fbd1423..e59714c15d5 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -1868,6 +1868,7 @@ declare module '@podman-desktop/api' { Labels: { [label: string]: string }; Containers: number; History?: string[]; + Digest: string; // isManifest will be returned and set to true if the image is identified to be a manifest list isManifest?: boolean; diff --git a/packages/main/src/plugin/api/image-info.ts b/packages/main/src/plugin/api/image-info.ts index 771fac9ee64..ee981ffdb45 100644 --- a/packages/main/src/plugin/api/image-info.ts +++ b/packages/main/src/plugin/api/image-info.ts @@ -26,6 +26,7 @@ export interface ImageInfo extends Dockerode.ImageInfo { engineName: string; History?: string[]; isManifest?: boolean; + Digest: string; } export interface BuildImageOptions { diff --git a/packages/main/src/plugin/container-registry.spec.ts b/packages/main/src/plugin/container-registry.spec.ts index 46ba6b19dfb..0e43105212e 100644 --- a/packages/main/src/plugin/container-registry.spec.ts +++ b/packages/main/src/plugin/container-registry.spec.ts @@ -3761,6 +3761,7 @@ describe('listImages', () => { Id: 'dummyImageId', engineId: 'dummyId', engineName: 'dummyName', + Digest: 'sha256:dummyImageId', }); }); }); @@ -3830,6 +3831,74 @@ test('expect images with podmanListImages to also include History as well as eng expect(image.History).toStrictEqual(['history1', 'history2']); }); +test('expect images with podmanListImages to also include Digest as engineId and engineName', async () => { + const imagesList = [ + { + Id: 'dummyImageId', + Digest: 'dummyDigest', + }, + ]; + + nock('http://localhost').get('/v4.2.0/libpod/images/json').reply(200, imagesList); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + // set provider + containerRegistry.addInternalProvider('podman', { + name: 'podman', + id: 'podman1', + api, + libpodApi: api, + connection: { + type: 'podman', + }, + } as unknown as InternalContainerProvider); + + const images = await containerRegistry.podmanListImages(); + // ensure the field are correct + expect(images).toBeDefined(); + expect(images).toHaveLength(1); + const image = images[0]; + expect(image.engineId).toBe('podman1'); + expect(image.engineName).toBe('podman'); + expect(image.Digest).toBe('dummyDigest'); +}); + +test('If image does not have Digest in list images, expect the Digest to be sha256:ID', async () => { + // Purposely be missing Digest, it should return Digest as sha256:ID + // this is because the compat API does not provide Digest return. + const imagesList = [ + { + Id: 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', + }, + ]; + + nock('http://localhost').get('/v4.2.0/libpod/images/json').reply(200, imagesList); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + // set provider + containerRegistry.addInternalProvider('podman', { + name: 'podman', + id: 'podman1', + api, + libpodApi: api, + connection: { + type: 'podman', + }, + } as unknown as InternalContainerProvider); + + const images = await containerRegistry.podmanListImages(); + + // ensure the field are correct + expect(images).toBeDefined(); + expect(images).toHaveLength(1); + const image = images[0]; + expect(image.engineId).toBe('podman1'); + expect(image.engineName).toBe('podman'); + expect(image.Digest).toBe('sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2'); +}); + test('expect to fall back to compat api images if podman provider does not have libpodApi', async () => { const imagesList = [ { diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index ebf41f05127..d8193b981cc 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -582,7 +582,12 @@ export class ContainerProviderRegistry { } const images = await provider.api.listImages({ all: false }); return images.map(image => { - const imageInfo: ImageInfo = { ...image, engineName: provider.name, engineId: provider.id }; + const imageInfo: ImageInfo = { + ...image, + engineName: provider.name, + engineId: provider.id, + Digest: `sha256:${image.Id}`, + }; return imageInfo; }); } catch (error) { @@ -641,6 +646,10 @@ export class ContainerProviderRegistry { // NOTE: This is a workaround until we have a better way to determine if an image is a manifest // and may result in false positives until issue: https://github.com/containers/podman/issues/22184 is resolved isManifest: guessIsManifest(image, provider.connection.type), + + // Compat API provider does not add the Digest field. + // if it is missing, add it as 'sha256:image.Id' + Digest: image.Digest || `sha256:${image.Id}`, })); }), ); diff --git a/packages/main/src/plugin/image-checker.spec.ts b/packages/main/src/plugin/image-checker.spec.ts index 5ccabc12634..fb02b79009d 100644 --- a/packages/main/src/plugin/image-checker.spec.ts +++ b/packages/main/src/plugin/image-checker.spec.ts @@ -147,6 +147,7 @@ suite('image checker module', () => { SharedSize: 1, Labels: {}, Containers: 1, + Digest: 'sha256:id', }; const result = await imageChecker.check(providers[0].id, imageInfo); expect(result).toBeDefined(); @@ -168,6 +169,7 @@ suite('image checker module', () => { SharedSize: 1, Labels: {}, Containers: 1, + Digest: 'sha256:id', }; await expect(() => imageChecker.check('unknown-id', imageInfo)).rejects.toThrow( 'provider not found with id unknown-id', diff --git a/packages/main/src/plugin/util/manifest.spec.ts b/packages/main/src/plugin/util/manifest.spec.ts index d84f285d78d..fb009f1f580 100644 --- a/packages/main/src/plugin/util/manifest.spec.ts +++ b/packages/main/src/plugin/util/manifest.spec.ts @@ -36,6 +36,7 @@ describe('guessIsManifest function', () => { VirtualSize: 40 * 1024, // 40KB (less than 50KB threshold) SharedSize: 0, Containers: 0, + Digest: 'sha256:manifestImage', }; expect(guessIsManifest(manifestImage, 'podman')).toBe(true); @@ -55,6 +56,7 @@ describe('guessIsManifest function', () => { VirtualSize: 2000000, // 2MB SharedSize: 0, Containers: 0, + Digest: 'sha256:largeImage', }; expect(guessIsManifest(largeImage, 'podman')).toBe(false); @@ -74,6 +76,7 @@ describe('guessIsManifest function', () => { VirtualSize: 500000, // 500KB SharedSize: 0, Containers: 0, + Digest: 'sha256:labeledImage', }; expect(guessIsManifest(labeledImage, 'podman')).toBe(false); @@ -93,6 +96,7 @@ describe('guessIsManifest function', () => { VirtualSize: 500000, // 500KB SharedSize: 0, Containers: 0, + Digest: 'sha256:noTagImage', }; expect(guessIsManifest(noTagImage, 'podman')).toBe(false); @@ -112,6 +116,7 @@ describe('guessIsManifest function', () => { VirtualSize: 500000, // 500KB SharedSize: 0, Containers: 0, + Digest: 'sha256:noDigestImage', }; expect(guessIsManifest(noDigestImage, 'podman')).toBe(false); @@ -135,6 +140,7 @@ describe('guessIsManifest function', () => { SharedSize: 0, Containers: 0, History: ['testdomain.io/library/hello:latest'], + Digest: 'sha256:ee301c921b8aadc002973b2e0c3da17d701dcd994b606769a7e6eaa100b81d44', }; // Should be false @@ -156,6 +162,7 @@ test('expect to fail even if engine name does not equal podman', () => { VirtualSize: 40 * 1024, // 40KB (less than 50KB threshold) SharedSize: 0, Containers: 0, + Digest: 'sha256:manifestImage', }; expect(guessIsManifest(manifestImage, 'foobar')).toBe(false); diff --git a/packages/renderer/src/lib/image/ImageDetails.spec.ts b/packages/renderer/src/lib/image/ImageDetails.spec.ts index b6642bf9d27..5038194132a 100644 --- a/packages/renderer/src/lib/image/ImageDetails.spec.ts +++ b/packages/renderer/src/lib/image/ImageDetails.spec.ts @@ -57,6 +57,7 @@ const myImage: ImageInfo = { VirtualSize: 0, SharedSize: 0, Containers: 0, + Digest: 'sha256:myImage', }; const myNoneNameImage: ImageInfo = {