dicker
Version:
Dicker - Trivial docker build system
327 lines (302 loc) • 9.68 kB
JavaScript
const shell = require('shelljs');
const { defaults } = require('./utils/objects');
const { getDataLines } = require('./utils/text');
const { promiseMap, forceArray } = require('./utils/lists');
const { now } = require('./utils/time');
const { face } = require('./utils/faces');
const { sortTasks, normalizeTask, makeTaskCommand } = require('./tasks');
const { loadManifests } = require('./manifest');
const { info, error, format, faceLogTask } = require('./utils/logger');
const { formatArgs } = require('./utils/cli');
const C = require('./constants');
/**
*
* @param e
*/
const onError = (e) => {
error(e);
shell.exit(1);
};
process.on('uncaughtException', onError);
process.on('unhandledRejection', onError);
const shellCommandExecutor = async (taskObj, cmd, faceLog) => {
const run = shell.exec(cmd, { async: true, silent: true });
run.stdout.on('data', buff => info(getDataLines(buff)
.map(l => faceLog(C.FACES.INSPECTOR, `STDOUT: ${l}`))
.join('\n')));
run.stderr.on('data', buff => info(getDataLines(buff)
.map(l => faceLog(C.FACES.INSPECTOR, `STDERR: ${l}`))
.join('\n')));
run.on('error', e => error(faceLog(C.FACES.INSPECTOR, format('STDERR:', e))));
return new Promise((cloRes) => {
run.on(
'close',
(code) => {
Object.assign(taskObj, {
code,
status: (code === 0) ? C.TASK_STATUS.DONE : C.TASK_STATUS.FAILED,
message: (code === 0) ? 'Completed successful!' : `Failed with code: ${code}!`,
});
// Don't fail
info(faceLog(code === 0 ? C.FACES.HAPPY : C.FACES.DEAD));
cloRes(taskObj);
},
);
});
};
/**
* Main logic
*/
const startTasks = async (normalizedTasks, params) => {
const { dryRun } = params;
if (dryRun) {
info('Using DRY RUN mode, no commands will be actually executes');
}
const sortedTasks = sortTasks(forceArray(normalizedTasks)).map((taskObj, idx) => {
let { command, status, validate } = taskObj;
const { type, skip, task, args, manifestPath } = taskObj;
if ((type === C.TASK_TYPES.DOCKER_BUILD) || (type === C.TASK_TYPES.DOCKER_PUSH)) {
command = makeTaskCommand(taskObj);
if (!command) {
error(`Can't make command for task: "${task}"`);
status = C.TASK_STATUS.FAILED;
}
} else if (type === C.TASK_TYPES.COMMAND) {
command = [command, formatArgs(args)].filter(x => !!x).join(' ');
}
status = (skip && (status !== C.TASK_STATUS.FAILED))
? C.TASK_STATUS.SKIPPED
: (status || C.TASK_STATUS.PENDING);
if (dryRun) {
command = command
? `echo "Dry run of command: '${command.replace(/([^\\])"/ig, '$1\\"')}'"`
: 'echo "Dry run with no command"';
validate = validate
? `echo "Dry run of validation: '${validate.replace(/([^\\])"/ig, '$1\\"')}'"`
: 'echo "Dry run with no validation"';
}
return defaults(
{ ...taskObj, command, validate },
{
order: idx + 1,
task,
skip,
validate,
type,
status,
manifestPath,
},
);
});
const tasksDict = {};
sortedTasks.forEach((taskObj) => {
const { command, validate, status, description, task, manifestPath } = taskObj;
tasksDict[task] = taskObj;
let publicResolve;
let publicReject;
const promise = new Promise((res, rej) => {
publicResolve = res;
publicReject = rej;
});
// Wrap logger for current task
const faceLog = (faceCode, message) => faceLogTask(
taskObj,
sortedTasks.length,
faceCode,
message,
);
let innerFunction = async () => {
Object.assign(
taskObj,
{
message: faceLog(C.FACES.NDE, 'Something wrong and task initialisation have failed'),
code: -1,
status: C.TASK_STATUS.FAILED,
},
);
return taskObj;
};
if (status === C.TASK_STATUS.SKIPPED) {
innerFunction = async () => {
const message = 'Build skipped due to "skip": true flag.';
const help = `To run this task remove "skip" flag or set "skip": true set in manifest file: "${manifestPath}".`;
[message, help].forEach(m => info(faceLog(C.FACES.DIZZY, m)));
Object.assign(
taskObj,
{
message,
code: 0,
},
);
return taskObj;
};
} else {
innerFunction = async () => {
if (taskObj.status === C.TASK_STATUS.FAILED) {
info(faceLog(C.FACES.DIZZY, 'Task have been failed before start.'));
Object.assign(
taskObj,
{
code: 1,
},
);
return taskObj;
}
const failedDeps = taskObj.dependsOn.filter(
dt => (tasksDict[dt].status !== C.TASK_STATUS.DONE),
);
if (failedDeps.length > 0) {
const fdStr = failedDeps.map(d => `"${d}"`).join(', ');
const message = `Task have been failed before start due following upstream dependencies not being resolved: ${fdStr}.`;
info(faceLog(C.FACES.DIZZY, message));
Object.assign(
taskObj,
{
message,
status: C.TASK_STATUS.FAILED,
code: 1,
},
);
return taskObj;
}
info(faceLog(C.FACES.CALMED_DOWN, command ? `$ ${command}` : 'No command'));
Object.assign(
taskObj,
{ message: `$ ${command}` },
);
Object.assign(
taskObj,
{
status: C.TASK_STATUS.RUNNING,
message: `Started at: ${now()}`,
},
);
info(faceLog(C.FACES.SURPRISED, taskObj.message));
const dockerExecutor = async () => shellCommandExecutor(taskObj, command, faceLog);
const commandExecutor = async () => {
if (!validate) {
return shellCommandExecutor(taskObj, command, faceLog);
}
const preValidationResult = await shellCommandExecutor(taskObj, validate, faceLog);
if (preValidationResult.status === C.TASK_STATUS.DONE) {
return preValidationResult;
}
const result = await shellCommandExecutor(taskObj, command, faceLog);
const validationResult = await shellCommandExecutor(taskObj, validate, faceLog);
return { ...result, ...validationResult };
};
const TYPE_EXECUTORS = {
[C.TASK_TYPES.CONTROL]: async () => Object.assign(taskObj, {
code: 0,
status: C.TASK_STATUS.DONE,
message: 'Completed successful!',
}),
[C.TASK_TYPES.DOCKER_BUILD]: dockerExecutor,
[C.TASK_TYPES.DOCKER_PUSH]: dockerExecutor,
[C.TASK_TYPES.COMMAND]: commandExecutor,
};
return TYPE_EXECUTORS[taskObj.type]();
};
}
const fn = async () => innerFunction().then(
(v) => {
info(C.SEP);
publicResolve(v);
return v;
},
).catch(
(e) => {
info(C.SEP);
publicReject(e);
return e;
},
);
Object.assign(
taskObj,
{
fn,
message: description,
code: null,
resolve: () => Promise.resolve(taskObj),
command,
promise,
},
);
});
info('Following tasks execution order will be used:');
info(sortedTasks.map(t => ` ${faceLogTask(t, sortedTasks.length)}`).join('\n'));
info('');
info(`Docker containers build started at: ${now()}`);
return promiseMap(sortedTasks, t => t.fn());
};
/**
* Main execution point
* @param params {{
* manifestPaths: Array<string>,
* tasksToRun: Array<string>,
* dryRun: boolean,
* }}
* @returns {Promise<Array<{
* status: string,
* message: string,
* type: string,
* }>>}
*/
const run = async (params = {}) => {
const tasksToRun = params.tasksToRun || [];
const tasksResults = await startTasks(
await promiseMap(
await loadManifests(params.manifestPaths, { args: params.args, buildArgs: params.buildArgs }),
async (task) => {
const normalized = await normalizeTask(task);
if (tasksToRun.length === 0) {
return normalized;
}
return {
...normalized,
status: (
(tasksToRun.indexOf(normalized.task) === -1) ? C.TASK_STATUS.SKIPPED : normalized.status
),
skip: (tasksToRun.indexOf(normalized.task) === -1),
};
},
),
params,
);
info([
'',
`Build ended at: ${now()}`,
'',
`From ${tasksResults.length} processed task${tasksResults.length === 1 ? '' : 's'}:`,
...(
[
C.TASK_STATUS.DONE,
C.TASK_STATUS.FAILED,
C.TASK_STATUS.SKIPPED,
C.TASK_STATUS.PENDING,
C.TASK_STATUS.UNKNOWN,
].map(
(ts) => {
const fc = C.TASK_STATUS_TO_FACE[ts];
const padSize = Math.log10(tasksResults.length) + 1;
const count = tasksResults.filter(({ status }) => (status === ts)).length;
if (!count) {
return null;
}
return ` ${ts.padStart(C.TASK_STATUS_MAX_LEN)}: ${count.toString()
.padStart(padSize)} ${face(fc, { count })}`;
},
).filter(x => !!x)
),
'',
'Tasks completion details:',
tasksResults.map(t => ` ${faceLogTask(t, tasksResults.length)}`).join('\n'),
'',
'Have a nice day!',
].join('\n'));
const failedCount = tasksResults.filter(
({ status }) => (status === C.TASK_STATUS.FAILED),
).length;
shell.exit((failedCount === 0) ? 0 : 1);
};
module.exports = run;