Skip to content

Commit

Permalink
feat: adding execute method to PodmanConnection class (#1813)
Browse files Browse the repository at this point in the history
Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 authored Oct 1, 2024
1 parent a17798e commit 9a1421f
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 5 deletions.
155 changes: 154 additions & 1 deletion packages/backend/src/managers/podmanConnection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { PodmanConnection } from './podmanConnection';
import type {
ContainerProviderConnection,
Extension,
ProviderConnectionStatus,
ProviderContainerConnection,
ProviderEvent,
Expand All @@ -29,10 +30,11 @@ import type {
UpdateContainerConnectionEvent,
Webview,
} from '@podman-desktop/api';
import { containerEngine, process, provider, EventEmitter, env } from '@podman-desktop/api';
import { containerEngine, extensions, process, provider, EventEmitter, env } from '@podman-desktop/api';
import { VMType } from '@shared/src/models/IPodman';
import { Messages } from '@shared/Messages';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import { getPodmanCli, getPodmanMachineName } from '../utils/podman';

const webviewMock = {
postMessage: vi.fn(),
Expand All @@ -51,6 +53,9 @@ vi.mock('@podman-desktop/api', async () => {
process: {
exec: vi.fn(),
},
extensions: {
getExtension: vi.fn(),
},
containerEngine: {
listInfos: vi.fn(),
},
Expand All @@ -64,6 +69,7 @@ vi.mock('@podman-desktop/api', async () => {
vi.mock('../utils/podman', () => {
return {
getPodmanCli: vi.fn(),
getPodmanMachineName: vi.fn(),
MIN_CPUS_VALUE: 4,
};
});
Expand All @@ -73,6 +79,8 @@ beforeEach(() => {

vi.mocked(webviewMock.postMessage).mockResolvedValue(true);
vi.mocked(provider.getContainerConnections).mockReturnValue([]);
vi.mocked(getPodmanCli).mockReturnValue('podman-executable');
vi.mocked(getPodmanMachineName).mockImplementation(connection => connection.name);

const listeners: ((value: unknown) => void)[] = [];

Expand All @@ -86,6 +94,151 @@ beforeEach(() => {
} as unknown as EventEmitter<unknown>);
});

const providerContainerConnectionMock: ProviderContainerConnection = {
connection: {
type: 'podman',
status: () => 'started',
name: 'Podman Machine',
endpoint: {
socketPath: './socket-path',
},
},
providerId: 'podman',
};

describe('execute', () => {
test('execute should get the podman extension from api', async () => {
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
const manager = new PodmanConnection(webviewMock);
await manager.execute(providerContainerConnectionMock.connection, ['ls']);
expect(extensions.getExtension).toHaveBeenCalledWith('podman-desktop.podman');
});

test('execute should call getPodmanCli if extension not available', async () => {
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
const manager = new PodmanConnection(webviewMock);
await manager.execute(providerContainerConnectionMock.connection, ['ls']);

expect(getPodmanCli).toHaveBeenCalledOnce();
expect(process.exec).toHaveBeenCalledWith('podman-executable', ['ls'], undefined);
});

test('options should be propagated to process execution when provided', async () => {
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
const manager = new PodmanConnection(webviewMock);
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
isAdmin: true,
});

expect(getPodmanCli).toHaveBeenCalledOnce();
expect(process.exec).toHaveBeenCalledWith('podman-executable', ['ls'], {
isAdmin: true,
});
});

test('execute should use extension exec if available', async () => {
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
const podmanAPI = {
exec: vi.fn(),
};
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
const manager = new PodmanConnection(webviewMock);
await manager.execute(providerContainerConnectionMock.connection, ['ls']);

expect(getPodmanCli).not.toHaveBeenCalledOnce();
expect(podmanAPI.exec).toHaveBeenCalledWith(['ls'], {
connection: providerContainerConnectionMock,
});
});

test('an error should be throw if the provided container connection do not exists', async () => {
vi.mocked(provider.getContainerConnections).mockReturnValue([]);
const podmanAPI = {
exec: vi.fn(),
};
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
const manager = new PodmanConnection(webviewMock);

await expect(async () => {
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
isAdmin: true,
});
}).rejects.toThrowError('cannot find podman provider with connection name Podman Machine');
});

test('execute should propagate options to extension exec if available', async () => {
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
const podmanAPI = {
exec: vi.fn(),
};
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
const manager = new PodmanConnection(webviewMock);
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
isAdmin: true,
});

expect(getPodmanCli).not.toHaveBeenCalledOnce();
expect(podmanAPI.exec).toHaveBeenCalledWith(['ls'], {
isAdmin: true,
connection: providerContainerConnectionMock,
});
});
});

describe('executeSSH', () => {
test('executeSSH should call getPodmanCli if extension not available', async () => {
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
const manager = new PodmanConnection(webviewMock);
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls']);

expect(getPodmanCli).toHaveBeenCalledOnce();
expect(process.exec).toHaveBeenCalledWith(
'podman-executable',
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
undefined,
);
});

test('executeSSH should use extension exec if available', async () => {
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
const podmanAPI = {
exec: vi.fn(),
};
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
const manager = new PodmanConnection(webviewMock);
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls']);

expect(getPodmanCli).not.toHaveBeenCalledOnce();
expect(podmanAPI.exec).toHaveBeenCalledWith(
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
{
connection: providerContainerConnectionMock,
},
);
});

test('executeSSH should propagate options to extension exec if available', async () => {
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
const podmanAPI = {
exec: vi.fn(),
};
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
const manager = new PodmanConnection(webviewMock);
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls'], {
isAdmin: true,
});

expect(getPodmanCli).not.toHaveBeenCalledOnce();
expect(podmanAPI.exec).toHaveBeenCalledWith(
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
{
isAdmin: true,
connection: providerContainerConnectionMock,
},
);
});
});

