feat: expose create/list/delete volumes for extensions (#5598)

fixes https://github.com/containers/podman-desktop/issues/5564
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
Florent BENOIT 2024-01-22 14:50:30 +01:00 committed by GitHub
parent aac0be1cb3
commit caed92f22f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 264 additions and 13 deletions

View file

@ -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<ContainerInfo[]>;
export function inspectContainer(engineId: string, id: string): Promise<ContainerInspectInfo>;
@ -2222,6 +2267,10 @@ declare module '@podman-desktop/api' {
containerProviderConnection: ContainerProviderConnection,
networkCreateOptions: NetworkCreateOptions,
): Promise<NetworkCreateResult>;
export function listVolumes(): Promise<VolumeListInfo[]>;
export function createVolume(options?: VolumeCreateOptions): Promise<VolumeCreateResponseInfo>;
export function deleteVolume(volumeName: string, options?: VolumeDeleteOptions): Promise<void>;
}
/**

View file

@ -107,3 +107,5 @@ export interface NetworkCreateResult {
export interface VolumeCreateOptions {
Name?: string;
}
export interface VolumeCreateResponseInfo extends Dockerode.VolumeCreateResponse {}

View file

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

View file

@ -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<void> {
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<void> {
let telemetryOptions = {};
try {
@ -1700,21 +1724,21 @@ export class ContainerProviderRegistry {
}
}
async createVolume(selectedProvider: ProviderContainerConnectionInfo, options: VolumeCreateOptions): Promise<void> {
async createVolume(
selectedProvider?: ProviderContainerConnectionInfo | containerDesktopAPI.ContainerProviderConnection,
options?: VolumeCreateOptions,
): Promise<VolumeCreateResponseInfo> {
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;

View file

@ -982,6 +982,17 @@ export class ExtensionLoader {
): Promise<containerDesktopAPI.NetworkCreateResult> {
return containerProviderRegistry.createNetwork(providerContainerConnection, networkCreateOptions);
},
listVolumes(): Promise<containerDesktopAPI.VolumeListInfo[]> {
return containerProviderRegistry.listVolumes();
},
createVolume(
volumeCreateOptions?: containerDesktopAPI.VolumeCreateOptions,
): Promise<containerDesktopAPI.VolumeCreateResponseInfo> {
return containerProviderRegistry.createVolume(volumeCreateOptions?.provider, volumeCreateOptions);
},
deleteVolume(volumeName: string, options?: containerDesktopAPI.VolumeDeleteOptions): Promise<void> {
return containerProviderRegistry.deleteVolume(volumeName, options);
},
};
const authenticationProviderRegistry = this.authenticationProviderRegistry;

View file

@ -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<void> => {
): Promise<VolumeCreateResponseInfo> => {
return containerProviderRegistry.createVolume(providerContainerConnectionInfo, options);
},
);