diff --git a/packages/api/src/image-registry.ts b/packages/api/src/image-registry.ts index 5bb51eea126d1..f08845f0e9587 100644 --- a/packages/api/src/image-registry.ts +++ b/packages/api/src/image-registry.ts @@ -27,3 +27,7 @@ export interface ImageSearchResult { star_count: number; is_official: boolean; } + +export interface ImageTagsListOptions { + image: string; +} diff --git a/packages/main/src/plugin/image-registry.spec.ts b/packages/main/src/plugin/image-registry.spec.ts index bd8c61322e667..f29fc28c2b503 100644 --- a/packages/main/src/plugin/image-registry.spec.ts +++ b/packages/main/src/plugin/image-registry.spec.ts @@ -1022,3 +1022,29 @@ test('searchImages with https', async () => { const result = await imageRegistry.searchImages({ registry: 'https://quay.io', query: 'http', limit: 10 }); expect(result).toEqual(list); }); + +test('listImageTags', async () => { + vi.spyOn(imageRegistry, 'extractImageDataFromImageName').mockReturnValue({ + name: 'a-name', + tag: 'a-tag', + registry: 'a-registry', + registryURL: 'https://registry.example.com/v2', + }); + vi.spyOn(imageRegistry, 'getAuthInfo').mockResolvedValue({ + authUrl: 'https://auth.example.com', + scheme: 'bearer', + }); + vi.spyOn(imageRegistry, 'getOptions').mockReturnValue({}); + vi.spyOn(imageRegistry, 'getToken').mockResolvedValue('a.token'); + nock('https://registry.example.com', { + reqheaders: { + Authorization: 'Bearer a.token', + }, + }) + .get('/v2/a-name/tags/list') + .reply(200, { + tags: ['1', '2', '3'], + }); + const result = await imageRegistry.listImageTags({ image: 'an-image' }); + expect(result).toEqual(['1', '2', '3']); +}); diff --git a/packages/main/src/plugin/image-registry.ts b/packages/main/src/plugin/image-registry.ts index 003a5eef7102f..7a6b427bb6ccb 100644 --- a/packages/main/src/plugin/image-registry.ts +++ b/packages/main/src/plugin/image-registry.ts @@ -32,7 +32,7 @@ import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import * as nodeTar from 'tar'; import validator from 'validator'; -import type { ImageSearchOptions, ImageSearchResult } from '/@api/image-registry.js'; +import type { ImageSearchOptions, ImageSearchResult, ImageTagsListOptions } from '/@api/image-registry.js'; import { isMac, isWindows } from '../util.js'; import type { ApiSenderType } from './api.js'; @@ -940,6 +940,29 @@ export class ImageRegistry { ); return JSON.parse(resultJSON.body).results; } + + async listImageTags(options: ImageTagsListOptions): Promise { + const imageData = this.extractImageDataFromImageName(options.image); + + // grab auth info from the registry + const authInfo = await this.getAuthInfo(imageData.registry); + const token = await this.getToken(authInfo, imageData); + if (authInfo.scheme.toLowerCase() !== 'bearer') { + throw new Error(`Unsupported auth scheme: ${authInfo.scheme}`); + } + const opts = this.getOptions(); + opts.headers = opts.headers ?? {}; + // add the Bearer token + opts.headers.Authorization = `Bearer ${token}`; + + try { + const catalog = await got.get(`${imageData.registryURL}/${imageData.name}/tags/list`, opts); + return JSON.parse(catalog.body).tags; + } catch (e: unknown) { + console.error('error getting tags of image', options.image, e); + return []; + } + } } interface ImageRegistryNameTag { diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 480e92c4675e8..d1e7a3ba84c2b 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -82,7 +82,7 @@ import type { ImageCheckerInfo } from '/@api/image-checker-info.js'; import type { ImageFilesInfo } from '/@api/image-files-info.js'; import type { ImageInfo } from '/@api/image-info.js'; import type { ImageInspectInfo } from '/@api/image-inspect-info.js'; -import type { ImageSearchOptions, ImageSearchResult } from '/@api/image-registry.js'; +import type { ImageSearchOptions, ImageSearchResult, ImageTagsListOptions } from '/@api/image-registry.js'; import type { ManifestCreateOptions, ManifestInspectInfo, ManifestPushOptions } from '/@api/manifest-info.js'; import type { NetworkInspectInfo } from '/@api/network-info.js'; import type { NotificationCard, NotificationCardOptions } from '/@api/notification.js'; @@ -1525,6 +1525,13 @@ export class PluginSystem { }, ); + this.ipcHandle( + 'image-registry:listImageTags', + async (_listener, options: ImageTagsListOptions): Promise => { + return imageRegistry.listImageTags(options); + }, + ); + this.ipcHandle( 'authentication-provider-registry:getAuthenticationProvidersInfo', async (): Promise => { diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index f0769b79a0409..a1098cf8c794f 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -63,7 +63,7 @@ import type { ImageCheckerInfo } from '/@api/image-checker-info'; import type { ImageFilesInfo } from '/@api/image-files-info'; import type { ImageInfo } from '/@api/image-info'; import type { ImageInspectInfo } from '/@api/image-inspect-info'; -import type { ImageSearchOptions, ImageSearchResult } from '/@api/image-registry'; +import type { ImageSearchOptions, ImageSearchResult, ImageTagsListOptions } from '/@api/image-registry'; import type { ManifestCreateOptions, ManifestInspectInfo, ManifestPushOptions } from '/@api/manifest-info'; import type { NetworkInspectInfo } from '/@api/network-info'; import type { NotificationCard, NotificationCardOptions } from '/@api/notification'; @@ -1244,6 +1244,13 @@ export function initExposure(): void { }, ); + contextBridge.exposeInMainWorld( + 'listImageTagsInRegistry', + async (options: ImageTagsListOptions): Promise => { + return ipcInvoke('image-registry:listImageTags', options); + }, + ); + contextBridge.exposeInMainWorld( 'getAuthenticationProvidersInfo', async (): Promise => {