Add features & update project
This commit is contained in:
@@ -2,20 +2,30 @@ import { execa } from 'execa';
|
||||
|
||||
import { env } from '../env';
|
||||
|
||||
type CommandResult = {
|
||||
exitCode: number;
|
||||
output: string;
|
||||
};
|
||||
|
||||
const environmentArgs = (environment: Record<string, string>) =>
|
||||
Object.entries(environment).flatMap(([name, value]) => [
|
||||
'-e',
|
||||
`${name}=${value}`,
|
||||
]);
|
||||
|
||||
const networkArgs = () => (env.network ? ['--network', env.network] : []);
|
||||
|
||||
const containerRuntime = () => env.containerRuntime;
|
||||
|
||||
export const runInJobContainer = async (args: {
|
||||
workdir: string;
|
||||
command: string[];
|
||||
environment: Record<string, string>;
|
||||
redact: (value: string) => string;
|
||||
timeoutMs: number;
|
||||
}) => {
|
||||
const envArgs = Object.entries(args.environment).flatMap(([name, value]) => [
|
||||
'-e',
|
||||
`${name}=${value}`,
|
||||
]);
|
||||
const networkArgs = env.network ? ['--network', env.network] : [];
|
||||
}): Promise<CommandResult> => {
|
||||
const result = await execa(
|
||||
'docker',
|
||||
containerRuntime(),
|
||||
[
|
||||
'run',
|
||||
'--rm',
|
||||
@@ -23,8 +33,8 @@ export const runInJobContainer = async (args: {
|
||||
'4g',
|
||||
'--cpus',
|
||||
'2',
|
||||
...networkArgs,
|
||||
...envArgs,
|
||||
...networkArgs(),
|
||||
...environmentArgs(args.environment),
|
||||
'-v',
|
||||
`${args.workdir}:/workspace`,
|
||||
'-w',
|
||||
@@ -43,3 +53,202 @@ export const runInJobContainer = async (args: {
|
||||
output: args.redact(result.all),
|
||||
};
|
||||
};
|
||||
|
||||
export const startWorkspaceContainer = async (args: {
|
||||
workdir: string;
|
||||
containerName: string;
|
||||
environment: Record<string, string>;
|
||||
command?: string[];
|
||||
publishTcpPort?: number;
|
||||
}) => {
|
||||
await execa(
|
||||
containerRuntime(),
|
||||
[
|
||||
'rm',
|
||||
'-f',
|
||||
args.containerName,
|
||||
],
|
||||
{ reject: false },
|
||||
);
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
[
|
||||
'run',
|
||||
'-d',
|
||||
'--name',
|
||||
args.containerName,
|
||||
'--memory',
|
||||
'4g',
|
||||
'--cpus',
|
||||
'2',
|
||||
...networkArgs(),
|
||||
...(args.publishTcpPort
|
||||
? ['-p', `127.0.0.1::${args.publishTcpPort}`]
|
||||
: []),
|
||||
...environmentArgs(args.environment),
|
||||
'-v',
|
||||
`${args.workdir}:/workspace`,
|
||||
'-w',
|
||||
'/workspace/repo',
|
||||
env.jobImage,
|
||||
...(args.command ?? ['sleep', 'infinity']),
|
||||
],
|
||||
{ all: true },
|
||||
);
|
||||
return {
|
||||
containerId: result.stdout.trim(),
|
||||
containerName: args.containerName,
|
||||
hostPort: args.publishTcpPort
|
||||
? await getPublishedPort(args.containerName, args.publishTcpPort)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getPublishedPort = async (containerName: string, containerPort: number) => {
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
['port', containerName, `${containerPort}/tcp`],
|
||||
{ all: true, reject: false },
|
||||
);
|
||||
const output = result.all.trim();
|
||||
const match = /:(\d+)\s*$/.exec(output);
|
||||
if (!match?.[1]) {
|
||||
throw new Error(
|
||||
`Could not determine published port for ${containerName}:${containerPort}.`,
|
||||
);
|
||||
}
|
||||
return match[1];
|
||||
};
|
||||
|
||||
export const execInWorkspaceContainer = async (args: {
|
||||
containerName: string;
|
||||
command: string[];
|
||||
environment?: Record<string, string>;
|
||||
redact: (value: string) => string;
|
||||
timeoutMs: number;
|
||||
}): Promise<CommandResult> => {
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
[
|
||||
'exec',
|
||||
...(args.environment ? environmentArgs(args.environment) : []),
|
||||
args.containerName,
|
||||
...args.command,
|
||||
],
|
||||
{
|
||||
all: true,
|
||||
reject: false,
|
||||
timeout: args.timeoutMs,
|
||||
},
|
||||
);
|
||||
return {
|
||||
exitCode: result.exitCode ?? 0,
|
||||
output: args.redact(result.all),
|
||||
};
|
||||
};
|
||||
|
||||
export const streamInJobContainer = async (args: {
|
||||
workdir: string;
|
||||
command: string[];
|
||||
environment: Record<string, string>;
|
||||
redact: (value: string) => string;
|
||||
timeoutMs: number;
|
||||
onStdoutLine?: (line: string) => Promise<void>;
|
||||
onStderrLine?: (line: string) => Promise<void>;
|
||||
}): Promise<CommandResult> => {
|
||||
const subprocess = execa(
|
||||
containerRuntime(),
|
||||
[
|
||||
'run',
|
||||
'--rm',
|
||||
'--memory',
|
||||
'4g',
|
||||
'--cpus',
|
||||
'2',
|
||||
...networkArgs(),
|
||||
...environmentArgs(args.environment),
|
||||
'-v',
|
||||
`${args.workdir}:/workspace`,
|
||||
'-w',
|
||||
'/workspace/repo',
|
||||
env.jobImage,
|
||||
...args.command,
|
||||
],
|
||||
{
|
||||
all: true,
|
||||
reject: false,
|
||||
timeout: args.timeoutMs,
|
||||
},
|
||||
);
|
||||
let stdoutBuffer = '';
|
||||
let stderrBuffer = '';
|
||||
const output: string[] = [];
|
||||
const consume = async (
|
||||
chunk: Buffer,
|
||||
source: 'stdout' | 'stderr',
|
||||
handler?: (line: string) => Promise<void>,
|
||||
) => {
|
||||
output.push(chunk.toString('utf8'));
|
||||
const next = `${source === 'stdout' ? stdoutBuffer : stderrBuffer}${chunk.toString('utf8')}`;
|
||||
const lines = next.split(/\r?\n/);
|
||||
const remainder = lines.pop() ?? '';
|
||||
if (source === 'stdout') stdoutBuffer = remainder;
|
||||
else stderrBuffer = remainder;
|
||||
for (const line of lines) {
|
||||
if (handler) {
|
||||
await handler(args.redact(line));
|
||||
}
|
||||
}
|
||||
};
|
||||
subprocess.stdout.on('data', (chunk: Buffer) => {
|
||||
void consume(chunk, 'stdout', args.onStdoutLine);
|
||||
});
|
||||
subprocess.stderr.on('data', (chunk: Buffer) => {
|
||||
void consume(chunk, 'stderr', args.onStderrLine);
|
||||
});
|
||||
const result = await subprocess;
|
||||
if (stdoutBuffer && args.onStdoutLine) {
|
||||
await args.onStdoutLine(args.redact(stdoutBuffer));
|
||||
}
|
||||
if (stderrBuffer && args.onStderrLine) {
|
||||
await args.onStderrLine(args.redact(stderrBuffer));
|
||||
}
|
||||
return {
|
||||
exitCode: result.exitCode ?? 0,
|
||||
output: args.redact(output.join('')),
|
||||
};
|
||||
};
|
||||
|
||||
export const stopWorkspaceContainer = async (containerName: string) => {
|
||||
await execa(containerRuntime(), ['rm', '-f', containerName], {
|
||||
reject: false,
|
||||
});
|
||||
};
|
||||
|
||||
export const inspectWorkspaceContainer = async (containerName: string) => {
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
['inspect', containerName],
|
||||
{
|
||||
all: true,
|
||||
reject: false,
|
||||
},
|
||||
);
|
||||
return {
|
||||
exists: result.exitCode === 0,
|
||||
output: result.all,
|
||||
};
|
||||
};
|
||||
|
||||
export const listWorkspaceContainerNames = async (prefix: string) => {
|
||||
const result = await execa(
|
||||
containerRuntime(),
|
||||
['ps', '-a', '--format', '{{.Names}}'],
|
||||
{ all: true, reject: false },
|
||||
);
|
||||
if (result.exitCode !== 0) return [];
|
||||
return result.all
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith(prefix));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user