mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 09:37:22 +00:00
fix: handle sha in extension image links (#15437)
* fix: handle sha in extension image links using sha link is not working in "install custom extension..." fixes https://github.com/podman-desktop/podman-desktop/issues/15434 Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
parent
a3cea8665d
commit
b0aa664265
2 changed files with 159 additions and 5 deletions
|
|
@ -374,6 +374,96 @@ describe('extractImageDataFromImageName', () => {
|
|||
expect(nameAndTag.tag).toBe('latest');
|
||||
expect(nameAndTag.name).toBe('level1/level2/level3/level4/myimage');
|
||||
});
|
||||
|
||||
test('digest format on library image', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'httpd@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('index.docker.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://index.docker.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('library/httpd');
|
||||
});
|
||||
|
||||
test('digest format on namespaced image', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'foo/bar@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('index.docker.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://index.docker.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('foo/bar');
|
||||
});
|
||||
|
||||
test('digest format with explicit registry', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'quay.io/org/image@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('quay.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://quay.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('org/image');
|
||||
});
|
||||
|
||||
test('digest format with localhost and port', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'localhost:5000/myimage@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('localhost:5000');
|
||||
expect(nameAndTag.registryURL).toBe('https://localhost:5000/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('myimage');
|
||||
});
|
||||
|
||||
test('tag and digest format on library image', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'httpd:2.4@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('index.docker.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://index.docker.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('library/httpd');
|
||||
});
|
||||
|
||||
test('tag and digest format on namespaced image', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'foo/bar:v1.0.0@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('index.docker.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://index.docker.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('foo/bar');
|
||||
});
|
||||
|
||||
test('tag and digest format with explicit registry (ghcr.io example)', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'ghcr.io/podman-desktop/pd-extension-quadlet:0.11.0@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('ghcr.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://ghcr.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef');
|
||||
expect(nameAndTag.name).toBe('podman-desktop/pd-extension-quadlet');
|
||||
});
|
||||
|
||||
test('tag and digest format with localhost and port', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'localhost:5000/myimage:latest@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('localhost:5000');
|
||||
expect(nameAndTag.registryURL).toBe('https://localhost:5000/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789');
|
||||
expect(nameAndTag.name).toBe('myimage');
|
||||
});
|
||||
|
||||
test('tag and digest format with multi-level path', () => {
|
||||
const nameAndTag = imageRegistry.extractImageDataFromImageName(
|
||||
'quay.io/org/team/image:v2.1.0@sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210',
|
||||
);
|
||||
expect(nameAndTag.registry).toBe('quay.io');
|
||||
expect(nameAndTag.registryURL).toBe('https://quay.io/v2');
|
||||
expect(nameAndTag.tag).toBe('sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210');
|
||||
expect(nameAndTag.name).toBe('org/team/image');
|
||||
});
|
||||
});
|
||||
|
||||
test('expect getImageConfigLabels works', async () => {
|
||||
|
|
@ -445,6 +535,53 @@ test('expect getManifestFromImageName works', async () => {
|
|||
expect(manifest).toStrictEqual(imageRegistryManifestJson);
|
||||
});
|
||||
|
||||
test('expect getManifestFromImageName works with digest', async () => {
|
||||
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
|
||||
spyGetAuthInfo.mockResolvedValue({ authUrl: 'http://foobar', scheme: 'bearer' });
|
||||
|
||||
const spyGetToken = vi.spyOn(imageRegistry, 'getToken');
|
||||
spyGetToken.mockResolvedValue('12345');
|
||||
|
||||
const digest = 'sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
|
||||
const handlers = [
|
||||
http.get(`https://my-podman-desktop-fake-registry.io/v2/my/extension/manifests/${digest}`, () =>
|
||||
HttpResponse.json(imageRegistryManifestJson),
|
||||
),
|
||||
];
|
||||
server = setupServer(...handlers);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
|
||||
const manifest = await imageRegistry.getManifestFromImageName(
|
||||
`my-podman-desktop-fake-registry.io/my/extension@${digest}`,
|
||||
);
|
||||
expect(manifest).toStrictEqual(imageRegistryManifestJson);
|
||||
});
|
||||
|
||||
test('expect getManifestFromImageName works with tag and digest', async () => {
|
||||
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
|
||||
spyGetAuthInfo.mockResolvedValue({ authUrl: 'http://foobar', scheme: 'bearer' });
|
||||
|
||||
const spyGetToken = vi.spyOn(imageRegistry, 'getToken');
|
||||
spyGetToken.mockResolvedValue('12345');
|
||||
|
||||
const digest = 'sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef';
|
||||
|
||||
const handlers = [
|
||||
http.get(`https://ghcr.io/v2/podman-desktop/pd-extension-quadlet/manifests/${digest}`, () =>
|
||||
HttpResponse.json(imageRegistryManifestJson),
|
||||
),
|
||||
];
|
||||
server = setupServer(...handlers);
|
||||
server.listen({ onUnhandledRequest: 'error' });
|
||||
|
||||
// Tag is ignored when digest is present, digest is used for manifest lookup
|
||||
const manifest = await imageRegistry.getManifestFromImageName(
|
||||
`ghcr.io/podman-desktop/pd-extension-quadlet:0.11.0@${digest}`,
|
||||
);
|
||||
expect(manifest).toStrictEqual(imageRegistryManifestJson);
|
||||
});
|
||||
|
||||
test('expect downloadAndExtractImage works', async () => {
|
||||
// need to mock the http request
|
||||
const spyGetAuthInfo = vi.spyOn(imageRegistry, 'getAuthInfo');
|
||||
|
|
|
|||
|
|
@ -393,13 +393,30 @@ export class ImageRegistry {
|
|||
throw new Error(`Invalid image name: ${imageName}`);
|
||||
}
|
||||
|
||||
// do we have a tag at the end with last
|
||||
// Check if image is referenced by digest (@sha256:hash) instead of tag
|
||||
// Format can be: name:tag, name@sha256:hash, or name:tag@sha256:hash
|
||||
let tag = 'latest';
|
||||
const lastColon = imageName.lastIndexOf(':');
|
||||
const atIndex = imageName.indexOf('@');
|
||||
const lastSlash = imageName.lastIndexOf('/');
|
||||
if (lastColon !== -1 && lastColon > lastSlash) {
|
||||
tag = imageName.substring(lastColon + 1);
|
||||
imageName = imageName.substring(0, lastColon);
|
||||
|
||||
if (atIndex !== -1 && atIndex > lastSlash) {
|
||||
// Image uses digest format: name@sha256:hash or name:tag@sha256:hash
|
||||
// The digest is the authoritative reference
|
||||
tag = imageName.substring(atIndex + 1);
|
||||
imageName = imageName.substring(0, atIndex);
|
||||
|
||||
// Remove any tag that might be present before the @ (e.g., :0.11.0 in name:0.11.0@sha256:...)
|
||||
const lastColon = imageName.lastIndexOf(':');
|
||||
if (lastColon !== -1 && lastColon > lastSlash) {
|
||||
imageName = imageName.substring(0, lastColon);
|
||||
}
|
||||
} else {
|
||||
// Check for tag format: name:tag
|
||||
const lastColon = imageName.lastIndexOf(':');
|
||||
if (lastColon !== -1 && lastColon > lastSlash) {
|
||||
tag = imageName.substring(lastColon + 1);
|
||||
imageName = imageName.substring(0, lastColon);
|
||||
}
|
||||
}
|
||||
|
||||
let registry = '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue