Skip to content

Commit

Permalink
feat: add inspectManifest API endpoint
Browse files Browse the repository at this point in the history
### 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 #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>
  • Loading branch information
cdrage committed Apr 16, 2024
1 parent 9321f73 commit 98a32f1
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 4 deletions.
21 changes: 21 additions & 0 deletions packages/extension-api/src/extension-api.d.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ManifestInspectInfo>;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/main/src/plugin/api/manifest-info.ts
Expand Up @@ -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;
}
69 changes: 69 additions & 0 deletions packages/main/src/plugin/container-registry.spec.ts
Expand Up @@ -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',
);
});
18 changes: 17 additions & 1 deletion packages/main/src/plugin/container-registry.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -1315,6 +1315,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 },
Expand Down
41 changes: 41 additions & 0 deletions packages/main/src/plugin/dockerode/libpod-dockerode.spec.ts
Expand Up @@ -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([]);
});
31 changes: 30 additions & 1 deletion packages/main/src/plugin/dockerode/libpod-dockerode.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
});
});
};
}
}
3 changes: 3 additions & 0 deletions packages/main/src/plugin/extension-loader.ts
Expand Up @@ -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 },
Expand Down
9 changes: 8 additions & 1 deletion packages/main/src/plugin/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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> => {
Expand Down
8 changes: 7 additions & 1 deletion packages/preload/src/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 98a32f1

Please sign in to comment.