"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.uniqueSpecFile = exports.matchSpec = exports.sameObject = exports.syncResources = exports.syncApplication = void 0;
const configuration_1 = require("@atomist/automation-client/lib/configuration");
const string_1 = require("@atomist/automation-client/lib/internal/util/string");
const projectUtils = require("@atomist/automation-client/lib/project/util/projectUtils");
const child_process_1 = require("@atomist/automation-client/lib/util/child_process");
const logger_1 = require("@atomist/automation-client/lib/util/logger");
const CachingProjectLoader_1 = require("../../../api-helper/project/CachingProjectLoader");
const config_1 = require("../config");
const spec_1 = require("../deploy/spec");
const request_1 = require("../kubernetes/request");
const spec_2 = require("../kubernetes/spec");
const retry_1 = require("../support/retry");
const clone_1 = require("./clone");
const diff_1 = require("./diff");
const tag_1 = require("./tag");
/**
 * Synchronize changes from deploying app to the configured syncRepo.
 * If no syncRepo is configured, do nothing.
 *
 * @param app Kubernetes application change that triggered the sync
 * @param resources Kubernetes resource objects to synchronize
 * @param action Action performed, "upsert" or "delete"
 */
async function syncApplication(app, resources, action = "upsert") {
    const slug = request_1.appName(app);
    const syncOpts = configuration_1.configurationValue("sdm.k8s.options.sync", {});
    if (!config_1.validSyncOptions(syncOpts)) {
        return;
    }
    const syncRepo = syncOpts.repo;
    if (resources.length < 1) {
        return;
    }
    const projectLoadingParameters = {
        credentials: syncOpts.credentials,
        cloneOptions: clone_1.defaultCloneOptions,
        id: syncRepo,
        readOnly: false,
    };
    const projectLoader = configuration_1.configurationValue("sdm.projectLoader", new CachingProjectLoader_1.CachingProjectLoader());
    try {
        await projectLoader.doWithProject(projectLoadingParameters, syncResources(app, resources, action, syncOpts));
    }
    catch (e) {
        e.message = `Failed to perform sync resources from ${slug} to sync repo ${syncRepo.owner}/${syncRepo.repo}: ${e.message}`;
        logger_1.logger.error(e.message);
        throw e;
    }
    return;
}
exports.syncApplication = syncApplication;
/**
 * Update the sync repo with the changed resources from a
 * KubernetesApplication.  For each changed resource in `resources`,
 * loop through all the existing Kubernetes spec files, i.e., those
 * that match [[k8sSpecGlob]], to see if the apiVersion, kind, name,
 * and namespace, which may be undefined, match.  If a match is found,
 * update that spec file.  If no match is found, create a unique file
 * name and store the resource spec in it.  If changes are made,
 * commit and push the changes.
 *
 * @param app Kubernetes application object
 * @param resources Resources that were upserted as part of this application
 * @param action Action performed, "upsert" or "delete"
 * @param opts Repo sync options, passed to the sync action
 * @return Function that updates the sync repo with the resource specs
 */
function syncResources(app, resources, action, opts) {
    return async (syncProject) => {
        const slug = `${syncProject.id.owner}/${syncProject.id.repo}`;
        const aName = request_1.appName(app);
        const specs = [];
        await projectUtils.doWithFiles(syncProject, diff_1.k8sSpecGlob, async (file) => {
            try {
                const spec = await spec_1.parseKubernetesSpecFile(file);
                specs.push({ file, spec });
            }
            catch (e) {
                logger_1.logger.warn(`Failed to process sync repo ${slug} spec ${file.path}, ignoring: ${e.message}`);
            }
        });
        const [syncAction, syncVerb] = (action === "delete") ? [resourceDeleted, "Delete"] : [resourceUpserted, "Update"];
        for (const resource of resources) {
            const fileSpec = matchSpec(resource, specs);
            await syncAction(resource, syncProject, fileSpec, opts);
        }
        if (await syncProject.isClean()) {
            return;
        }
        try {
            const v = request_1.isKubernetesApplication(app) ? app.image.replace(/^.*:/, ":") : "";
            await syncProject.commit(`${syncVerb} ${aName}${v}\n\n[atomist:generated] ${tag_1.commitTag()}\n`);
        }
        catch (e) {
            e.message = `Failed to commit resource changes for ${aName} to sync repo ${slug}: ${e.message}`;
            logger_1.logger.error(e.message);
            throw e;
        }
        try {
            await syncProject.push();
        }
        catch (e) {
            logger_1.logger.warn(`Failed on initial sync repo ${slug} push attempt: ${e.message}`);
            try {
                await retry_1.logRetry(async () => {
                    const pullResult = await child_process_1.execPromise("git", ["pull", "--rebase"], { cwd: syncProject.baseDir });
                    logger_1.logger.debug(`Sync project 'git pull --rebase': ${pullResult.stdout}; ${pullResult.stderr}`);
                    await syncProject.push();
                }, `sync project ${slug} git pull and push`);
            }
            catch (e) {
                e.message = `Failed sync repo ${slug} pull and rebase retries: ${e.message}`;
                logger_1.logger.error(e.message);
                throw e;
            }
        }
    };
}
exports.syncResources = syncResources;
/**
 * Persist the creation of or update to a resource to the sync repo
 * project.
 *
 * @param resource Kubernetes resource that was upserted
 * @param p Sync repo project
 * @param fs File and spec object that matches resource, may be undefined
 */
