Skip to content

Commit 9a1421f

Browse files
authored
feat: adding execute method to PodmanConnection class (#1813)
Signed-off-by: axel7083 <[email protected]>
1 parent a17798e commit 9a1421f

File tree

2 files changed

+245
-5
lines changed

2 files changed

+245
-5
lines changed

packages/backend/src/managers/podmanConnection.spec.ts

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
2020
import { PodmanConnection } from './podmanConnection';
2121
import type {
2222
ContainerProviderConnection,
23+
Extension,
2324
ProviderConnectionStatus,
2425
ProviderContainerConnection,
2526
ProviderEvent,
@@ -29,10 +30,11 @@ import type {
2930
UpdateContainerConnectionEvent,
3031
Webview,
3132
} from '@podman-desktop/api';
32-
import { containerEngine, process, provider, EventEmitter, env } from '@podman-desktop/api';
33+
import { containerEngine, extensions, process, provider, EventEmitter, env } from '@podman-desktop/api';
3334
import { VMType } from '@shared/src/models/IPodman';
3435
import { Messages } from '@shared/Messages';
3536
import type { ModelInfo } from '@shared/src/models/IModelInfo';
37+
import { getPodmanCli, getPodmanMachineName } from '../utils/podman';
3638

3739
const webviewMock = {
3840
postMessage: vi.fn(),
@@ -51,6 +53,9 @@ vi.mock('@podman-desktop/api', async () => {
5153
process: {
5254
exec: vi.fn(),
5355
},
56+
extensions: {
57+
getExtension: vi.fn(),
58+
},
5459
containerEngine: {
5560
listInfos: vi.fn(),
5661
},
@@ -64,6 +69,7 @@ vi.mock('@podman-desktop/api', async () => {
6469
vi.mock('../utils/podman', () => {
6570
return {
6671
getPodmanCli: vi.fn(),
72+
getPodmanMachineName: vi.fn(),
6773
MIN_CPUS_VALUE: 4,
6874
};
6975
});
@@ -73,6 +79,8 @@ beforeEach(() => {
7379

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

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

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

97+
const providerContainerConnectionMock: ProviderContainerConnection = {
98+
connection: {
99+
type: 'podman',
100+
status: () => 'started',
101+
name: 'Podman Machine',
102+
endpoint: {
103+
socketPath: './socket-path',
104+
},
105+
},
106+
providerId: 'podman',
107+
};
108+
109+
describe('execute', () => {
110+
test('execute should get the podman extension from api', async () => {
111+
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
112+
const manager = new PodmanConnection(webviewMock);
113+
await manager.execute(providerContainerConnectionMock.connection, ['ls']);
114+
expect(extensions.getExtension).toHaveBeenCalledWith('podman-desktop.podman');
115+
});
116+
117+
test('execute should call getPodmanCli if extension not available', async () => {
118+
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
119+
const manager = new PodmanConnection(webviewMock);
120+
await manager.execute(providerContainerConnectionMock.connection, ['ls']);
121+
122+
expect(getPodmanCli).toHaveBeenCalledOnce();
123+
expect(process.exec).toHaveBeenCalledWith('podman-executable', ['ls'], undefined);
124+
});
125+
126+
test('options should be propagated to process execution when provided', async () => {
127+
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
128+
const manager = new PodmanConnection(webviewMock);
129+
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
130+
isAdmin: true,
131+
});
132+
133+
expect(getPodmanCli).toHaveBeenCalledOnce();
134+
expect(process.exec).toHaveBeenCalledWith('podman-executable', ['ls'], {
135+
isAdmin: true,
136+
});
137+
});
138+
139+
test('execute should use extension exec if available', async () => {
140+
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
141+
const podmanAPI = {
142+
exec: vi.fn(),
143+
};
144+
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
145+
const manager = new PodmanConnection(webviewMock);
146+
await manager.execute(providerContainerConnectionMock.connection, ['ls']);
147+
148+
expect(getPodmanCli).not.toHaveBeenCalledOnce();
149+
expect(podmanAPI.exec).toHaveBeenCalledWith(['ls'], {
150+
connection: providerContainerConnectionMock,
151+
});
152+
});
153+
154+
test('an error should be throw if the provided container connection do not exists', async () => {
155+
vi.mocked(provider.getContainerConnections).mockReturnValue([]);
156+
const podmanAPI = {
157+
exec: vi.fn(),
158+
};
159+
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
160+
const manager = new PodmanConnection(webviewMock);
161+
162+
await expect(async () => {
163+
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
164+
isAdmin: true,
165+
});
166+
}).rejects.toThrowError('cannot find podman provider with connection name Podman Machine');
167+
});
168+
169+
test('execute should propagate options to extension exec if available', async () => {
170+
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
171+
const podmanAPI = {
172+
exec: vi.fn(),
173+
};
174+
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
175+
const manager = new PodmanConnection(webviewMock);
176+
await manager.execute(providerContainerConnectionMock.connection, ['ls'], {
177+
isAdmin: true,
178+
});
179+
180+
expect(getPodmanCli).not.toHaveBeenCalledOnce();
181+
expect(podmanAPI.exec).toHaveBeenCalledWith(['ls'], {
182+
isAdmin: true,
183+
connection: providerContainerConnectionMock,
184+
});
185+
});
186+
});
187+
188+
describe('executeSSH', () => {
189+
test('executeSSH should call getPodmanCli if extension not available', async () => {
190+
vi.mocked(extensions.getExtension).mockReturnValue(undefined);
191+
const manager = new PodmanConnection(webviewMock);
192+
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls']);
193+
194+
expect(getPodmanCli).toHaveBeenCalledOnce();
195+
expect(process.exec).toHaveBeenCalledWith(
196+
'podman-executable',
197+
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
198+
undefined,
199+
);
200+
});
201+
202+
test('executeSSH should use extension exec if available', async () => {
203+
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
204+
const podmanAPI = {
205+
exec: vi.fn(),
206+
};
207+
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
208+
const manager = new PodmanConnection(webviewMock);
209+
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls']);
210+
211+
expect(getPodmanCli).not.toHaveBeenCalledOnce();
212+
expect(podmanAPI.exec).toHaveBeenCalledWith(
213+
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
214+
{
215+
connection: providerContainerConnectionMock,
216+
},
217+
);
218+
});
219+
220+
test('executeSSH should propagate options to extension exec if available', async () => {
221+
vi.mocked(provider.getContainerConnections).mockReturnValue([providerContainerConnectionMock]);
222+
const podmanAPI = {
223+
exec: vi.fn(),
224+
};
225+
vi.mocked(extensions.getExtension).mockReturnValue({ exports: podmanAPI } as unknown as Extension<unknown>);
226+
const manager = new PodmanConnection(webviewMock);
227+
await manager.executeSSH(providerContainerConnectionMock.connection, ['ls'], {
228+
isAdmin: true,
229+
});
230+
231+
expect(getPodmanCli).not.toHaveBeenCalledOnce();
232+
expect(podmanAPI.exec).toHaveBeenCalledWith(
233+
['machine', 'ssh', providerContainerConnectionMock.connection.name, 'ls'],
234+
{
235+
isAdmin: true,
236+
connection: providerContainerConnectionMock,
237+
},
238+
);
239+
});
240+
});
241+
89242
describe('podman connection initialization', () => {
90243
test('init should notify publisher', () => {
91244
const manager = new PodmanConnection(webviewMock);

packages/backend/src/managers/podmanConnection.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import type {
2323
RegisterContainerConnectionEvent,
2424
UpdateContainerConnectionEvent,
2525
Webview,
26+
RunResult,
27+
RunOptions,
28+
ProviderContainerConnection,
2629
} from '@podman-desktop/api';
27-
import { containerEngine, env, navigation, EventEmitter, process, provider } from '@podman-desktop/api';
28-
import type { MachineJSON } from '../utils/podman';
29-
import { MIN_CPUS_VALUE, getPodmanCli } from '../utils/podman';
30+
import { containerEngine, env, navigation, EventEmitter, process, provider, extensions } from '@podman-desktop/api';
31+
import { getPodmanMachineName, type MachineJSON, MIN_CPUS_VALUE, getPodmanCli } from '../utils/podman';
3032
import { VMType } from '@shared/src/models/IPodman';
3133
import { Publisher } from '../utils/Publisher';
3234
import type {
@@ -40,6 +42,10 @@ export interface PodmanConnectionEvent {
4042
status: 'stopped' | 'started' | 'unregister' | 'register';
4143
}
4244

45+
export interface PodmanRunOptions extends RunOptions {
46+
connection?: ProviderContainerConnection;
47+
}
48+
4349
export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[]> implements Disposable {
4450
// Map of providerId with corresponding connections
4551
#providers: Map<string, ContainerProviderConnection[]>;
@@ -54,6 +60,71 @@ export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[
5460
this.#disposables = [];
5561
}
5662

63+
/**
64+
* Execute the podman cli with the arguments provided
65+
*
66+
* @example
67+
* ```
68+
* const result = await podman.execute(connection, ['machine', 'ls', '--format=json']);
69+
* ```
70+
* @param connection
71+
* @param args
72+
* @param options
73+
*/
74+
execute(connection: ContainerProviderConnection, args: string[], options?: RunOptions): Promise<RunResult> {
75+
const podman = extensions.getExtension('podman-desktop.podman');
76+
if (!podman) {
77+
console.warn('cannot find podman extension api');
78+
return this.executeLegacy(args, options);
79+
}
80+
81+
const podmanApi: {
82+
exec(args: string[], options?: PodmanRunOptions): Promise<RunResult>;
83+
} = podman.exports;
84+
85+
return podmanApi.exec(args, {
86+
...options,
87+
connection: this.getProviderContainerConnection(connection),
88+
});
89+
}
90+
91+
/**
92+
* Execute a command inside the podman machine
93+
*
94+
* @example
95+
* ```
96+
* const result = await podman.executeSSH(connection, ['ls', '/dev']);
97+
* ```
98+
* @param connection
99+
* @param args
100+
* @param options
101+
*/
102+
executeSSH(connection: ContainerProviderConnection, args: string[], options?: RunOptions): Promise<RunResult> {
103+
return this.execute(connection, ['machine', 'ssh', this.getNameLegacyCompatibility(connection), ...args], options);
104+
}
105+
106+
/**
107+
* Before 1.13, the podman extension was not exposing any api.
108+
*
109+
* Therefore, to support old version we need to get the podman executable ourself
110+
* @deprecated
111+
*/
112+
protected executeLegacy(args: string[], options?: RunOptions): Promise<RunResult> {
113+
return process.exec(getPodmanCli(), [...args], options);
114+
}
115+
116+
/**
117+
* Before 1.13, the {@link ContainerProviderConnection.name} field was used as friendly user
118+
* field also.
119+
*
120+
* Therefore, we could have `Podman Machine Default` as name, where the real machine was `podman-machine-default`.
121+
* @param connection
122+
* @deprecated
123+
*/
124+
protected getNameLegacyCompatibility(connection: ContainerProviderConnection): string {
125+
return getPodmanMachineName(connection);
126+
}
127+
57128
getContainerProviderConnections(): ContainerProviderConnection[] {
58129
return Array.from(this.#providers.values()).flat();
59130
}
@@ -92,11 +163,27 @@ export class PodmanConnection extends Publisher<ContainerProviderConnectionInfo[
92163
this.#disposables.forEach(disposable => disposable.dispose());
93164
}
94165

166+
/**
167+
* This method allow us to get the ProviderContainerConnection given a ContainerProviderConnection
168+
* @param connection
169+
* @protected
170+
*/
171+
protected getProviderContainerConnection(connection: ContainerProviderConnection): ProviderContainerConnection {
172+
const providers: ProviderContainerConnection[] = provider.getContainerConnections();
173+
174+
const podmanProvider = providers
175+
.filter(({ connection }) => connection.type === 'podman')
176+
.find(provider => provider.connection.name === connection.name);
177+
if (!podmanProvider) throw new Error(`cannot find podman provider with connection name ${connection.name}`);
178+
179+
return podmanProvider;
180+
}
181+
95182
protected refreshProviders(): void {
96183
// clear all providers
97184
this.#providers.clear();
98185

99-
const providers = provider.getContainerConnections();
186+
const providers: ProviderContainerConnection[] = provider.getContainerConnections();
100187

101188
// register the podman container connection
102189
providers

0 commit comments

Comments
 (0)