mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-05-24 02:08:24 +00:00
feat: add inspectManifest API endpoint (#6812)
* feat: add inspectManifest API endpoint ### What does this PR do? * Adds the inspectManifest API endpoint so we can retrieve information regarding a manifest from the podman libpodApi endpoint ### Screenshot / video of UI <!-- If this PR is changing UI, please include screenshots or screencasts showing the difference --> N/A, it's an API call. ### 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/6793 ### 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 Tests cover, otherwise you could also put the following (after creating ANY manifest, in a svelte file:) ```ts // List all the images const images = await window.listImages(); // Get all the ones that have isManifest set to true const manifestImages = images.filter(image => image.isManifest); // For each manifestImages use inspectManifest to get the information (passing in engineId as well) const imageInfoPromises = manifestImages.map(image => window.inspectManifest(image.engineId, image.Id)); // Wait for all the promises to resolve const imageInfos = await Promise.all(imageInfoPromises); // Consoel log each one imageInfos.forEach(imageInfo => { console.log(imageInfo); }); ``` Signed-off-by: Charlie Drage <charlie@charliedrage.com> * do not use osFeatures and osVersion as not used often Signed-off-by: Charlie Drage <charlie@charliedrage.com> --------- Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
parent
13cac8946f
commit
1cb85aef18
9 changed files with 211 additions and 4 deletions
19
packages/extension-api/src/extension-api.d.ts
vendored
19
packages/extension-api/src/extension-api.d.ts
vendored
|
|
@ -294,6 +294,24 @@ declare module '@podman-desktop/api' {
|
|||
// Provider to use for the manifest creation, if not, we will try to select the first one available (similar to podCreate)
|
||||
provider?: ContainerProviderConnection;
|
||||
}
|
||||
export interface ManifestInspectInfo {
|
||||
engineId: string;
|
||||
engineName: string;
|
||||
manifests: {
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
platform: {
|
||||
architecture: string;
|
||||
features?: string[];
|
||||
os: string;
|
||||
variant?: string;
|
||||
};
|
||||
size: number;
|
||||
urls?: string[];
|
||||
}[];
|
||||
mediaType: string;
|
||||
schemaVersion: number;
|
||||
}
|
||||
|
||||
export interface KubernetesProviderConnectionEndpoint {
|
||||
apiURL: string;
|
||||
|
|
@ -3385,6 +3403,7 @@ declare module '@podman-desktop/api' {
|
|||
|
||||
// Manifest related methods
|
||||
export function createManifest(options: ManifestCreateOptions): Promise<{ engineId: string; Id: string }>;
|
||||
export function inspectManifest(engineId: string, id: string): Promise<ManifestInspectInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,3 +27,22 @@ export interface ManifestCreateOptions {
|
|||
// Provider to use for the manifest creation, if not, we will try to select the first one available (similar to podCreate)
|
||||
provider?: ContainerProviderConnection;
|
||||
}
|
||||
|
||||
export interface ManifestInspectInfo {
|
||||
engineId: string;
|
||||
engineName: string;
|
||||
manifests: {
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
platform: {
|
||||
architecture: string;
|
||||
features?: string[];
|
||||
os: string;
|
||||
variant?: string;
|
||||
};
|
||||
size: number;
|
||||
urls?: string[];
|
||||
}[];
|
||||
mediaType: string;
|
||||
schemaVersion: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4574,3 +4574,70 @@ test('if configuration setting is disabled for using libpodApi, it should fall b
|
|||
expect(image.engineId).toBe('podman1');
|
||||
expect(image.engineName).toBe('podman');
|
||||
});
|
||||
|
||||
test('check that inspectManifest returns information from libPod.podmanInspectManifest', async () => {
|
||||
const inspectManifestMock = vi.fn().mockResolvedValue({
|
||||
engineId: 'podman1',
|
||||
engineName: 'podman',
|
||||
manifests: [
|
||||
{
|
||||
digest: 'digest',
|
||||
mediaType: 'mediaType',
|
||||
platform: {
|
||||
architecture: 'architecture',
|
||||
features: [],
|
||||
os: 'os',
|
||||
variant: 'variant',
|
||||
},
|
||||
size: 100,
|
||||
urls: ['url1', 'url2'],
|
||||
},
|
||||
],
|
||||
mediaType: 'mediaType',
|
||||
schemaVersion: 1,
|
||||
});
|
||||
|
||||
const fakeLibPod = {
|
||||
podmanInspectManifest: inspectManifestMock,
|
||||
} as unknown as LibPod;
|
||||
|
||||
const api = new Dockerode({ protocol: 'http', host: 'localhost' });
|
||||
|
||||
containerRegistry.addInternalProvider('podman1', {
|
||||
name: 'podman',
|
||||
id: 'podman1',
|
||||
api,
|
||||
libpodApi: fakeLibPod,
|
||||
connection: {
|
||||
type: 'podman',
|
||||
},
|
||||
} as unknown as InternalContainerProvider);
|
||||
|
||||
const result = await containerRegistry.inspectManifest('podman1', 'manifestId');
|
||||
|
||||
// Expect that inspectManifest was called with manifestId
|
||||
expect(inspectManifestMock).toBeCalledWith('manifestId');
|
||||
|
||||
// Check the results are as expected
|
||||
expect(result).toBeDefined();
|
||||
expect(result.engineId).toBe('podman1');
|
||||
expect(result.engineName).toBe('podman');
|
||||
expect(result.manifests).toBeDefined();
|
||||
});
|
||||
|
||||
test('inspectManifest should fail if libpod is missing from the provider', async () => {
|
||||
const api = new Dockerode({ protocol: 'http', host: 'localhost' });
|
||||
|
||||
containerRegistry.addInternalProvider('podman1', {
|
||||
name: 'podman',
|
||||
id: 'podman1',
|
||||
api,
|
||||
connection: {
|
||||
type: 'podman',
|
||||
},
|
||||
} as unknown as InternalContainerProvider);
|
||||
|
||||
await expect(() => containerRegistry.inspectManifest('podman1', 'manifestId')).rejects.toThrowError(
|
||||
'LibPod is not supported by this engine',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import type { ContainerStatsInfo } from './api/container-stats-info.js';
|
|||
import type { HistoryInfo } from './api/history-info.js';
|
||||
import type { BuildImageOptions, ImageInfo, ListImagesOptions, PodmanListImagesOptions } from './api/image-info.js';
|
||||
import type { ImageInspectInfo } from './api/image-inspect-info.js';
|
||||
import type { ManifestCreateOptions } from './api/manifest-info.js';
|
||||
import type { ManifestCreateOptions, ManifestInspectInfo } from './api/manifest-info.js';
|
||||
import type { NetworkInspectInfo } from './api/network-info.js';
|
||||
import type { PodCreateOptions, PodInfo, PodInspectInfo } from './api/pod-info.js';
|
||||
import type { ProviderContainerConnectionInfo } from './api/provider-info.js';
|
||||
|
|
@ -1324,6 +1324,22 @@ export class ContainerProviderRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
async inspectManifest(engineId: string, manifestId: string): Promise<ManifestInspectInfo> {
|
||||
let telemetryOptions = {};
|
||||
try {
|
||||
const libPod = this.getMatchingPodmanEngineLibPod(engineId);
|
||||
if (!libPod) {
|
||||
throw new Error('No podman provider with a running engine');
|
||||
}
|
||||
return await libPod.podmanInspectManifest(manifestId);
|
||||
} catch (error) {
|
||||
telemetryOptions = { error: error };
|
||||
throw error;
|
||||
} finally {
|
||||
this.telemetryService.track('inspectManifest', telemetryOptions);
|
||||
}
|
||||
}
|
||||
|
||||
async replicatePodmanContainer(
|
||||
source: { engineId: string; id: string },
|
||||
target: { engineId: string },
|
||||
|
|
|
|||
|
|
@ -218,3 +218,44 @@ test('Check using libpod/manifests/ endpoint', async () => {
|
|||
});
|
||||
expect(manifest.Id).toBe('testId1');
|
||||
});
|
||||
|
||||
test('Check using libpod/manifests/{name}/json endpoint', async () => {
|
||||
// Below is the example return output from
|
||||
// https://docs.podman.io/en/latest/_static/api.html?version=v4.2#tag/manifests/operation/ManifestInspectLibpod
|
||||
const mockJsonManifest = {
|
||||
manifests: [
|
||||
{
|
||||
digest: 'sha256:1234567890',
|
||||
mediaType: 'application/vnd.docker.distribution.manifest.v2+json',
|
||||
platform: {
|
||||
architecture: 'amd64',
|
||||
features: [],
|
||||
os: 'linux',
|
||||
'os.features': [],
|
||||
'os.version': '',
|
||||
variant: '',
|
||||
},
|
||||
size: 0,
|
||||
urls: [],
|
||||
},
|
||||
],
|
||||
mediaType: 'application/vnd.docker.distribution.manifest.v2+json',
|
||||
schemaVersion: 2,
|
||||
};
|
||||
|
||||
nock('http://localhost').get('/v4.2.0/libpod/manifests/name1/json').reply(200, mockJsonManifest);
|
||||
const api = new Dockerode({ protocol: 'http', host: 'localhost' });
|
||||
const manifest = await (api as unknown as LibPod).podmanInspectManifest('name1');
|
||||
|
||||
// Check manifest information returned
|
||||
expect(manifest.mediaType).toBe('application/vnd.docker.distribution.manifest.v2+json');
|
||||
expect(manifest.schemaVersion).toBe(2);
|
||||
expect(manifest.manifests.length).toBe(1);
|
||||
|
||||
// Check manifest.manifests
|
||||
expect(manifest.manifests[0].mediaType).toBe('application/vnd.docker.distribution.manifest.v2+json');
|
||||
expect(manifest.manifests[0].platform.architecture).toBe('amd64');
|
||||
expect(manifest.manifests[0].platform.os).toBe('linux');
|
||||
expect(manifest.manifests[0].size).toBe(0);
|
||||
expect(manifest.manifests[0].urls).toEqual([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type { ManifestCreateOptions } from '@podman-desktop/api';
|
||||
import type { ManifestCreateOptions, ManifestInspectInfo } from '@podman-desktop/api';
|
||||
import Dockerode from 'dockerode';
|
||||
|
||||
import type { ImageInfo, PodmanListImagesOptions } from '../api/image-info.js';
|
||||
|
|
@ -352,6 +352,7 @@ export interface LibPod {
|
|||
getImages(options: GetImagesOptions): Promise<NodeJS.ReadableStream>;
|
||||
podmanListImages(options?: PodmanListImagesOptions): Promise<ImageInfo[]>;
|
||||
podmanCreateManifest(manifestOptions: ManifestCreateOptions): Promise<{ engineId: string; Id: string }>;
|
||||
podmanInspectManifest(manifestName: string): Promise<ManifestInspectInfo>;
|
||||
}
|
||||
|
||||
// tweak Dockerode by adding the support of libpod API
|
||||
|
|
@ -830,5 +831,33 @@ export class LibpodDockerode {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
// add inspectManifest
|
||||
prototypeOfDockerode.podmanInspectManifest = function (manifestName: string): Promise<unknown> {
|
||||
// make sure encodeURI component for the name ex. domain.com/foo/bar:latest
|
||||
const encodedManifestName = encodeURIComponent(manifestName);
|
||||
|
||||
const optsf = {
|
||||
path: `/v4.2.0/libpod/manifests/${encodedManifestName}/json`,
|
||||
method: 'GET',
|
||||
|
||||
// Match the status codes from https://docs.podman.io/en/latest/_static/api.html#tag/manifests/operation/ManifestInspectLibpod
|
||||
statusCodes: {
|
||||
200: true,
|
||||
404: 'no such manifest',
|
||||
500: 'server error',
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.modem.dial(optsf, (err: unknown, data: unknown) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1052,6 +1052,9 @@ export class ExtensionLoader {
|
|||
): Promise<{ engineId: string; Id: string }> {
|
||||
return containerProviderRegistry.createManifest(manifestOptions);
|
||||
},
|
||||
inspectManifest(engineId: string, id: string): Promise<containerDesktopAPI.ManifestInspectInfo> {
|
||||
return containerProviderRegistry.inspectManifest(engineId, id);
|
||||
},
|
||||
replicatePodmanContainer(
|
||||
source: { engineId: string; id: string },
|
||||
target: { engineId: string },
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ import type { IconInfo } from './api/icon-info.js';
|
|||
import type { ImageCheckerInfo } from './api/image-checker-info.js';
|
||||
import type { ImageInfo } from './api/image-info.js';
|
||||
import type { ImageInspectInfo } from './api/image-inspect-info.js';
|
||||
import type { ManifestCreateOptions } from './api/manifest-info.js';
|
||||
import type { ManifestCreateOptions, ManifestInspectInfo } from './api/manifest-info.js';
|
||||
import type { NetworkInspectInfo } from './api/network-info.js';
|
||||
import type { NotificationCard, NotificationCardOptions } from './api/notification.js';
|
||||
import type { OnboardingInfo, OnboardingStatus } from './api/onboarding.js';
|
||||
|
|
@ -775,6 +775,13 @@ export class PluginSystem {
|
|||
},
|
||||
);
|
||||
|
||||
this.ipcHandle(
|
||||
'container-provider-registry:inspectManifest',
|
||||
async (_listener, engine: string, manifestId: string): Promise<ManifestInspectInfo> => {
|
||||
return containerProviderRegistry.inspectManifest(engine, manifestId);
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcHandle(
|
||||
'container-provider-registry:generatePodmanKube',
|
||||
async (_listener, engine: string, names: string[]): Promise<string> => {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ import type { ImageCheckerInfo } from '../../main/src/plugin/api/image-checker-i
|
|||
import type { ImageInfo } from '../../main/src/plugin/api/image-info';
|
||||
import type { ImageInspectInfo } from '../../main/src/plugin/api/image-inspect-info';
|
||||
import type { KubernetesGeneratorInfo } from '../../main/src/plugin/api/KubernetesGeneratorInfo';
|
||||
import type { ManifestCreateOptions } from '../../main/src/plugin/api/manifest-info';
|
||||
import type { ManifestCreateOptions, ManifestInspectInfo } from '../../main/src/plugin/api/manifest-info';
|
||||
import type { NetworkInspectInfo } from '../../main/src/plugin/api/network-info';
|
||||
import type { NotificationCard, NotificationCardOptions } from '../../main/src/plugin/api/notification';
|
||||
import type { OnboardingInfo, OnboardingStatus } from '../../main/src/plugin/api/onboarding';
|
||||
|
|
@ -291,6 +291,12 @@ export function initExposure(): void {
|
|||
return ipcInvoke('container-provider-registry:createManifest', createOptions);
|
||||
},
|
||||
);
|
||||
contextBridge.exposeInMainWorld(
|
||||
'inspectManifest',
|
||||
async (engine: string, manifestId: string): Promise<ManifestInspectInfo> => {
|
||||
return ipcInvoke('container-provider-registry:inspectManifest', engine, manifestId);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @deprecated This method is deprecated and will be removed in a future release.
|
||||
|
|
|
|||
Loading…
Reference in a new issue