async function resourceUpserted(resource, p, fs, opts) {
    let format = "yaml";
    if (fs && fs.file) {
        format = (/\.ya?ml$/.test(fs.file.path)) ? "yaml" : "json";
    }
    else if (opts.specFormat) {
        format = opts.specFormat;
    }
    const stringifyOptions = {
        format,
        secretKey: opts.secretKey,
    };
    const resourceString = await spec_2.kubernetesSpecStringify(resource, stringifyOptions);
    if (fs) {
        await fs.file.setContent(resourceString);
    }
    else {
        const specPath = await uniqueSpecFile(resource, p, format);
        await p.addFile(specPath, resourceString);
    }
}
/**
 * Safely persist the deletion of a resource to the sync repo project.
 * If `fs` is `undefined`, do nothing.
 *
 * @param resource Kubernetes resource that was upserted
 * @param p Sync repo project
 * @param fs File and spec object that matches resource, may be `undefined`
 */
async function resourceDeleted(resource, p, fs) {
    if (fs) {
        await p.deleteFile(fs.file.path);
    }
}
/**
 * Determine if two Kubernetes resource specifications represent the
 * same object.  When determining if they are the same, only the kind,
 * name, and namespace, which may be `undefined`, must match.  The
 * apiVersion is not considered when matching because the same
 * resource can appear under different API versions.  Other object
 * properties are not considered.
 *
 * @param a First Kubernetes object spec to match
 * @param b Second Kubernetes object spec to match
 * @return `true` if specs match, `false` otherwise
 */
function sameObject(a, b) {
    return a && b && a.metadata && b.metadata &&
        a.kind === b.kind &&
        a.metadata.name === b.metadata.name &&
        a.metadata.namespace === b.metadata.namespace;
}
exports.sameObject = sameObject;
/**
 * Search `fileSpecs` for a spec that matches `spec`.  To be
 * considered a match, the kind, name, and namespace, which may be
 * undefined, must match.  The apiVersion is not considered when
 * matching because the same resource can appear under different API
 * versions.
 *
 * @param spec Kubernetes object spec to match
 * @param fileSpecs Array of spec and file objects to search
 * @return First file and spec object to match spec or `undefined` if no match is found
 */
function matchSpec(spec, fileSpecs) {
    return fileSpecs.find(fs => sameObject(spec, fs.spec));
}
exports.matchSpec = matchSpec;
/**
 * Return a unique name for a resource spec that lexically sorts so
 * resources that should be created earlier than others sort earlier
 * than others.
 *
 * @param resource Kubernetes object spec
 * @param p Kubernetes spec project
 * @return Unique spec file name that sorts properly
 */
async function uniqueSpecFile(resource, p, format) {
    const specRoot = spec_2.kubernetesSpecFileBasename(resource);
    const specExt = `.${format}`;
    let specPath = specRoot + specExt;
    while (await p.getFile(specPath)) {
        specPath = specRoot + "_" + string_1.guid().split("-")[0] + specExt;
    }
    return specPath;
}
exports.uniqueSpecFile = uniqueSpecFile;
//# sourceMappingURL=application.js.map