describe('podman connection initialization', () => {
test('init should notify publisher', () => {
const manager = new PodmanConnection(webviewMock);
Expand Down
95 changes: 91 additions & 4 deletions packages/backend/src/managers/podmanConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import type {
RegisterContainerConnectionEvent,
UpdateContainerConnectionEvent,
Webview,
RunResult,
RunOptions,
ProviderContainerConnection,
} from '@podman-desktop/api';
import { containerEngine, env, navigation, EventEmitter, process, provider } from '@podman-desktop/api';
import type { MachineJSON } from '../utils/podman';
import { MIN_CPUS_VALUE, getPodmanCli } from '../utils/podman';
import { containerEngine, env, navigation, EventEmitter, process, provider, extensions } from '@podman-desktop/api';
import { getPodmanMachineName, type MachineJSON, MIN_CPUS_VALUE, getPodmanCli } from '../utils/podman';
import { VMType } from '@shared/src/models/IPodman';
import { Publisher } from '../utils/Publisher';
import type {
Expand All @@ -40,6 +42,10 @@ export interface PodmanConnectionEvent {
status: 'stopped' | 'started' | 'unregister' | 'register';
}

export interface PodmanRunOptions extends RunOptions {
connection?: ProviderContainerConnection;
}

export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[]> implements Disposable {
// Map of providerId with corresponding connections
#providers: Map<string, ContainerProviderConnection[]>;
Expand All @@ -54,6 +60,71 @@ export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[
this.#disposables = [];
}

/**
* Execute the podman cli with the arguments provided
*
* @example
* ```
* const result = await podman.execute(connection, ['machine', 'ls', '--format=json']);
* ```
* @param connection
* @param args
* @param options
*/
execute(connection: ContainerProviderConnection, args: string[], options?: RunOptions): Promise<RunResult> {
const podman = extensions.getExtension('podman-desktop.podman');
if (!podman) {
console.warn('cannot find podman extension api');
return this.executeLegacy(args, options);
}

const podmanApi: {
exec(args: string[], options?: PodmanRunOptions): Promise<RunResult>;
} = podman.exports;

return podmanApi.exec(args, {
...options,
connection: this.getProviderContainerConnection(connection),
});
}

/**
* Execute a command inside the podman machine
*
* @example
* ```
* const result = await podman.executeSSH(connection, ['ls', '/dev']);
* ```
* @param connection
* @param args
* @param options
*/
executeSSH(connection: ContainerProviderConnection, args: string[], options?: RunOptions): Promise<RunResult> {
return this.execute(connection, ['machine', 'ssh', this.getNameLegacyCompatibility(connection), ...args], options);
}

/**
* Before 1.13, the podman extension was not exposing any api.
*
* Therefore, to support old version we need to get the podman executable ourself
* @deprecated
*/
protected executeLegacy(args: string[], options?: RunOptions): Promise<RunResult> {
return process.exec(getPodmanCli(), [...args], options);
}

/**
* Before 1.13, the {@link ContainerProviderConnection.name} field was used as friendly user
* field also.
*
* Therefore, we could have `Podman Machine Default` as name, where the real machine was `podman-machine-default`.
* @param connection
* @deprecated
*/
protected getNameLegacyCompatibility(connection: ContainerProviderConnection): string {
return getPodmanMachineName(connection);
}

getContainerProviderConnections(): ContainerProviderConnection[] {
return Array.from(this.#providers.values()).flat();
}
Expand Down Expand Up @@ -92,11 +163,27 @@ export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[
this.#disposables.forEach(disposable => disposable.dispose());
}

/**
* This method allow us to get the ProviderContainerConnection given a ContainerProviderConnection
* @param connection
* @protected
*/
protected getProviderContainerConnection(connection: ContainerProviderConnection): ProviderContainerConnection {
const providers: ProviderContainerConnection[] = provider.getContainerConnections();

const podmanProvider = providers
.filter(({ connection }) => connection.type === 'podman')
.find(provider => provider.connection.name === connection.name);
if (!podmanProvider) throw new Error(`cannot find podman provider with connection name ${connection.name}`);

return podmanProvider;
}

protected refreshProviders(): void {
// clear all providers
this.#providers.clear();

const providers = provider.getContainerConnections();
const providers: ProviderContainerConnection[] = provider.getContainerConnections();

// register the podman container connection
providers
Expand Down

0 comments on commit 9a1421f

Please sign in to comment.