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:
Charlie Drage 2024-04-18 16:55:41 -04:00 committed by GitHub
parent 13cac8946f
commit 1cb85aef18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 211 additions and 4 deletions

View file

@ -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>;
}
/**

View file

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

View file

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

View file

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

View file

@ -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([]);
});

View file

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

View file

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

View file

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

View file

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