podman-desktop/packages/main/src/plugin/docker-extension/docker-plugin-adapter.ts
Florent Benoit 4a89ece59f chore: add missing copryright headers in files
related to https://github.com/containers/podman-desktop/issues/6372
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
2024-03-15 11:53:23 +01:00

332 lines
11 KiB
TypeScript

/**********************************************************************
* Copyright (C) 2023-2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import type { IpcMainEvent, IpcMainInvokeEvent } from 'electron';
import { ipcMain } from 'electron';
import * as os from 'node:os';
import { spawn } from 'child_process';
import type { ContributionManager } from '../contribution-manager.js';
import type { ContainerProviderRegistry } from '../container-registry.js';
import type { SimpleContainerInfo } from '../api/container-info.js';
export interface RawExecResult {
cmd?: string;
killed?: boolean;
signal?: string;
code?: number;
stdout: string;
stderr: string;
}
export class DockerPluginAdapter {
static MACOS_EXTRA_PATH = '/usr/local/bin:/opt/homebrew/bin:/opt/local/bin:/opt/podman/bin';
constructor(
private contributionManager: ContributionManager,
private containerRegistry: ContainerProviderRegistry,
) {}
filterDockerArgs(cmd: string, args: string[]): string[] {
// filter out the "-v", "/var/run/docker.sock:/var/run/docker.sock" from args
let result: string[] = [];
for (let i = args.length - 1; i >= 0; i--) {
const j = i - 1;
if (j >= 0 && args[j] === '-v' && args[i] === '/var/run/docker.sock:/var/run/docker.sock') {
i--;
} else {
result = [args[i], ...result];
}
}
return [cmd, ...result];
}
protected addExtraPathToEnv(extensionId: string, env: NodeJS.ProcessEnv): void {
// add host path of the contribution
const contributionPath = this.contributionManager.getExtensionPath(extensionId);
if (contributionPath) {
if (os.platform() === 'win32') {
if (process.env.Path) {
env.Path = `${contributionPath};${process.env.Path}`;
} else {
env.Path = `${contributionPath}`;
}
} else {
env.PATH = `${contributionPath}:${env.PATH}`;
}
}
}
protected async getVmServiceContainer(contributionId: string): Promise<SimpleContainerInfo> {
// found the matching container
const containers = await this.containerRegistry.listSimpleContainers();
// filter all containers for this extension (matching 'io.podman_desktop.PodmanDesktop.extensionName' === extensionName)
const matchingContainers = containers.filter(
container => container.Labels['io.podman_desktop.PodmanDesktop.extensionName'] === contributionId,
);
if (matchingContainers.length === 0) {
throw new Error(
`No container having the label 'io.podman_desktop.PodmanDesktop.extensionName' == ${contributionId} found.`,
);
}
// get the matching container having label 'io.podman_desktop.PodmanDesktop.vm-service'] = 'true';'
const vmServiceContainer = matchingContainers.find(
container => container.Labels['io.podman_desktop.PodmanDesktop.vm-service'] === 'true',
);
if (!vmServiceContainer) {
throw new Error(`No container having the label 'io.podman_desktop.PodmanDesktop.vm-service' == 'true' found.`);
}
return vmServiceContainer;
}
async handleExec(
contributionId: string,
launcher: string | undefined,
cmd: string,
args: string[],
): Promise<RawExecResult> {
const execResult: RawExecResult = {
cmd,
stdout: '',
stderr: '',
};
// in case of VM_SERVICE, we need to execute the command in the container
if (launcher === 'VM_SERVICE') {
const vmServiceContainer = await this.getVmServiceContainer(contributionId);
// ok we do have the container, let's execute the command in it
const containerId = vmServiceContainer.Id;
const engineId = vmServiceContainer.engineId;
// merge command and args
const fullCommandLine = [cmd, ...args];
return new Promise(resolve => {
const onStdout = (data: Buffer): void => {
execResult.stdout += data.toString();
};
const onStderr = (data: Buffer): void => {
execResult.stderr += data.toString();
};
this.containerRegistry
.execInContainer(engineId, containerId, fullCommandLine, onStdout, onStderr)
.then(() => {
execResult.code = 0;
resolve(execResult);
})
.catch((error: unknown) => {
console.log('got error', error);
execResult.code = 1;
execResult.killed = true;
execResult.signal = String(error);
resolve(execResult);
});
});
}
const env = process.env;
if (os.platform() === 'darwin' && env.PATH) {
env.PATH = env.PATH.concat(':').concat(DockerPluginAdapter.MACOS_EXTRA_PATH);
}
// In production mode, applications don't have access to the 'user' path like brew
return new Promise(resolve => {
// need to add launcher as command and we move command as the first arg
let updatedCommand;
let updatedArgs;
if (launcher) {
updatedCommand = launcher;
updatedArgs = this.filterDockerArgs(cmd, args);
} else {
this.addExtraPathToEnv(contributionId, env);
updatedCommand = cmd;
updatedArgs = args;
}
const spawnProcess = spawn(updatedCommand, updatedArgs, { env, shell: true });
spawnProcess.stdout.setEncoding('utf8');
spawnProcess.stdout.on('data', data => {
execResult.stdout += data;
});
spawnProcess.stderr.setEncoding('utf8');
spawnProcess.stderr.on('data', data => {
execResult.stderr += data;
});
spawnProcess.on('close', code => {
if (code) {
execResult.code = code;
} else {
execResult.code = 0;
}
resolve(execResult);
});
spawnProcess.on('error', error => {
execResult.killed = true;
execResult.signal = error.toString();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).stderr = execResult.stderr;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any).stdout = execResult.stdout;
resolve(error as unknown as RawExecResult);
});
});
}
async handleExecWithOptions(
event: IpcMainEvent,
contributionId: string,
launcher: string | undefined,
cmd: string,
streamCallbackId: number,
options: { splitOutputLines?: boolean },
args: string[],
): Promise<void> {
// in case of VM_SERVICE, we need to execute the command in the container
if (launcher === 'VM_SERVICE') {
// found the matching container
const vmServiceContainer = await this.getVmServiceContainer(contributionId);
// ok we do have the container, let's execute the command in it
const containerId = vmServiceContainer.Id;
const engineId = vmServiceContainer.engineId;
// merge command and args
const fullCommandLine = [cmd, ...args];
const onStdout = (data: Buffer): void => {
event.reply('docker-plugin-adapter:execWithOptions-callback-stdout', streamCallbackId, data);
};
const onStderr = (data: Buffer): void => {
event.reply('docker-plugin-adapter:execWithOptions-callback-stderr', streamCallbackId, data);
};
try {
await this.containerRegistry.execInContainer(engineId, containerId, fullCommandLine, onStdout, onStderr);
event.reply('docker-plugin-adapter:execWithOptions-callback-close', streamCallbackId, 0);
} catch (error) {
event.reply('docker-plugin-adapter:execWithOptions-callback-error', streamCallbackId, error);
event.reply('docker-plugin-adapter:execWithOptions-callback-close', streamCallbackId, 1);
}
return;
}
// need to add launcher as command and we move command as the first arg
let updatedCommand;
let updatedArgs;
const env = process.env;
if (os.platform() === 'darwin' && env.PATH) {
env.PATH = env.PATH.concat(':').concat(DockerPluginAdapter.MACOS_EXTRA_PATH);
}
if (launcher) {
updatedCommand = launcher;
updatedArgs = this.filterDockerArgs(cmd, args);
} else {
this.addExtraPathToEnv(contributionId, env);
updatedCommand = cmd;
updatedArgs = args;
}
const spawnProcess = spawn(updatedCommand, updatedArgs, { env });
let isSplitOutputLines = false;
if (options.splitOutputLines) {
isSplitOutputLines = true;
}
let stdoutLines = '';
spawnProcess.stdout.setEncoding('utf8');
spawnProcess.stdout.on('data', (data: string) => {
if (!isSplitOutputLines) {
// send back the data
event.reply('docker-plugin-adapter:execWithOptions-callback-stdout', streamCallbackId, data);
} else {
// append the text
stdoutLines += data;
// iterate on each newLine
let index = stdoutLines.indexOf('\n');
while (index >= 0) {
// we do have newlines
const line = stdoutLines.substring(0, index);
stdoutLines = stdoutLines.substring(index + 1);
event.reply('docker-plugin-adapter:execWithOptions-callback-stdout', streamCallbackId, line);
// update index
index = stdoutLines.indexOf('\n');
}
}
});
spawnProcess.stderr.setEncoding('utf8');
spawnProcess.stderr.on('data', data => {
// send back the data
event.reply('docker-plugin-adapter:execWithOptions-callback-stderr', streamCallbackId, data);
});
spawnProcess.on('close', code => {
event.reply('docker-plugin-adapter:execWithOptions-callback-close', streamCallbackId, code);
});
spawnProcess.on('error', error => {
event.reply('docker-plugin-adapter:execWithOptions-callback-error', streamCallbackId, error);
});
}
init(): void {
ipcMain.handle(
'docker-plugin-adapter:exec',
async (
event: IpcMainInvokeEvent,
contributionId: string,
launcher: string | undefined,
cmd: string,
args: string[],
): Promise<unknown> => {
return this.handleExec(contributionId, launcher, cmd, args);
},
);
ipcMain.on(
'docker-plugin-adapter:execWithOptions',
(
event: IpcMainEvent,
contributionId: string,
launcher: string | undefined,
cmd: string,
streamCallbackId: number,
options: { splitOutputLines?: boolean },
args: string[],
): void => {
this.handleExecWithOptions(event, contributionId, launcher, cmd, streamCallbackId, options, args).catch(
(error: unknown) => {
event.reply('docker-plugin-adapter:execWithOptions-callback-error', streamCallbackId, error);
},
);
},
);
}
}