@atomist/sdm
Version:
Atomist Software Delivery Machine SDK
295 lines • 12.7 kB
JavaScript
;
/*
* Copyright © 2020 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultDockerImageNameCreator = exports.executeDockerBuild = void 0;
const HandlerResult_1 = require("@atomist/automation-client/lib/HandlerResult");
const GraphClient_1 = require("@atomist/automation-client/lib/spi/graph/GraphClient");
const pool_1 = require("@atomist/automation-client/lib/util/pool");
const fs = require("fs-extra");
const _ = require("lodash");
const os = require("os");
const path = require("path");
const LoggingProgressLog_1 = require("../../../api-helper/log/LoggingProgressLog");
const StringCapturingProgressLog_1 = require("../../../api-helper/log/StringCapturingProgressLog");
const child_process_1 = require("../../../api-helper/misc/child_process");
const projectConfiguration_1 = require("../../../api-helper/project/configuration/projectConfiguration");
const withProject_1 = require("../../../api-helper/project/withProject");
const GoalWithFulfillment_1 = require("../../../api/goal/GoalWithFulfillment");
const projectVersioner_1 = require("../../../core/delivery/build/local/projectVersioner");
const array_1 = require("../../../core/util/misc/array");
const name_1 = require("../support/name");
/**
* Execute a Docker build for the project
*/
function executeDockerBuild(options) {
return withProject_1.doWithProject(async (gi) => {
const { goalEvent, context, project } = gi;
const optsToUse = GoalWithFulfillment_1.mergeOptions(options, {}, "docker.build");
switch (optsToUse.builder) {
case "docker":
await checkIsBuilderAvailable("docker", "help");
break;
case "kaniko":
await checkIsBuilderAvailable("/kaniko/executor", "--help");
break;
}
// Check the graph for registries if we don't have any configured
if (!optsToUse.config && array_1.toArray(optsToUse.registry || []).length === 0) {
optsToUse.registry = await readRegistries(context);
}
const imageNames = await optsToUse.dockerImageNameCreator(project, goalEvent, optsToUse, context);
const images = _.flatten(imageNames.map(imageName => imageName.tags.map(tag => `${imageName.registry ? `${imageName.registry}/` : ""}${imageName.name}:${tag}`)));
const dockerfilePath = await (optsToUse.dockerfileFinder
? optsToUse.dockerfileFinder(project)
: "Dockerfile");
let externalUrls = [];
if (await pushEnabled(gi, optsToUse)) {
externalUrls = getExternalUrls(imageNames, optsToUse);
}
// 1. run docker login
let result = await dockerLogin(optsToUse, gi);
if (result.code !== 0) {
return result;
}
if (optsToUse.builder === "docker") {
result = await buildWithDocker(images, dockerfilePath, gi, optsToUse);
if (result.code !== 0) {
return result;
}
}
else if (optsToUse.builder === "kaniko") {
result = await buildWithKaniko(images, imageNames, dockerfilePath, gi, optsToUse);
if (result.code !== 0) {
return result;
}
}
return Object.assign(Object.assign({}, result), { externalUrls });
}, {
readOnly: true,
detachHead: false,
});
}
exports.executeDockerBuild = executeDockerBuild;
async function buildWithDocker(images, dockerfilePath, gi, optsToUse) {
// 2. run docker build
const tags = _.flatten(images.map(i => ["-t", i]));
let result = await gi.spawn("docker", ["build", "-f", dockerfilePath, ...tags, ...optsToUse.builderArgs, optsToUse.builderPath], {
env: Object.assign(Object.assign({}, process.env), { DOCKER_CONFIG: dockerConfigPath(optsToUse, gi.goalEvent) }),
log: gi.progressLog,
});
// 3. run docker push
result = await dockerPush(images, optsToUse, gi);
if (result.code !== 0) {
return result;
}
return result;
}
async function buildWithKaniko(images, imageNames, dockerfilePath, gi, optsToUse) {
// 2. run kaniko build
const builderArgs = [];
if (await pushEnabled(gi, optsToUse)) {
builderArgs.push(...images.map(i => `-d=${i}`), "--cache=true", `--cache-repo=${imageNames[0].registry ? `${imageNames[0].registry}/` : ""}${imageNames[0].name}-cache`);
}
else {
builderArgs.push("--no-push");
}
builderArgs.push(...(optsToUse.builderArgs.length > 0 ? optsToUse.builderArgs : ["--snapshotMode=time", "--reproducible"]));
// Check if base image cache dir is available
const cacheFilPath = _.get(gi, "configuration.sdm.cache.path", "/opt/data");
if (_.get(gi, "configuration.sdm.cache.enabled") === true && (await fs.pathExists(cacheFilPath))) {
const baseImageCache = path.join(cacheFilPath, "base-image-cache");
await fs.mkdirs(baseImageCache);
builderArgs.push(`--cache-dir=${baseImageCache}`, "--cache=true");
}
const kanikoContext = `dir://${gi.project.baseDir}` + (optsToUse.builderPath === "." ? "" : `/${optsToUse.builderPath}`);
return gi.spawn("/kaniko/executor", ["--dockerfile", dockerfilePath, "--context", kanikoContext, ..._.uniq(builderArgs)], {
env: Object.assign(Object.assign({}, process.env), { DOCKER_CONFIG: dockerConfigPath(optsToUse, gi.goalEvent) }),
log: gi.progressLog,
});
}
async function dockerLogin(options, gi) {
const registries = array_1.toArray(options.registry || []).filter(r => !!r.user && !!r.password);
if (registries.length > 0) {
let result;
for (const registry of registries) {
gi.progressLog.write("Running 'docker login'");
const loginArgs = ["login", "--username", registry.user, "--password", registry.password];
if (/[^A-Za-z0-9]/.test(registry.registry)) {
loginArgs.push(registry.registry);
}
// 2. run docker login
result = await gi.spawn("docker", loginArgs, {
logCommand: false,
log: gi.progressLog,
});
if (!!result && result.code !== 0) {
return result;
}
}
return result;
}
else if (options.config) {
gi.progressLog.write("Authenticating with provided Docker 'config.json'");
const dockerConfig = path.join(dockerConfigPath(options, gi.goalEvent), "config.json");
await fs.ensureDir(path.dirname(dockerConfig));
await fs.writeFile(dockerConfig, options.config);
}
else {
gi.progressLog.write("Skipping 'docker auth' because no credentials configured");
}
return HandlerResult_1.Success;
}
async function dockerPush(images, options, gi) {
let result = HandlerResult_1.Success;
if (await pushEnabled(gi, options)) {
if (!!options.concurrentPush) {
const results = await pool_1.executeAll(images.map(image => async () => {
const log = new StringCapturingProgressLog_1.StringCapturingProgressLog();
const r = await gi.spawn("docker", ["push", image], {
env: Object.assign(Object.assign({}, process.env), { DOCKER_CONFIG: dockerConfigPath(options, gi.goalEvent) }),
log,
});
gi.progressLog.write(log.log);
return r;
}));
return {
code: results.some(r => !!r && r.code !== 0) ? 1 : 0,
};
}
else {
for (const image of images) {
result = await gi.spawn("docker", ["push", image], {
env: Object.assign(Object.assign({}, process.env), { DOCKER_CONFIG: dockerConfigPath(options, gi.goalEvent) }),
log: gi.progressLog,
});
if (!!result && result.code !== 0) {
return result;
}
}
}
}
else {
gi.progressLog.write("Skipping 'docker push'");
}
return result;
}
const DefaultDockerImageNameCreator = async (p, sdmGoal, options, context) => {
const name = name_1.cleanImageName(p.name);
const tags = [];
const version = await projectVersioner_1.readSdmVersion(sdmGoal.repo.owner, sdmGoal.repo.name, sdmGoal.repo.providerId, sdmGoal.sha, sdmGoal.branch, context);
if (!!version) {
tags.push(version);
}
const latestTag = await projectConfiguration_1.projectConfigurationValue("docker.tag.latest", p, false);
if ((latestTag && sdmGoal.branch === sdmGoal.push.repo.defaultBranch) || tags.length === 0) {
tags.push("latest");
}
if (!!options.registry) {
return array_1.toArray(options.registry).map(r => ({
registry: !!r.registry ? r.registry : undefined,
name,
tags,
}));
}
else {
return [
{
registry: undefined,
name,
tags,
},
];
}
};
exports.DefaultDockerImageNameCreator = DefaultDockerImageNameCreator;
async function checkIsBuilderAvailable(cmd, ...args) {
try {
await child_process_1.spawnLog(cmd, args, { log: new LoggingProgressLog_1.LoggingProgressLog("docker-build-check") });
}
catch (e) {
throw new Error(`Configured Docker image builder '${cmd}' is not available`);
}
}
async function pushEnabled(gi, options) {
let push = false;
// tslint:disable-next-line:no-boolean-literal-compare
if (options.push === true || options.push === false) {
push = options.push;
}
else if (array_1.toArray(options.registry || []).some(r => !!r.user && !!r.password) || !!options.config) {
push = true;
}
return projectConfiguration_1.projectConfigurationValue("docker.build.push", gi.project, push);
}
function dockerConfigPath(options, goalEvent) {
if (!!options.config) {
return path.join(os.homedir(), `.docker-${goalEvent.goalSetId}`);
}
else {
return path.join(os.homedir(), ".docker");
}
}
function getExternalUrls(images, options) {
const externalUrls = images.map(i => {
const reg = array_1.toArray(options.registry || []).find(r => r.registry === i.registry);
if (!!reg && !!reg.display) {
return i.tags.map(t => {
let url = `${!!reg.displayUrl ? reg.displayUrl : i.registry}/${i.name}`;
if (!!reg.displayBrowsePath) {
const replace = url.split(":").pop();
url = url.replace(`:${replace}`, reg.displayBrowsePath);
}
if (!!reg.label) {
return { label: reg.label, url };
}
else {
return { url };
}
});
}
return undefined;
});
return _.uniqBy(_.flatten(externalUrls).filter(u => !!u), "url");
}
async function readRegistries(ctx) {
const registries = [];
const dockerRegistries = await ctx.graphClient.query({
name: "DockerRegistryProviderAll",
options: GraphClient_1.QueryNoCacheOptions,
});
if (!!dockerRegistries && !!dockerRegistries.DockerRegistryProvider) {
for (const dockerRegistry of dockerRegistries.DockerRegistryProvider) {
const credential = await ctx.graphClient.query({
name: "Password",
variables: {
id: dockerRegistry.credential.id,
},
});
// Strip out the protocol
const registryUrl = new URL(dockerRegistry.url);
registries.push({
registry: registryUrl.host,
user: credential.Password[0].owner.login,
password: credential.Password[0].secret,
label: dockerRegistry.name,
display: false,
});
}
}
return registries;
}
//# sourceMappingURL=executeDockerBuild.js.map