diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index 8bb7fbd142384..21eb3dc9ca557 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -294,6 +294,26 @@ 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; + osFeatures: string[]; + osVersion: string; + variant: string; + }; + size: number; + urls: string[]; + }[]; + mediaType: string; + schemaVersion: number; + } export interface KubernetesProviderConnectionEndpoint { apiURL: string; @@ -3384,6 +3404,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; } /** diff --git a/packages/main/src/plugin/api/manifest-info.ts b/packages/main/src/plugin/api/manifest-info.ts index 5b89100500c95..2bb831061443e 100644 --- a/packages/main/src/plugin/api/manifest-info.ts +++ b/packages/main/src/plugin/api/manifest-info.ts @@ -27,3 +27,24 @@ 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; + osFeatures: string[]; + osVersion: string; + variant: string; + }; + size: number; + urls: string[]; + }[]; + mediaType: string; + schemaVersion: number; +} diff --git a/packages/main/src/plugin/container-registry.spec.ts b/packages/main/src/plugin/container-registry.spec.ts index 46ba6b19dfb02..7f3ef5f697d46 100644 --- a/packages/main/src/plugin/container-registry.spec.ts +++ b/packages/main/src/plugin/container-registry.spec.ts @@ -4505,3 +4505,72 @@ 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', + osFeatures: [], + osVersion: 'osVersion', + 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', + ); +}); diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index ebf41f0512710..ba18ca92cac21 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -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'; @@ -1315,6 +1315,22 @@ export class ContainerProviderRegistry { } } + async inspectManifest(engineId: string, manifestId: string): Promise { + 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 }, diff --git a/packages/main/src/plugin/dockerode/libpod-dockerode.spec.ts b/packages/main/src/plugin/dockerode/libpod-dockerode.spec.ts index b3ab563cbbbca..a613e4c3fab45 100644 --- a/packages/main/src/plugin/dockerode/libpod-dockerode.spec.ts +++ b/packages/main/src/plugin/dockerode/libpod-dockerode.spec.ts @@ -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([]); +}); diff --git a/packages/main/src/plugin/dockerode/libpod-dockerode.ts b/packages/main/src/plugin/dockerode/libpod-dockerode.ts index d02bd66d9472a..62f100a7fc199 100644 --- a/packages/main/src/plugin/dockerode/libpod-dockerode.ts +++ b/packages/main/src/plugin/dockerode/libpod-dockerode.ts @@ -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; podmanListImages(options?: PodmanListImagesOptions): Promise; podmanCreateManifest(manifestOptions: ManifestCreateOptions): Promise<{ engineId: string; Id: string }>; + podmanInspectManifest(manifestName: string): Promise; } // tweak Dockerode by adding the support of libpod API @@ -830,5 +831,33 @@ export class LibpodDockerode { }); }); }; + + // add inspectManifest + prototypeOfDockerode.podmanInspectManifest = function (manifestName: string): Promise { + // 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); + }); + }); + }; } } diff --git a/packages/main/src/plugin/extension-loader.ts b/packages/main/src/plugin/extension-loader.ts index 9ba13c83e58ca..404ebd3c443f0 100644 --- a/packages/main/src/plugin/extension-loader.ts +++ b/packages/main/src/plugin/extension-loader.ts @@ -1052,6 +1052,9 @@ export class ExtensionLoader { ): Promise<{ engineId: string; Id: string }> { return containerProviderRegistry.createManifest(manifestOptions); }, + inspectManifest(engineId: string, id: string): Promise { + return containerProviderRegistry.inspectManifest(engineId, id); + }, replicatePodmanContainer( source: { engineId: string; id: string }, target: { engineId: string }, diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index b5591b214ceda..a1efa135aee48 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -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 => { + return containerProviderRegistry.inspectManifest(engine, manifestId); + }, + ); + this.ipcHandle( 'container-provider-registry:generatePodmanKube', async (_listener, engine: string, names: string[]): Promise => { diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index 68053d7a13309..cfdaa295ba424 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -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 => { + return ipcInvoke('container-provider-registry:inspectManifest', engine, manifestId); + }, + ); /** * @deprecated This method is deprecated and will be removed in a future release.