abstruse
Version:
Abstruse CI
319 lines (289 loc) • 9.52 kB
text/typescript
import { spawn } from 'child_process';
import { Observable, Observer } from 'rxjs';
import * as fs from './fs';
import * as dockerode from 'dockerode';
import { Writable } from 'stream';
import { CommandType } from './config';
import { ProcessOutput } from './process';
import * as envVars from './env-variables';
import * as style from 'ansi-styles';
import { platform } from 'os';
import * as commandExists from 'command-exists';
export const docker = new dockerode();
const binds = platform() === 'darwin' ? [] : ['/var/run/docker.sock:/var/run/docker.sock'];
export function createContainer(
name: string,
image: string,
envs: envVars.EnvVariables
): Observable<ProcessOutput> {
return new Observable(observer => {
docker.createContainer({
Image: image,
name: name,
Tty: true,
OpenStdin: true,
StdinOnce: false,
Env: envVars.serialize(envs) || [],
Binds: binds,
Privileged: true,
ExposedPorts: {
'22/tcp': {},
'5900/tcp': {}
},
PortBindings: {
'22/tcp': [{ HostPort: '' }],
'5900/tcp': [{ HostPort: '' }]
}
} as any)
.then(container => {
let msg = style.bold.open + style.yellow.open + '==> ' + style.yellow.close +
`starting container ` + style.yellow.open + name + ' ' + style.yellow.close +
`from image ` + style.yellow.open + image + ' ' + style.yellow.close +
`... ` + style.bold.close;
observer.next({ type: 'data', data: msg });
return container;
})
.then(container => container.start())
.then(container => container.inspect())
.then(info => observer.next({ type: 'containerInfo', data: info }))
.then(() => observer.next({ type: 'data', data: 'done.\r\n' }))
.then(() => observer.complete())
.catch(err => {
observer.next({ type: 'data', data: 'error.\r\n' });
observer.next({ type: 'containerError', data: err });
observer.error(err);
});
});
}
export function startContainer(id: string): Promise<dockerode.Container> {
return docker.getContainer(id).start();
}
export function dockerExec(
id: string, cmd: any, env: envVars.EnvVariables = {}
): Observable<any> {
return new Observable(observer => {
let exitCode = 255;
let command;
// don't show access token on UI
if (cmd.command.includes('http') && cmd.command.includes('@')) {
command = cmd.command.replace(/\/\/(.*)@/, '//');
} else {
command = cmd.command;
}
if (cmd.type === CommandType.store_cache) {
let msg = style.yellow.open + style.bold.open + '==> storing cache ...' +
style.bold.close + style.yellow.close + '\r\n';
observer.next({ type: 'data', data: msg });
} else if (cmd.type === CommandType.restore_cache) {
let msg = style.yellow.open + style.bold.open + '==> restoring cache ...' +
style.bold.close + style.yellow.close + '\r\n';
observer.next({ type: 'data', data: msg });
} else {
let msg = style.yellow.open + style.bold.open + '==> ' + command +
style.bold.close + style.yellow.close + '\r\n';
observer.next({ type: 'data', data: msg });
}
let container = docker.getContainer(id);
let execOptions = {
Cmd: ['/usr/bin/abstruse-pty', cmd.command],
Env: envVars.serialize(env),
AttachStdout: true,
AttachStderr: true,
Tty: true
};
container.exec(execOptions)
.then(exe => exe.start())
.then(stream => {
let ws = new Writable();
ws.setDefaultEncoding('utf8');
ws.on('finish', () => {
if (cmd.type === CommandType.script) {
envVars.set(env, 'ABSTRUSE_TEST_RESULT', exitCode);
}
observer.next({ type: 'env', data: env });
observer.next({ type: 'exit', data: exitCode });
observer.complete();
});
ws._write = (chunk, enc, next) => {
let str = chunk.toString('utf8');
if (str.includes('[error]')) {
let splitted = str.split(' ');
exitCode = Number(splitted[splitted.length - 1]) || 1;
ws.end();
} else if (str.includes('[success]')) {
exitCode = 0;
ws.end();
} else {
if (str.includes('//') && str.includes('@')) {
str = str.replace(/\/\/(.*)@/, '//');
}
let variable =
Object.keys(env).find(k => env[k].secure && str.indexOf(env[k].value) >= 0);
if (typeof variable !== 'undefined') {
str = str.replace(env[variable].value, '******');
}
observer.next({ type: 'data', data: str });
}
next();
};
container.modem.demuxStream(stream.output, ws, ws);
stream.output.on('end', () => ws.end());
})
.catch(err => observer.error(err));
});
}
export function dockerPwd(id: string, env: envVars.EnvVariables): Observable<ProcessOutput> {
return new Observable(observer => {
dockerExec(id, { type: CommandType.before_install, command: 'pwd' }, env)
.subscribe(event => {
if (event && event.data && event.type === 'data') {
envVars.set(env, 'ABSTRUSE_BUILD_DIR', event.data.replace('\r\n', ''));
}
},
err => observer.error(err),
() => {
observer.next({ type: 'env', data: env });
observer.complete();
});
});
}
export function listContainers(): Promise<dockerode.ContainerInfo[]> {
return docker.listContainers();
}
export function stopAllContainers(): Promise<any[]> {
return listContainers()
.then(containers => {
return Promise.all(containers.map(containerInfo => {
return docker.getContainer(containerInfo.Id).stop();
}));
});
}
export function stopContainer(id: string): Observable<any> {
return new Observable(observer => {
let container = null;
try {
container = docker.getContainer(id);
} catch (e) {
observer.complete();
}
if (container) {
try {
container.inspect()
.then(containerInfo => {
if (containerInfo.State.Running) {
container.stop()
.then(() => container.remove())
.then(() => observer.next(container))
.then(() => observer.complete())
.catch(err => observer.complete());
} else {
container.remove()
.then(() => observer.next(container))
.then(() => observer.complete())
.catch(err => observer.complete());
}
}).catch(err => observer.complete());
} catch (e) {
observer.next();
observer.complete();
}
} else {
observer.next();
observer.complete();
}
});
}
export function killContainer(id: string): Promise<void> {
return new Promise((resolve, reject) => {
let container = null;
try {
container = docker.getContainer(id);
container.inspect()
.then(containerInfo => {
if (containerInfo.State.Status === 'running') {
return container.kill();
} else {
return Promise.resolve();
}
})
.then(() => removeContainer(id))
.then(() => resolve())
.catch(err => {
if (err.statusCode === 404) {
resolve();
} else {
console.error(err);
}
});
} catch (e) {
resolve();
}
});
}
export function removeContainer(id: string): Promise<void> {
return new Promise(resolve => {
try {
let container = docker.getContainer(id);
return container.inspect()
.then(containerInfo => container.remove())
.then(() => resolve())
.catch(() => resolve());
} catch {
resolve();
}
});
}
export function imageExists(name: string): Observable<boolean> {
return new Observable(observer => {
let image = spawn('docker', ['inspect', '--type=image', name]);
image.on('close', code => {
observer.next(code === 0 ? true : false);
observer.complete();
});
});
}
export function isDockerRunning(): Observable<boolean> {
return new Observable((observer: Observer<boolean>) => {
fs.exists('/var/run/docker.sock')
.then(isRunning => {
observer.next(isRunning);
observer.complete();
});
});
}
export function isDockerInstalled(): Observable<boolean> {
return new Observable((observer: Observer<boolean>) => {
commandExists('docker')
.then(() => {
observer.next(true);
observer.complete();
})
.catch(() => {
observer.next(false);
observer.complete();
});
});
}
export function calculateContainerStats(
container: dockerode.ContainerInfo,
processes: any
): Promise<any> {
return docker.getContainer(container.Id).stats({ stream: false })
.then(stats => {
const data = stats;
if (data && data.precpu_stats.system_cpu_usage) {
const jobId = container.Names[0].split('_')[2] || -1;
const job = processes.find(p => p.job_id === Number(jobId));
const debug = job && job.debug || false;
const statsData = {
id: container.Id,
name: container.Names[0].substr(1) || '',
debug: debug,
data: data
};
return statsData;
} else {
return null;
}
});
}