"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.sortSpecs = exports.repoSync = exports.kubernetesSync = exports.KubernetesSync = void 0;
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const messages_1 = require("../../../api-helper/misc/slack/messages");
const apply_1 = require("../kubernetes/apply");
const secret_1 = require("../kubernetes/secret");
const spec_1 = require("../kubernetes/spec");
const error_1 = require("../support/error");
const name_1 = require("../support/name");
const clone_1 = require("./clone");
const diff_1 = require("./diff");
const repo_1 = require("./repo");
exports.KubernetesSync = "KubernetesSync";
/**
 * Command to synchronize the resources in a Kubernetes cluster with
 * the resource specs in the configured sync repo.  The sync repo will
 * be cloned and the resources applied against the Kubernetes API in
 * lexical order sorted by file name.  If no sync repo is configured
 * in the SDM, the command errors.  This command is typically executed
 * on an interval timer by setting the `intervalMinutes`
 * [[KubernetesSyncOptions]].
 *
 * If the SDM configuration says this packs commands should be added,
 * i.e., `sdm.configuration.sdm.k8s.options.addCommands` is `true`,
 * the command will have the intent `kube sync SDM_NAME`.  Otherwise
 * the command will be registered without an intent.
 */
function kubernetesSync(sdm) {
    var _a, _b;
    const cmd = {
        name: exports.KubernetesSync,
        listener: repoSync,
    };
    if ((_b = (_a = sdm.configuration.sdm.k8s) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.addCommands) {
        cmd.intent = `kube sync ${name_1.cleanName(sdm.configuration.name)}`;
    }
    return cmd;
}
exports.kubernetesSync = kubernetesSync;
/**
 * Clone the sync repo and apply the specs to the Kubernetes cluster.
 */
async function repoSync(cli) {
    var _a, _b, _c, _d, _e;
    const opts = (_b = (_a = cli.configuration.sdm.k8s) === null || _a === void 0 ? void 0 : _a.options) === null || _b === void 0 ? void 0 : _b.sync;
    if (!opts) {
        const message = `SDM has no sync options defined`;
        logger_1.logger.error(message);
        await cli.context.messageClient.respond(message);
        return { code: 2, message };
    }
    if ((_e = (_d = (_c = cli.configuration.sdm.k8s) === null || _c === void 0 ? void 0 : _c.options) === null || _d === void 0 ? void 0 : _d.sync) === null || _e === void 0 ? void 0 : _e.credentials) {
        delete cli.configuration.sdm.k8s.options.sync.credentials;
    }
    if (!await repo_1.queryForScmProvider(cli.configuration)) {
        const message = `Failed to get sync repo and credentials, skipping sync`;
        logger_1.logger.error(message);
        await cli.context.messageClient.respond(message);
        return { code: 2, message };
    }
    if (!repo_1.isRemoteRepo(opts.repo)) {
        const message = `SDM sync option repo is not a valid remote repo`;
        logger_1.logger.error(message);
        await cli.context.messageClient.respond(message);
        return { code: 2, message };
    }
    const projectLoadingParameters = {
        credentials: opts.credentials,
        cloneOptions: clone_1.defaultCloneOptions,
        context: cli.context,
        id: opts.repo,
        readOnly: true,
    };
    const slug = `${opts.repo.owner}/${opts.repo.repo}`;
    try {
        logger_1.logger.info(`Starting sync of repo ${slug}`);
        await cli.configuration.sdm.projectLoader.doWithProject(projectLoadingParameters, syncApply(opts));
    }
    catch (e) {
        const message = `Failed to sync repo ${slug}: ${e.message}`;
        logger_1.logger.error(message);
        await cli.context.messageClient.respond(message);
        return { code: 1, message };
    }
    const result = {
        code: 0,
        message: `Successfully completed sync of repo ${slug}`,
    };
    logger_1.logger.info(result.message);
    try {
        await cli.context.messageClient.respond(messages_1.slackSuccessMessage("Kubernetes Sync", result.message));
    }
    catch (e) {
        result.code++;
        result.message = `${result.message}; Failed to send response message: ${e.message}`;
        logger_1.logger.error(result.message);
    }
    return result;
}
exports.repoSync = repoSync;
/**
 * Return a function that ensures all specs in `syncRepo` have
 * corresponding resources in the Kubernetes cluster.  If the resource
 * does not exist, it is created using the spec.  If it does exist, it
 * is patched using the spec.  Errors are collected and thrown after
 * processing all specs so one bad spec does not stop processing.
 *
 * @param opts Kubernetes sync options
 */
function syncApply(opts) {
    return async (syncRepo) => {
        const specFiles = await sortSpecs(syncRepo);
        const errors = [];
        for (const specFile of specFiles) {
            logger_1.logger.debug(`Processing spec ${specFile.path}`);
            try {
                const specString = await specFile.getContent();
                const specs = spec_1.parseKubernetesSpecs(specString);
                for (let spec of specs) {
                    try {
                        if (spec.kind === "Secret" && opts && opts.secretKey) {
                            spec = await secret_1.decryptSecret(spec, opts.secretKey);
                        }
                        await apply_1.applySpec(spec);
                    }
                    catch (e) {
                        const slug = spec_1.specSlug(spec);
                        e.message = `Failed to apply spec '${slug}' from '${specFile.path}': ${error_1.k8sErrMsg(e)}`;
                        logger_1.logger.error(e.message);
                        errors.push(e);
                    }
                }
            }
            catch (e) {
                e.message = `Failed to apply '${specFile.path}': ${error_1.k8sErrMsg(e)}`;
                logger_1.logger.error(e.message);
                errors.push(e);
            }
        }
        if (errors.length > 0) {
            errors[0].message = `There were errors during repo sync: ${errors.map(e => e.message).join("; ")}`;
            throw errors[0];
        }
        return;
    };
}
/**
 * Consume stream of files from project and sort them by their `path`
 * property using `localeCompare`.  Any file at the root of the
 * project, i.e., not in a subdirectory, having the extensions
 * ".json", ".yaml", or ".yml` are considered specs.
 *
 * Essentially, this function converts a FileStream into a Promise of
 * sorted ProjectFiles.
 *
 * @param syncRepo Repository of specs to sort
 * @return Sorted array of specs in project
 */
function sortSpecs(syncRepo) {
    return new Promise((resolve, reject) => {
        const specsStream = syncRepo.streamFiles(diff_1.k8sSpecGlob);
        const specs = [];
        specsStream.on("data", f => specs.push(f));
        specsStream.on("error", reject);
        specsStream.on("end", () => resolve(specs.sort((a, b) => a.path.localeCompare(b.path))));
    });
}
exports.sortSpecs = sortSpecs;
//# sourceMappingURL=sync.js.map