dicker
Version:
Dicker - Trivial docker build system
247 lines (233 loc) • 7.12 kB
JavaScript
const fs = require('fs');
const path = require('path');
const shell = require('shelljs');
const toposort = require('toposort');
const { isEmpty } = require('./utils/types');
const { promiseMap, forceArray } = require('./utils/lists');
const { formatArgs, formatBuildArgs } = require('./utils/cli');
const { error } = require('./utils/logger');
const normalizeDockerRef = require('./normalize_docker_ref');
const C = require('./constants');
const uniq = (arr) => {
const seen = {};
const result = [];
arr.forEach((el) => {
if (!seen[el]) {
seen[el] = true;
result.push(el);
}
});
return result;
};
/**
*
* @param taskObj
* @returns {Object}
*/
const normalizeTask = async (taskObj) => {
let { task, tags, skip, type, dependsOn, status, context, dockerfile } = taskObj;
const { command, validate, tag, manifestPath } = taskObj;
// Defaults
tags = uniq([
...(Array.isArray(tag) ? tags : [tag]),
...(Array.isArray(tags) ? tags : [tags]),
].filter(t => !!t).sort());
status = status || C.DEFAULT_TASK_STATUS;
type = type || (
(command && ((!dockerfile) && (tags.length === 0))) ? C.TASK_TYPES.COMMAND : C.DEFAULT_TASK_TYPE
);
skip = !!skip;
dockerfile = dockerfile || null;
context = context || null;
status = (skip && (status === C.TASK_STATUS.PENDING)) ? C.TASK_STATUS.SKIPPED : status;
dependsOn = forceArray(isEmpty(forceArray(dependsOn)) ? C.ROOT_TASK.task : dependsOn);
// Common checks
if (!C.TASK_TYPES[type]) {
error(`Unknown task type: "${type}", exiting with code 1`);
status = C.TASK_STATUS.FAILED;
}
if (!skip) {
if (type === C.TASK_TYPES.DOCKER_BUILD) {
task = task || (tags[0] || null);
tags = ((tags.length === 0) && task) ? [task] : tags;
if (tags.length > 0) {
try {
tags = await promiseMap(
tags,
async t => (await normalizeDockerRef(t)).replace(C.DEFAULT_DOCKER_PUBLIC_REGISTRY, ''),
);
} catch (e) {
error(e.message);
tags = [];
status = C.TASK_STATUS.FAILED;
}
}
if (dockerfile) {
if (!path.isAbsolute(dockerfile)) {
dockerfile = path.join(path.dirname(manifestPath), dockerfile);
if (fs.existsSync(dockerfile)) {
if (fs.statSync(dockerfile).isDirectory()) {
dockerfile = path.join(dockerfile, C.DEFAULT_DOCKERFILE_NAME);
}
context = context || path.dirname(dockerfile);
if (context && (!path.isAbsolute(context)) && manifestPath) {
context = `${path.join(path.dirname(manifestPath), context)}`;
}
}
}
}
}
}
// Common post-checks
if (!task) {
error(`No "task" or "tag"/"tags" params defined for ${JSON.stringify(taskObj)} so can't define task name`);
task = C.DEAD_TASK_NAME;
status = C.TASK_STATUS.FAILED;
}
return {
...taskObj,
dependsOn,
dockerfile,
context,
skip,
status,
tags,
task,
type,
command,
validate,
};
};
const sortTasks = (tasks) => {
const tasksDict = { [C.ROOT_TASK.task]: C.ROOT_TASK };
const existingTasksDict = {};
const extrapolatedTasks = [];
tasks.forEach((taskObj) => {
if (existingTasksDict[taskObj.task]) {
return;
}
taskObj.task.split(':').reduce(
(a, o) => ([
...a,
a.length === 0 ? o.toLowerCase() : [(a.slice(-1)[0] || ''), o.toLowerCase()].join(':'),
]),
[],
).forEach((dt) => {
if (!existingTasksDict[dt]) {
let t = taskObj;
if (taskObj.task !== dt) {
t = {
task: dt,
type: C.TASK_TYPES.CONTROL,
skip: false,
dependsOn: [taskObj.task],
};
}
// Insert generated technical tasks
if ((t.type === C.TASK_TYPES.DOCKER_BUILD) && (t.push === true)) {
const pushTaskName = t.task;
// Push is keeps name of original task and dependency of predceeding being inserted to
// keep downstream to this task integrity
const buildTaskName = `${t.task}:$_build_$`;
const buildTask = {
...t,
type: C.TASK_TYPES.DOCKER_BUILD,
task: buildTaskName,
};
const pushTask = {
...t,
type: C.TASK_TYPES.DOCKER_PUSH,
dependsOn: [buildTaskName],
};
extrapolatedTasks.push(buildTask);
existingTasksDict[buildTaskName] = buildTask;
extrapolatedTasks.push(pushTask);
existingTasksDict[pushTaskName] = pushTask;
} else {
extrapolatedTasks.push(t);
}
}
});
});
const tasksEdges = [];
extrapolatedTasks.forEach((taskObj) => {
const { task, dependsOn } = taskObj;
if (task === C.ROOT_TASK.task) {
error(`Don't use reserved root task name in manifests: "${C.ROOT_TASK.task}"`);
shell.exit(1);
}
if (!tasksDict[task]) {
tasksDict[task] = taskObj;
dependsOn.forEach((dependsOnTaskName) => {
tasksEdges.push([dependsOnTaskName, task]);
});
}
});
const sorted = toposort(tasksEdges).map((image) => {
if (!tasksDict[image]) {
error(`Can't find dependency: "${image}"`);
}
return tasksDict[image];
}).filter(x => x);
// Mark required dependencies as not skipped
sorted.forEach((task) => {
if ((!task.skip) && (task.status !== C.TASK_STATUS.SKIPPED)) {
(task.dependsOn || []).forEach((depName) => {
if (tasksDict[depName].skip) {
tasksDict[depName].skip = false;
tasksDict[depName].status = C.TASK_STATUS.PENDING;
}
});
}
});
return sorted;
};
/**
*
* @param taskObj
* @returns {string}
*/
const makeTaskCommand = (taskObj) => {
const { type, buildArgs, args, tags, dockerfile, context, skip, task } = taskObj;
if (type === C.TASK_TYPES.CONTROL) {
return null;
}
const dockerfileCmd = `-f ${dockerfile} `;
const buildArgsTxt = formatBuildArgs(buildArgs);
const argsTxt = formatArgs(args);
const imageNamesWithTag = tags.length > 0 ? tags : [C.DEFAULT_IMAGE_NAME];
if (skip) {
return ['echo', `'Task "${task}" is skipped'`].filter(x => !!x).join(' ');
}
if (taskObj.type === C.TASK_TYPES.DOCKER_BUILD) {
const tag = imageNamesWithTag[0];
const buildCommand = [
'docker',
'build',
...buildArgsTxt,
dockerfileCmd,
`-t ${tag}`,
...argsTxt,
context,
].filter(x => !!x).join(' ');
const tagCommands = (imageNamesWithTag.length > 1)
? imageNamesWithTag.slice(1).map(t => `docker tag ${tag} ${t}`)
: [];
return [buildCommand, ...tagCommands].join(' && ');
}
if (taskObj.type === C.TASK_TYPES.DOCKER_PUSH) {
return imageNamesWithTag.map(t => `docker push ${t}`).join(' && ');
}
if (taskObj.type === C.TASK_TYPES.COMMAND) {
return [
taskObj.command,
argsTxt,
].filter(x => !!x).join(' ');
}
return null;
};
module.exports = {
normalizeTask,
sortTasks,
makeTaskCommand,
};