chore: add Digest to ImageInfo (#6817)

* chore: add Digest to ImageInfo

### What does this PR do?

* Adds ability to get Digest from podman API calls.
* Already within PD list images calls, now we can get it via ImageInfo

### Screenshot / video of UI

<!-- If this PR is changing UI, please include
screenshots or screencasts showing the difference -->

N/A

### What issues does this PR fix or reference?

<!-- Include any related issues from Podman Desktop
repository (or from another issue tracker). -->

Closes https://github.com/containers/podman-desktop/issues/6816

### How to test this PR?

<!-- Please explain steps to verify the functionality,
do not forget to provide unit/component tests -->

- [X] Tests are covering the bug fix or the new feature

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

* add Digest for all list images calls

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

* update

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

* update

Signed-off-by: Charlie Drage <charlie@charliedrage.com>

---------

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
Charlie Drage 2024-04-17 17:42:29 -04:00 committed by GitHub
parent c1359eea5d
commit 70c19fc31a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 91 additions and 1 deletions

View file

@ -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;

View file

@ -26,6 +26,7 @@ export interface ImageInfo extends Dockerode.ImageInfo {
engineName: string;
History?: string[];
isManifest?: boolean;
Digest: string;
}
export interface BuildImageOptions {

View file

@ -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 = [
{

View file

@ -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}`,
}));
}),
);

View file

@ -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',

View file

@ -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);

View file

@ -57,6 +57,7 @@ const myImage: ImageInfo = {
VirtualSize: 0,
SharedSize: 0,
Containers: 0,
Digest: 'sha256:myImage',
};
const myNoneNameImage: ImageInfo = {