"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.dockerTmpDir = exports.containerDockerOptions = exports.executeDockerJob = exports.dockerContainerScheduler = void 0;
const string_1 = require("@atomist/automation-client/lib/internal/util/string");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const fs = require("fs-extra");
const stringify = require("json-stringify-safe");
const _ = require("lodash");
const os = require("os");
const path = require("path");
const minimalClone_1 = require("../../../api-helper/goal/minimalClone");
const child_process_1 = require("../../../api-helper/misc/child_process");
const container_1 = require("./container");
const provider_1 = require("./provider");
const util_1 = require("./util");
const dockerContainerScheduler = (goal, registration) => {
    goal.addFulfillment(Object.assign({ goalExecutor: executeDockerJob(goal, registration) }, registration));
};
exports.dockerContainerScheduler = dockerContainerScheduler;
/**
 * Execute container goal using Docker CLI.  Wait on completion of
 * first container, then kill all the rest.
 */
function executeDockerJob(goal, registration) {
    // tslint:disable-next-line:cyclomatic-complexity
    return async (gi) => {
        const { goalEvent, progressLog, configuration } = gi;
        const goalName = goalEvent.uniqueName.split("#")[0].toLowerCase();
        const namePrefix = "sdm-";
        const nameSuffix = `-${goalEvent.goalSetId.slice(0, 7)}-${goalName}`;
        const tmpDir = path.join(dockerTmpDir(), goalEvent.repo.owner, goalEvent.repo.name, goalEvent.goalSetId);
        const containerDir = path.join(tmpDir, `${namePrefix}tmp-${string_1.guid()}${nameSuffix}`);
        return configuration.sdm.projectLoader.doWithProject(Object.assign(Object.assign({}, gi), { readOnly: false, cloneDir: containerDir, cloneOptions: minimalClone_1.minimalClone(goalEvent.push, { detachHead: true }) }), 
        // tslint:disable-next-line:cyclomatic-complexity
        async (project) => {
            const spec = Object.assign(Object.assign({}, registration), (!!registration.callback ? await registration.callback(_.cloneDeep(registration), project, goal, goalEvent, gi) : {}));
            if (!spec.containers || spec.containers.length < 1) {
                throw new Error("No containers defined in GoalContainerSpec");
            }
            const inputDir = path.join(tmpDir, `${namePrefix}tmp-${string_1.guid()}${nameSuffix}`);
            const outputDir = path.join(tmpDir, `${namePrefix}tmp-${string_1.guid()}${nameSuffix}`);
            try {
                await util_1.prepareInputAndOutput(inputDir, outputDir, gi);
            }
            catch (e) {
                const message = `Failed to prepare input and output for goal ${goalName}: ${e.message}`;
                progressLog.write(message);
                return { code: 1, message };
            }
            const spawnOpts = {
                log: progressLog,
                cwd: containerDir,
            };
            const network = `${namePrefix}network-${string_1.guid()}${nameSuffix}`;
            let networkCreateRes;
            try {
                networkCreateRes = await child_process_1.spawnLog("docker", ["network", "create", network], spawnOpts);
            }
            catch (e) {
                networkCreateRes = {
                    cmdString: `'docker' 'network' 'create' '${network}'`,
                    code: 128,
                    error: e,
                    output: [undefined, "", e.message],
                    pid: -1,
                    signal: undefined,
                    status: 128,
                    stdout: "",
                    stderr: e.message,
                };
            }
            if (networkCreateRes.code) {
                let message = `Failed to create Docker network '${network}'` +
                    ((networkCreateRes.error) ? `: ${networkCreateRes.error.message}` : "");
                progressLog.write(message);
                try {
                    await dockerCleanup({ spawnOpts });
                }
                catch (e) {
                    networkCreateRes.code++;
                    message += `; ${e.message}`;
                }
                return { code: networkCreateRes.code, message };
            }
            const atomistEnvs = (await util_1.containerEnvVars(gi.goalEvent, gi)).map(env => `--env=${env.name}=${env.value}`);
            const spawnedContainers = [];
            const failures = [];
            for (const container of spec.containers) {
                let secrets = {
                    env: [],
                    files: [],
                };
                try {
                    secrets = await provider_1.prepareSecrets(container, gi);
                    if (!!(secrets === null || secrets === void 0 ? void 0 : secrets.files)) {
                        const secretPath = path.join(inputDir, ".secrets");
                        await fs.ensureDir(secretPath);
                        for (const file of secrets.files) {
                            const secretFile = path.join(secretPath, string_1.guid());
                            file.hostPath = secretFile;
                            await fs.writeFile(secretFile, file.value);
                        }
                    }
                }
                catch (e) {
                    failures.push(e.message);
                }
                const containerName = `${namePrefix}${container.name}${nameSuffix}`;
                let containerArgs;
                try {
                    containerArgs = containerDockerOptions(container, registration);
                }
                catch (e) {
                    progressLog.write(e.message);
                    failures.push(e.message);
                    break;
                }
                const dockerArgs = [
                    "run",
                    "--tty",
                    "--rm",
                    `--name=${containerName}`,
                    `--volume=${containerDir}:${container_1.ContainerProjectHome}`,
                    `--volume=${inputDir}:${container_1.ContainerInput}`,
                    `--volume=${outputDir}:${container_1.ContainerOutput}`,
                    ...secrets.files.map(f => `--volume=${f.hostPath}:${f.mountPath}`),
                    `--network=${network}`,
                    `--network-alias=${container.name}`,
                    ...containerArgs,
                    ...(registration.dockerOptions || []),
                    ...(container.dockerOptions || []),
                    ...atomistEnvs,
                    ...secrets.env.map(e => `--env=${e.name}=${e.value}`),
                    container.image,
                    ...(container.args || []),
                ];
                if (spawnedContainers.length < 1) {
                    dockerArgs.splice(5, 0, `--workdir=${container_1.ContainerProjectHome}`);
                }
                const promise = child_process_1.spawnLog("docker", dockerArgs, spawnOpts);
                spawnedContainers.push({ name: containerName, promise });
            }
            if (failures.length > 0) {
                try {
                    await dockerCleanup({
                        network,
                        spawnOpts,
                        containers: spawnedContainers,
                    });
                }
                catch (e) {
                    failures.push(e.message);
                }
                return {
                    code: failures.length,
                    message: `Failed to spawn Docker containers: ${failures.join("; ")}`,
                };
            }
            const main = spawnedContainers[0];
            try {
                const result = await main.promise;
                if (result.code) {
                    const msg = `Docker container '${main.name}' failed` + ((result.error) ? `: ${result.error.message}` : "");
                    progressLog.write(msg);
                    failures.push(msg);
                }
            }
            catch (e) {
                const message = `Failed to execute main Docker container '${main.name}': ${e.message}`;
                progressLog.write(message);
                failures.push(message);
            }
            const outputFile = path.join(outputDir, "result.json");
            let outputResult;
            if ((await fs.pathExists(outputFile)) && failures.length === 0) {
                try {
                    outputResult = await util_1.processResult(await fs.readJson(outputFile), gi);
                }
                catch (e) {
                    const message = `Failed to read output from Docker container '${main.name}': ${e.message}`;
                    progressLog.write(message);
                    failures.push(message);
                }
            }
            const sidecars = spawnedContainers.slice(1);
            try {
                await dockerCleanup({
                    network,
                    spawnOpts,
                    containers: sidecars,
                });
            }
            catch (e) {
                failures.push(e.message);
            }
            if (failures.length === 0 && !!outputResult) {
                return outputResult;
            }
            else {
                return {
                    code: failures.length,
                    message: (failures.length > 0) ? failures.join("; ") : "Successfully completed container job",
                };
            }
        });
    };
}
exports.executeDockerJob = executeDockerJob;
/**
 * Generate container specific Docker command-line options.
 *
 * @param container Goal container spec
 * @param registration Container goal registration object
 * @return Docker command-line entrypoint, env, p, and volume options
 */
