"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
/*
 * Copyright 2020 Unbounded Systems, LLC
 *
 * 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.
 */
const utils_1 = require("@adpt/utils");
const execa_1 = tslib_1.__importDefault(require("execa"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const lodash_1 = require("lodash");
const path_1 = tslib_1.__importDefault(require("path"));
const env_1 = require("../env");
const bk_types_1 = require("./bk-types");
const cli_1 = require("./cli");
const image_ref_1 = require("./image-ref");
const image_tools_1 = require("./image-tools");
const labels_1 = require("./labels");
exports.pickBuildKitGlobals = (opts) => lodash_1.pick(opts, "buildKitHost");
const buildKitVersion = "v0.7.2";
function buildKitPlatform() {
    switch (process.platform) {
        case "darwin":
        case "linux":
            return process.platform;
        case "win32":
            return "windows";
        default:
            throw new Error(`Platform ${process.platform} unsupported by BuildKit releases`);
    }
}
function buildKitArch() {
    switch (process.arch) {
        case "arm64":
        case "s390x":
            return process.arch;
        case "x64":
            return "amd64";
        case "arm":
            return "arm-v7";
        case "ppc64":
            return "ppc64le";
        default:
            throw new Error(`CPU architecture ${process.arch} unsupported by BuildKit releases`);
    }
}
async function buildctlPath() {
    const url = `https://github.com/moby/buildkit/releases/download/${buildKitVersion}/buildkit-${buildKitVersion}.${buildKitPlatform()}-${buildKitArch()}.tar.gz`;
    let filename = "buildctl";
    if (process.platform === "win32")
        filename += ".exe";
    const { dir } = await utils_1.fetchToCache({
        name: "buildkit",
        untar: true,
        url,
        version: buildKitVersion,
        fileList: [`bin/${filename}`],
        tarOptions: {
            strip: 1,
        },
    });
    return path_1.default.join(dir, filename);
}
function outputInfo(output) {
    const { imageName, imageTag, uniqueTag = false } = output;
    const ref = image_ref_1.mutableImageRef({
        path: imageName,
    });
    // When we're generating a unique tag, wait to add any tag
    if (imageTag && !uniqueTag) {
        ref.tag = imageTag;
    }
    if (bk_types_1.isBuildKitOutputRegistry(output)) {
        ref.domain = output.registry;
        const params = [
            "type=image",
            "push=true",
            `name=${ref.nameTag || ref.name}`,
        ];
        if (!ref.tag)
            params.push(`push-by-digest=true`);
        if (output.insecure)
            params.push(`registry.insecure=true`);
        return {
            args: ["--output", params.join(",")],
            ref,
        };
    }
    throw new Error(`BuildKit output type not supported`);
}
const defaultBuildKitBuildOptions = {
    frontend: "dockerfile.v0",
};
async function buildKitBuild(dockerfile, contextPath, output, options = {}) {
    const opts = Object.assign({}, defaultBuildKitBuildOptions, options);
    const { imageName, imageTag, prevUniqueNameTag, uniqueTag = false } = output;
    dockerfile = path_1.default.resolve(dockerfile);
    const args = [
        "build",
        "--frontend", opts.frontend,
        "--local", `context=${path_1.default.resolve(contextPath)}`,
        // This is actually the context dir for the dockerfile
        "--local", `dockerfile=${path_1.default.dirname(dockerfile)}`,
        "--opt", `filename=${path_1.default.basename(dockerfile)}`,
        "--progress", "plain",
    ];
    if (uniqueTag && !imageName) {
        throw new Error(`buildKitBuild: imageName must be set if uniqueTag is true`);
    }
    const { args: oArgs, ref: mRef } = outputInfo(output);
    if (opts.deployID) {
        args.push("--opt", `label:${labels_1.adaptDockerDeployIDKey}=${opts.deployID}`);
    }
    args.push(...collectBuildArgs(opts));
    args.push(...oArgs);
    const cmdRet = await execBuildKit(args, opts);
    const { stderr } = cmdRet;
    // if (debug.enabled) debugBuild(cmdRet);
    const { id, digest } = idFromBuild(stderr);
    mRef.id = id;
    mRef.digest = digest;
    if (!mRef.registryDigest)
        throw new utils_1.InternalError(`ref.registryDigest should not be null`);
    if (uniqueTag) {
        let prevId;
        if (prevUniqueNameTag) {
            const prevRef = image_ref_1.mutableImageRef();
            prevRef.nameTag = prevUniqueNameTag;
            // Since prevUniqueNameTag contains the registry, only allow reusing
            // it if the registry for the built image is the same.
            if (mRef.domain === prevRef.domain) {
                prevId = await image_tools_1.registryImageId(prevUniqueNameTag);
            }
        }
        if (prevId === id) {
            mRef.nameTag = prevUniqueNameTag; // prev points to current id
        }
        else {
            const newTag = cli_1.createTag(imageTag, uniqueTag);
            if (!newTag)
                throw new utils_1.InternalError(`newTag should not be null`);
            mRef.tag = newTag;
            await image_tools_1.registryTag({ existing: mRef.registryDigest, newTag });
        }
    }
    const final = mRef.freeze();
    if (!image_ref_1.isImageRefRegistryWithId(final)) {
        throw new utils_1.InternalError(`Built image reference '${final.ref}' is not ` +
            `a complete registry image with ID`);
    }
    return final;
}
exports.buildKitBuild = buildKitBuild;
function collectBuildArgs(opts) {
    const buildArgs = env_1.mergeEnvPairs(opts.buildArgs);
    if (!buildArgs)
        return [];
    const expanded = buildArgs.map((e) => ["--opt", `build-arg:${e.name}=${e.value}`]);
    return lodash_1.flatten(expanded);
}
const digestRe = /exporting manifest (sha\d+:[0-9a-f]+) .*?done/m;
const idRe = /exporting config (sha\d+:[0-9a-f]+) .*?done/m;
function idFromBuild(stderr) {
    let m = stderr.match(digestRe);
    const digest = m && m[1];
    m = stderr.match(idRe);
    const id = m && m[1];
    if (!id)
        throw new Error("Could not extract image ID from BuildKit output\n" + stderr);
    if (!digest)
        throw new Error("Could not extract image digest from BuildKit output\n" + stderr);
    return { digest, id };
}
/** @internal */
async function execBuildKit(args, options) {
    const globalArgs = [];
    if (options.buildKitHost)
        globalArgs.push("--addr", options.buildKitHost);
    const env = env_1.mergeEnvSimple(options.env) || {};
    args = globalArgs.concat(args);
    const execaOpts = {
        all: true,
        env,
    };
    const buildctl = await buildctlPath();
    const cmdDebug = cli_1.debugOut.enabled ? cli_1.debugOut.extend((cli_1.cmdId()).toString()) :
        cli_1.debug.enabled ? cli_1.debug :
            null;
    if (cmdDebug)
        cmdDebug(`Running: ${buildctl} ${args.join(" ")}`);
    try {
        const ret = execa_1.default(buildctl, args, execaOpts);
        if (cli_1.debugOut.enabled && cmdDebug) {
            cli_1.streamToDebug(ret.stdout, cmdDebug);
            cli_1.streamToDebug(ret.stderr, cmdDebug);
        }
        return await ret;
    }
    catch (e) {
        if (e.all)
            e.message = `${e.shortMessage}\n${e.all}`;
        throw e;
    }
}
exports.execBuildKit = execBuildKit;
async function buildKitFilesImage(files, options) {
    const { storage } = options, opts = tslib_1.__rest(options, ["storage"]);
    const dockerfile = `
        FROM scratch
        COPY . /
        `;
    return utils_1.withTmpDir(async (dir) => {
        const dockerfileName = path_1.default.join(dir, "adapt_temp.Dockerfile");
        await cli_1.writeFiles(dir, files);
        await fs_extra_1.default.writeFile(dockerfileName, dockerfile);
        return buildKitBuild(dockerfileName, dir, Object.assign({}, storage, { imageName: "adapt-tmp-files", uniqueTag: true }), Object.assign({}, exports.pickBuildKitGlobals(opts), { deployID: opts.deployID }));
    }, { prefix: "adapt-buildkit-build" });
}
exports.buildKitFilesImage = buildKitFilesImage;
async function withBuildKitFilesImage(files, opts, fn) {
    if (!files || files.length === 0)
        return fn(undefined);
    const image = await buildKitFilesImage(files, opts);
    const ref = image.registryTag || image.registryDigest;
    if (!ref)
        throw new utils_1.InternalError(`buildKitFilesImage did not create a tag or digest`);
    try {
        return await fn(image);
    }
    finally {
        try {
            await image_tools_1.registryDelete(ref);
        }
        catch (err) {
            if (!err.stderr || !/UNSUPPORTED/.test(err.stderr)) {
                err = utils_1.ensureError(err);
                // tslint:disable-next-line: no-console
                console.warn(`Unable to delete temporary Docker image: `, err.message);
            }
        }
    }
}
exports.withBuildKitFilesImage = withBuildKitFilesImage;
//# sourceMappingURL=bk-cli.js.map