103 lines
3.7 KiB
JavaScript
103 lines
3.7 KiB
JavaScript
import Docker from 'dockerode';
|
|
import { resolve } from 'path';
|
|
export class DockerManager {
|
|
docker;
|
|
constructor() {
|
|
this.docker = new Docker();
|
|
}
|
|
async pullImage(image) {
|
|
const stream = await this.docker.pull(image);
|
|
return new Promise((resolve, reject) => {
|
|
this.docker.modem.followProgress(stream, (err) => {
|
|
if (err)
|
|
reject(err);
|
|
else
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
async createContainer(image, options) {
|
|
const absWorkspace = resolve(options.workspace);
|
|
const container = await this.docker.createContainer({
|
|
Image: image,
|
|
Cmd: ['sleep', '3600'],
|
|
WorkingDir: options.workingDir || '/github/workspace',
|
|
HostConfig: {
|
|
Binds: [`${absWorkspace}:/github/workspace`],
|
|
AutoRemove: true,
|
|
},
|
|
Env: Object.entries(options.env).map(([k, v]) => `${k}=${v}`),
|
|
Entrypoint: ['sh', '-c', 'while true; do sleep 3600; done'],
|
|
});
|
|
await container.start();
|
|
return container.id;
|
|
}
|
|
async execInContainer(containerId, step, modifiedCommand) {
|
|
const container = this.docker.getContainer(containerId);
|
|
const cmd = modifiedCommand ?? step.run ?? 'true';
|
|
const shell = step.shell ?? 'bash';
|
|
const workingDir = step.workingDirectory ?? '/github/workspace';
|
|
// Handle 'uses' steps by converting to a comment
|
|
if (!modifiedCommand && step.uses) {
|
|
const usesCmd = `echo "[CISandbox] Action step: ${step.uses}" && echo "Note: 'uses' actions are not yet supported in CISandbox. Convert to 'run' or modify the step to debug."`;
|
|
return this.runShell(container, usesCmd, workingDir, shell, step.env);
|
|
}
|
|
return this.runShell(container, cmd, workingDir, shell, step.env);
|
|
}
|
|
async runShell(container, cmd, workingDir, shell, extraEnv) {
|
|
const exec = await container.exec({
|
|
Cmd: [shell, '-c', cmd],
|
|
WorkingDir: workingDir,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Env: extraEnv ? Object.entries(extraEnv).map(([k, v]) => `${k}=${v}`) : undefined,
|
|
});
|
|
const stream = await exec.start({ hijack: true, stdin: false });
|
|
let stdout = '';
|
|
let stderr = '';
|
|
return new Promise((resolve, reject) => {
|
|
stream.on('data', (chunk) => {
|
|
// Docker multiplexed stream: first byte is stream type
|
|
const type = chunk[0];
|
|
const payload = chunk.slice(8).toString('utf-8');
|
|
if (type === 1)
|
|
stdout += payload;
|
|
else
|
|
stderr += payload;
|
|
});
|
|
stream.on('end', async () => {
|
|
try {
|
|
const inspect = await exec.inspect();
|
|
resolve({
|
|
exitCode: inspect.ExitCode ?? 0,
|
|
stdout: stdout.trimEnd(),
|
|
stderr: stderr.trimEnd(),
|
|
});
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
async stopContainer(containerId) {
|
|
try {
|
|
const container = this.docker.getContainer(containerId);
|
|
await container.stop({ t: 2 });
|
|
}
|
|
catch {
|
|
// Already stopped or doesn't exist
|
|
}
|
|
}
|
|
async imageExists(image) {
|
|
try {
|
|
await this.docker.getImage(image).inspect();
|
|
return true;
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=docker.js.map
|