dryrun-ci
Version:
DryRun CI - Local GitLab CI/CD pipeline testing tool with Docker execution, performance monitoring, and security sandboxing
182 lines (181 loc) โข 8.26 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockerExecutor = void 0;
const child_process_1 = require("child_process");
class DockerExecutor {
async createContainer(job) {
const image = job.image || 'node:18';
await this.validateAndPullImage(image, job);
const envVars = this.prepareEnvironmentVariables(job);
const createArgs = [
'create',
'-it',
'--entrypoint', 'sh',
...envVars,
'-w', '/workspace',
'-v', `${process.cwd()}:/workspace`,
image,
'-c', 'sleep infinity'
];
const create = (0, child_process_1.spawnSync)('docker', createArgs, { encoding: 'utf-8' });
if (create.status !== 0) {
throw new Error(`Failed to create Docker container: ${create.stderr || create.stdout}`);
}
const containerId = create.stdout.trim();
const start = (0, child_process_1.spawnSync)('docker', ['start', containerId], { encoding: 'utf-8' });
if (start.status !== 0) {
throw new Error(`Failed to start Docker container: ${start.stderr || start.stdout}`);
}
return containerId;
}
async validateAndPullImage(image, job) {
console.log(`๐ Validating image: ${image}`);
const inspect = (0, child_process_1.spawnSync)('docker', ['inspect', image], { encoding: 'utf-8' });
if (inspect.status !== 0) {
console.log(`๐ฅ Image not found locally, pulling: ${image}`);
const pull = (0, child_process_1.spawnSync)('docker', ['pull', image], { encoding: 'utf-8' });
if (pull.status !== 0) {
const suggestions = this.getImageSuggestions(image, job);
throw new Error(`Failed to pull Docker image '${image}':\n${pull.stderr || pull.stdout}\n\n๐ก Suggestions:\n${suggestions}`);
}
}
else {
console.log(`โ
Image found locally: ${image}`);
}
}
getImageSuggestions(image, job) {
const suggestions = [];
if (image.includes('latest')) {
suggestions.push('- Consider using a specific version tag for reproducible builds');
}
if (image === 'docker:latest' && job.script?.some(cmd => cmd.includes('pip install'))) {
suggestions.push('- For Python/pip operations, consider using python:3.11-slim instead of docker:latest');
suggestions.push('- Or use alpine:latest with apk package manager');
}
if (image.includes('node') && job.script?.some(cmd => cmd.includes('docker build'))) {
suggestions.push('- For Docker operations, consider using docker:latest or docker:dind');
}
suggestions.push('- Check your internet connection');
suggestions.push('- Verify Docker daemon is running: docker info');
return suggestions.join('\n');
}
async executeJob(containerId, job) {
let output = '';
let exitCode = 0;
try {
if (job.before_script && job.before_script.length > 0) {
console.log('๐ง Running before_script...');
const beforeResult = await this.executeCommands(containerId, job.before_script);
output += beforeResult.output;
if (beforeResult.exitCode !== 0) {
output += this.getBeforeScriptDebugInfo(job);
return { exitCode: beforeResult.exitCode, output };
}
}
if (job.script && job.script.length > 0) {
console.log('๐ Running script...');
const scriptResult = await this.executeCommands(containerId, job.script);
output += scriptResult.output;
exitCode = scriptResult.exitCode;
}
if (job.after_script && job.after_script.length > 0) {
console.log('๐งน Running after_script...');
const afterResult = await this.executeCommands(containerId, job.after_script);
output += afterResult.output;
}
}
catch (error) {
output += `\nError executing job: ${error}\n`;
exitCode = 1;
}
return { exitCode, output };
}
getBeforeScriptDebugInfo(job) {
const debugInfo = ['\n๐ Debug Information:'];
if (job.image?.includes('alpine') && job.before_script?.some(cmd => cmd.includes('pip install'))) {
debugInfo.push('- Alpine Linux detected with pip install commands');
debugInfo.push('- Consider using apk package manager instead: apk add py3-awscli');
debugInfo.push('- Or use python:3.11-slim base image');
}
if (job.image?.includes('docker:latest') && job.before_script?.some(cmd => cmd.includes('python'))) {
debugInfo.push('- Docker image with Python operations detected');
debugInfo.push('- Consider using python:3.11-slim for Python-heavy jobs');
}
debugInfo.push('- Check the command output above for specific error details');
debugInfo.push('- Verify all required tools are available in the base image');
return debugInfo.join('\n');
}
async executeCommands(containerId, commands) {
let output = '';
let exitCode = 0;
for (const command of commands) {
console.log(` โ ${command}`);
let exec = (0, child_process_1.spawnSync)('docker', [
'exec',
'-i',
containerId,
'sh', '-c', command
], {
encoding: 'utf-8',
cwd: process.cwd()
});
if (exec.stdout)
output += exec.stdout;
if (exec.stderr)
output += exec.stderr;
if (exec.status !== 0) {
console.log(` โ Command failed: ${command}`);
console.log(` Exit code: ${exec.status || 1}`);
if (exec.stderr) {
console.log(` Error: ${exec.stderr}`);
}
output += `\nโ Command failed: ${command}\n`;
output += `Exit code: ${exec.status || 1}\n`;
if (exec.stderr) {
output += `Error: ${exec.stderr}\n`;
}
if (exec.stderr && exec.stderr.includes('externally-managed-environment')) {
output += `\n๐ก This is a real Python environment issue (PEP 668).\n`;
output += ` Your pipeline needs to be fixed to handle this properly.\n`;
output += ` Consider using: apk add aws-cli (instead of pip install awscli)\n`;
}
exitCode = exec.status || 1;
break;
}
}
return { exitCode, output };
}
prepareEnvironmentVariables(job) {
const envVars = [];
const commonVars = {
'CI': 'true',
'CI_COMMIT_SHA': 'test-commit-sha',
'CI_PIPELINE_ID': '12345',
'CI_PROJECT_DIR': '/workspace',
'CI_JOB_NAME': job.name || 'unknown',
'CI_JOB_STAGE': job.stage || 'unknown'
};
Object.entries(commonVars).forEach(([key, value]) => {
envVars.push('-e', `${key}=${value}`);
});
if (job.variables) {
Object.entries(job.variables).forEach(([key, value]) => {
envVars.push('-e', `${key}=${value}`);
});
}
return envVars;
}
async stopContainer(containerId) {
const stop = (0, child_process_1.spawnSync)('docker', ['stop', containerId], { encoding: 'utf-8' });
if (stop.status !== 0) {
throw new Error(`Failed to stop Docker container: ${stop.stderr || stop.stdout}`);
}
}
async removeContainer(containerId) {
const rm = (0, child_process_1.spawnSync)('docker', ['rm', '-f', containerId], { encoding: 'utf-8' });
if (rm.status !== 0) {
throw new Error(`Failed to remove Docker container: ${rm.stderr || rm.stdout}`);
}
}
}
exports.DockerExecutor = DockerExecutor;