UNPKG

@atomist/sdm

Version:

Atomist Software Delivery Machine SDK

295 lines 12.7 kB
"use strict"; /* * 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