function containerDockerOptions(container, registration) {
    const entryPoint = [];
    if (container.command && container.command.length > 0) {
        // Docker CLI entrypoint must be a binary...
        entryPoint.push(`--entrypoint=${container.command[0]}`);
        // ...so prepend any other command elements to args array
        if (container.args) {
            container.args.splice(0, 0, ...container.command.slice(1));
        }
        else {
            container.args = container.command.slice(1);
        }
    }
    const envs = (container.env || []).map(env => `--env=${env.name}=${env.value}`);
    const ports = (container.ports || []).map(port => `-p=${port.containerPort}`);
    const volumes = [];
    for (const vm of (container.volumeMounts || [])) {
        const volume = (registration.volumes || []).find(v => v.name === vm.name);
        if (!volume) {
            const msg = `Container '${container.name}' references volume '${vm.name}' which not provided in goal registration ` +
                `volumes: ${stringify(registration.volumes)}`;
            logger_1.logger.error(msg);
            throw new Error(msg);
        }
        volumes.push(`--volume=${volume.hostPath.path}:${vm.mountPath}`);
    }
    return [
        ...entryPoint,
        ...envs,
        ...ports,
        ...volumes,
    ];
}
exports.containerDockerOptions = containerDockerOptions;
/**
 * Use a temporary under the home directory so Docker can use it as a
 * volume mount.
 */
function dockerTmpDir() {
    return path.join(os.homedir(), ".atomist", "tmp");
}
exports.dockerTmpDir = dockerTmpDir;
/**
 * Kill running Docker containers, then delete network, and
 * remove directory container directory.  If the copy fails, it throws
 * an error.  Other errors are logged and ignored.
 *
 * @param opts See [[CleanupOptions]]
 */
async function dockerCleanup(opts) {
    if (opts.containers) {
        await dockerKill(opts.containers, opts.spawnOpts);
    }
    if (opts.network) {
        const networkDeleteRes = await child_process_1.spawnLog("docker", ["network", "rm", opts.network], opts.spawnOpts);
        if (networkDeleteRes.code) {
            const msg = `Failed to delete Docker network '${opts.network}'` +
                ((networkDeleteRes.error) ? `: ${networkDeleteRes.error.message}` : "");
            opts.spawnOpts.log.write(msg);
        }
    }
}
/**
 * Kill Docker containers.  Any errors are caught and logged, but not
 * re-thrown.
 *
 * @param containers Containers to kill, they will be killed by name
 * @param opts Options to use when calling spawnLog
 */
async function dockerKill(containers, opts) {
    try {
        const killPromises = [];
        for (const container of containers) {
            killPromises.push(child_process_1.spawnLog("docker", ["kill", container.name], opts));
        }
        await Promise.all(killPromises);
    }
    catch (e) {
        const message = `Failed to kill Docker containers: ${e.message}`;
        opts.log.write(message);
    }
}
//# sourceMappingURL=docker.js.map