diff --git a/packages/extension-api/src/extension-api.d.ts b/packages/extension-api/src/extension-api.d.ts index 7b2dba26aba..c309d8e76ab 100644 --- a/packages/extension-api/src/extension-api.d.ts +++ b/packages/extension-api/src/extension-api.d.ts @@ -2174,6 +2174,51 @@ declare module '@podman-desktop/api' { diskUsed?: number; } + export interface VolumeInfo { + engineId: string; + engineName: string; + CreatedAt: string; + containersUsage: { id: string; names: string[] }[]; + Name: string; + Driver: string; + Mountpoint: string; + Status?: { [key: string]: string }; + Labels: { [key: string]: string }; + Scope: 'local' | 'global'; + Options?: { [key: string]: string } | null; + UsageData?: { + Size: number; + RefCount: number; + } | null; + } + export interface VolumeListInfo { + Volumes: VolumeInfo[]; + Warnings: string[]; + engineId: string; + engineName: string; + } + export interface VolumeCreateOptions { + // name of the volume to create + Name: string; + // Set the provider to use, if not we will try select the first one available (sorted in favor of Podman). + provider?: ProviderContainerConnectionInfo | containerDesktopAPI.ContainerProviderConnection; + } + + export interface VolumeDeleteOptions { + // Set the provider to use, if not we will try select the first one available (sorted in favor of Podman). + provider?: ProviderContainerConnectionInfo | containerDesktopAPI.ContainerProviderConnection; + } + + export interface VolumeCreateResponseInfo { + Name: string; + Driver: string; + Mountpoint: string; + CreatedAt?: string; + Status?: { [key: string]: string }; + Labels: { [label: string]: string }; + Scope: string; + } + export namespace containerEngine { export function listContainers(): Promise; export function inspectContainer(engineId: string, id: string): Promise; @@ -2222,6 +2267,10 @@ declare module '@podman-desktop/api' { containerProviderConnection: ContainerProviderConnection, networkCreateOptions: NetworkCreateOptions, ): Promise; + + export function listVolumes(): Promise; + export function createVolume(options?: VolumeCreateOptions): Promise; + export function deleteVolume(volumeName: string, options?: VolumeDeleteOptions): Promise; } /** diff --git a/packages/main/src/plugin/api/container-info.ts b/packages/main/src/plugin/api/container-info.ts index bf1bf5fd2b4..a17459dbd56 100644 --- a/packages/main/src/plugin/api/container-info.ts +++ b/packages/main/src/plugin/api/container-info.ts @@ -107,3 +107,5 @@ export interface NetworkCreateResult { export interface VolumeCreateOptions { Name?: string; } + +export interface VolumeCreateResponseInfo extends Dockerode.VolumeCreateResponse {} diff --git a/packages/main/src/plugin/container-registry.spec.ts b/packages/main/src/plugin/container-registry.spec.ts index 7962f28d44a..4f0bf901b5c 100644 --- a/packages/main/src/plugin/container-registry.spec.ts +++ b/packages/main/src/plugin/container-registry.spec.ts @@ -35,6 +35,7 @@ import type { ProviderContainerConnectionInfo } from './api/provider-info.js'; import * as util from '../util.js'; import { PassThrough } from 'node:stream'; import type { EnvfileParser } from './env-file-parser.js'; +import type { ProviderRegistry } from './provider-registry.js'; const tar: { pack: (dir: string) => NodeJS.ReadableStream } = require('tar-fs'); /* eslint-disable @typescript-eslint/no-empty-function */ @@ -1829,6 +1830,169 @@ describe('createVolume', () => { // check that it's calling the right nock method await containerRegistry.createVolume(providerConnectionInfo, {}); }); + + test('provided user API connection', async () => { + nock('http://localhost').post('/volumes/create?Name=myFirstVolume').reply(200, ''); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + const internalContainerProvider: InternalContainerProvider = { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + name: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + }, + }; + + const containerProviderConnection: podmanDesktopAPI.ContainerProviderConnection = { + name: 'podman', + type: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + } as unknown as podmanDesktopAPI.ContainerProviderConnection; + + // set provider + containerRegistry.addInternalProvider('podman', internalContainerProvider); + + // check that it's calling the right nock method + await containerRegistry.createVolume(containerProviderConnection, { Name: 'myFirstVolume' }); + }); + + test('no provider', async () => { + nock('http://localhost').post('/volumes/create?Name=myFirstVolume').reply(200, ''); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + const internalContainerProvider: InternalContainerProvider = { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + name: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + }, + }; + // set provider + containerRegistry.addInternalProvider('podman.podman', internalContainerProvider); + + const containerProviderConnection: podmanDesktopAPI.ContainerProviderConnection = { + name: 'podman', + type: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + } as unknown as podmanDesktopAPI.ContainerProviderConnection; + + const podmanProvider = { + name: 'podman', + id: 'podman', + } as unknown as podmanDesktopAPI.Provider; + + const providerRegistry: ProviderRegistry = { + onBeforeDidUpdateContainerConnection: vi.fn(), + onDidUpdateContainerConnection: vi.fn(), + } as unknown as ProviderRegistry; + + containerRegistry.registerContainerConnection(podmanProvider, containerProviderConnection, providerRegistry); + + // check that it's calling the right nock method + await containerRegistry.createVolume(undefined, { Name: 'myFirstVolume' }); + }); +}); + +describe('deleteVolume', () => { + test('no provider', async () => { + nock('http://localhost').delete('/volumes/myFirstVolume').reply(204, ''); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + const internalContainerProvider: InternalContainerProvider = { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + name: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + }, + }; + // set provider + containerRegistry.addInternalProvider('podman.podman', internalContainerProvider); + + const containerProviderConnection: podmanDesktopAPI.ContainerProviderConnection = { + name: 'podman', + type: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + } as unknown as podmanDesktopAPI.ContainerProviderConnection; + + const podmanProvider = { + name: 'podman', + id: 'podman', + } as unknown as podmanDesktopAPI.Provider; + + const providerRegistry: ProviderRegistry = { + onBeforeDidUpdateContainerConnection: vi.fn(), + onDidUpdateContainerConnection: vi.fn(), + } as unknown as ProviderRegistry; + + containerRegistry.registerContainerConnection(podmanProvider, containerProviderConnection, providerRegistry); + + // check that it's calling the right nock method + await containerRegistry.deleteVolume('myFirstVolume'); + }); + + test('provided connection', async () => { + nock('http://localhost').delete('/volumes/myFirstVolume').reply(204, ''); + + const api = new Dockerode({ protocol: 'http', host: 'localhost' }); + + const internalContainerProvider: InternalContainerProvider = { + name: 'podman', + id: 'podman1', + api, + connection: { + type: 'podman', + name: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + }, + }; + + const containerProviderConnection: podmanDesktopAPI.ContainerProviderConnection = { + name: 'podman', + type: 'podman', + endpoint: { + socketPath: '/endpoint1.sock', + }, + status: () => 'started', + } as unknown as podmanDesktopAPI.ContainerProviderConnection; + + // set provider + containerRegistry.addInternalProvider('podman', internalContainerProvider); + // check that it's calling the right nock method + await containerRegistry.deleteVolume('myFirstVolume', { provider: containerProviderConnection }); + }); }); test('container logs callback notified when messages arrive', async () => { diff --git a/packages/main/src/plugin/container-registry.ts b/packages/main/src/plugin/container-registry.ts index c08f89bbc44..76362224149 100644 --- a/packages/main/src/plugin/container-registry.ts +++ b/packages/main/src/plugin/container-registry.ts @@ -29,6 +29,7 @@ import type { NetworkCreateResult, SimpleContainerInfo, VolumeCreateOptions, + VolumeCreateResponseInfo, } from './api/container-info.js'; import type { ImageInfo } from './api/image-info.js'; import type { PodInfo, PodInspectInfo } from './api/pod-info.js'; @@ -766,6 +767,29 @@ export class ContainerProviderRegistry { } } + // method like remove Volume but instead of taking engineId/engineName it's taking connection info + async deleteVolume( + volumeName: string, + options?: { provider?: ProviderContainerConnectionInfo | containerDesktopAPI.ContainerProviderConnection }, + ): Promise { + let telemetryOptions = {}; + try { + let matchingContainerProviderApi: Dockerode; + if (options?.provider) { + // grab all connections + matchingContainerProviderApi = this.getMatchingEngineFromConnection(options.provider); + } else { + // Get the first running connection (preference for podman) + matchingContainerProviderApi = this.getFirstRunningConnection()[1]; + } + return matchingContainerProviderApi.getVolume(volumeName).remove(); + } catch (error) { + telemetryOptions = { error: error }; + throw error; + } finally { + this.telemetryService.track('removeVolume', telemetryOptions); + } + } async removeVolume(engineId: string, volumeName: string): Promise { let telemetryOptions = {}; try { @@ -1700,21 +1724,21 @@ export class ContainerProviderRegistry { } } - async createVolume(selectedProvider: ProviderContainerConnectionInfo, options: VolumeCreateOptions): Promise { + async createVolume( + selectedProvider?: ProviderContainerConnectionInfo | containerDesktopAPI.ContainerProviderConnection, + options?: VolumeCreateOptions, + ): Promise { let telemetryOptions = {}; try { - // filter from connections - const matchingContainerProvider = Array.from(this.internalProviders.values()).find( - containerProvider => - containerProvider.connection.endpoint.socketPath === selectedProvider.endpoint.socketPath && - containerProvider.connection.name === selectedProvider.name && - selectedProvider.status === 'started', - ); - if (!matchingContainerProvider?.api) { - throw new Error('No provider with a running engine'); + let matchingContainerProviderApi: Dockerode; + if (selectedProvider) { + // grab all connections + matchingContainerProviderApi = this.getMatchingEngineFromConnection(selectedProvider); + } else { + // Get the first running connection (preference for podman) + matchingContainerProviderApi = this.getFirstRunningConnection()[1]; } - - await matchingContainerProvider.api.createVolume(options); + return matchingContainerProviderApi.createVolume(options); } catch (error) { telemetryOptions = { error: error }; throw error; diff --git a/packages/main/src/plugin/extension-loader.ts b/packages/main/src/plugin/extension-loader.ts index edc384473bd..4b571c0937e 100644 --- a/packages/main/src/plugin/extension-loader.ts +++ b/packages/main/src/plugin/extension-loader.ts @@ -982,6 +982,17 @@ export class ExtensionLoader { ): Promise { return containerProviderRegistry.createNetwork(providerContainerConnection, networkCreateOptions); }, + listVolumes(): Promise { + return containerProviderRegistry.listVolumes(); + }, + createVolume( + volumeCreateOptions?: containerDesktopAPI.VolumeCreateOptions, + ): Promise { + return containerProviderRegistry.createVolume(volumeCreateOptions?.provider, volumeCreateOptions); + }, + deleteVolume(volumeName: string, options?: containerDesktopAPI.VolumeDeleteOptions): Promise { + return containerProviderRegistry.deleteVolume(volumeName, options); + }, }; const authenticationProviderRegistry = this.authenticationProviderRegistry; diff --git a/packages/main/src/plugin/index.ts b/packages/main/src/plugin/index.ts index 3c5c8342f12..be7eaf5a020 100644 --- a/packages/main/src/plugin/index.ts +++ b/packages/main/src/plugin/index.ts @@ -46,6 +46,7 @@ import type { ContainerInfo, SimpleContainerInfo, VolumeCreateOptions, + VolumeCreateResponseInfo, } from './api/container-info.js'; import type { ImageInfo } from './api/image-info.js'; import type { PullEvent } from './api/pull-event.js'; @@ -1080,7 +1081,7 @@ export class PluginSystem { _listener, providerContainerConnectionInfo: ProviderContainerConnectionInfo, options: VolumeCreateOptions, - ): Promise => { + ): Promise => { return containerProviderRegistry.createVolume(providerContainerConnectionInfo, options); }, );