"use strict";
/*
 * Copyright 2019-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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.kubectlProxy = exports.kubectlOpManifest = exports.kubectlDiff = exports.kubectlGet = exports.kubectl = exports.getKubectl = void 0;
const tslib_1 = require("tslib");
const utils_1 = require("@adpt/utils");
const execa = require("execa");
const fs_extra_1 = require("fs-extra");
const ld = tslib_1.__importStar(require("lodash"));
const os = tslib_1.__importStar(require("os"));
const path = tslib_1.__importStar(require("path"));
const readline = tslib_1.__importStar(require("readline"));
const common_1 = require("../common");
const env_1 = require("../env");
function kubectlPlatform(platform) {
    switch (platform) {
        case "linux":
        case "darwin":
            return platform;
        case "win32":
            return "windows";
        default:
            throw new Error(`Unsupported platform for kubectl: ${platform}`);
    }
}
const kubeRelease = "v1.19.2";
/**
 * Downloads kubectl and returns path to its location
 *
 * @returns path to kubectl on host
 * @internal
 */
async function getKubectl() {
    const platform = kubectlPlatform(os.platform());
    const extension = platform === "windows" ? ".exe" : "";
    const url = `https://storage.googleapis.com/kubernetes-release/release/${kubeRelease}/bin/${platform}/amd64/kubectl${extension}`;
    const { file } = await utils_1.fetchToCache({
        name: "kubectl",
        url,
        mode: 0o700,
        version: kubeRelease,
    });
    return file;
}
exports.getKubectl = getKubectl;
const kubectlDefaults = {};
async function kubectl(args, options) {
    const kubectlPath = await getKubectl();
    const opts = Object.assign(Object.assign({}, kubectlDefaults), options);
    const actualArgs = [];
    const kubeconfigLoc = opts.kubeconfig;
    if (kubeconfigLoc)
        actualArgs.push("--kubeconfig", kubeconfigLoc);
    actualArgs.push(...args);
    let execaOptions = { all: true };
    if (options.env !== undefined) {
        execaOptions = Object.assign(Object.assign({}, execaOptions), { env: env_1.mergeEnvSimple(options.env) });
    }
    return execa(kubectlPath, actualArgs, execaOptions);
}
exports.kubectl = kubectl;
async function getKubeconfigPath(tmpDir, config) {
    if (ld.isString(config))
        return config;
    const loc = path.join(tmpDir, "kubeconfig");
    await fs_extra_1.writeFile(loc, JSON.stringify(config));
    return loc;
}
const getManifestDefaults = {};
/** @internal */
async function kubectlGet(options) {
    const opts = Object.assign(Object.assign({}, getManifestDefaults), options);
    const { kubeconfig, kind, name } = opts;
    return utils_1.withTmpDir(async (tmpDir) => {
        const configPath = kubeconfig && await getKubeconfigPath(tmpDir, kubeconfig);
        const args = ["get", "-o", "json", kind, name];
        let result;
        try {
            result = await kubectl(args, { kubeconfig: configPath });
        }
        catch (e) {
            if (common_1.isExecaError(e) && e.all) {
                e.message = `${e.shortMessage}\n${e.all}`;
                if (e.exitCode !== 0) {
                    if (e.all.match(/Error from server \(NotFound\)/))
                        return undefined;
                }
            }
            throw e;
        }
        return JSON.parse(result.stdout);
    });
}
exports.kubectlGet = kubectlGet;
const diffDefaults = {};
async function doExeca(f) {
    try {
        return await f();
    }
    catch (e) {
        if (!common_1.isExecaError(e))
            throw e;
        if (e.all)
            e.message = `${e.shortMessage}\n${e.all}`;
        return e;
    }
}
const lastApplied = "kubectl.kubernetes.io/last-applied-configuration";
const diffEnv = os.platform() === "win32" ? {
    KUBECTL_EXTERNAL_DIFF: path.join(__dirname, "diff.cmd")
} : {};
/** @internal */
async function kubectlDiff(options) {
    const opts = Object.assign(Object.assign({}, diffDefaults), options);
    const { kubeconfig, manifest } = opts;
    return utils_1.withTmpDir(async (tmpDir) => {
        const configPath = kubeconfig && await getKubeconfigPath(tmpDir, kubeconfig);
        const manifestLoc = path.join(tmpDir, "manifest.json");
        await fs_extra_1.writeFile(manifestLoc, JSON.stringify(manifest));
        const args = ["diff", "-f", manifestLoc];
        let result = await doExeca(() => kubectl(args, {
            env: diffEnv,
            kubeconfig: configPath,
        }));
        const serverInternalErrorRegex = new RegExp("^Error from server \\(InternalError\\)");
        if ((result.exitCode !== 0) && serverInternalErrorRegex.test(result.stderr)) {
            // Some k8s clusters, GKE included, do not support API-server dry-run for all resources,
            // which kubectl diff uses so fallback to using the old style client side diff algorithm that kubectl uses.
            result = await doExeca(() => kubectl(["get", "-o", "json", "-f", manifestLoc], { kubeconfig: configPath }));
            if (result.exitCode === 0) {
                const srvManifest = JSON.parse(result.stdout);
                if (!srvManifest.annotations || !srvManifest.annotations[lastApplied]) {
                    return {
                        //FIXME(manishv) mimic kubectl diff output here
                        diff: `No ${lastApplied} annotation, assuming diff`,
                        errs: "",
                        forbidden: false,
                        clientFallback: true
                    };
                }
                const srvApplyManifestJSON = srvManifest.annotations[lastApplied];
                const srvApplyManifest = JSON.parse(srvApplyManifestJSON);
                const strippedManifest = JSON.parse(JSON.stringify(manifest));
                if (!ld.isEqual(strippedManifest, srvApplyManifest)) {
                    return {
                        diff: "Unknown diff",
                        errs: "",
                        forbidden: false,
                        clientFallback: true
                    };
                }
                else {
                    return {
                        errs: result.stderr,
                        forbidden: false,
                        clientFallback: true
                    };
                }
            }
        }
        if (result.exitCode === 0) {
            return {
                errs: result.stderr,
                forbidden: false,
                clientFallback: false
            };
        }
        const forbiddenRegex = new RegExp(`^The ${manifest.kind} \"${manifest.metadata.name}\" is invalid: spec: Forbidden`);
        if (forbiddenRegex.test(result.stderr)) {
            return {
                errs: result.stderr,
                forbidden: true,
                clientFallback: false
            };
        }
        if ((result.exitCode === 1)
            && (result.stderr.length === 0)
            && (result.stdout.startsWith("diff -u"))) {
            return {
                diff: result.stdout,
                errs: "",
                forbidden: false,
                clientFallback: false
            };
        }
        throw result; //Should be ExecaError if result.exitCode was not zero
    });
}
exports.kubectlDiff = kubectlDiff;
const opManifestDefaults = {
    dryRun: false,
    wait: false
};
async function kubectlOpManifest(op, options) {
    const opts = Object.assign(Object.assign({}, opManifestDefaults), options);
    const { kubeconfig, manifest, dryRun } = opts;
    return utils_1.withTmpDir(async (tmpDir) => {
        const configPath = kubeconfig && await getKubeconfigPath(tmpDir, kubeconfig);
        const manifestLoc = path.join(tmpDir, "manifest.json");
        await fs_extra_1.writeFile(manifestLoc, JSON.stringify(manifest));
        const args = [op, "-f", manifestLoc];
        if (op !== "delete" && dryRun)
            args.push("--dry-run");
        if (op === "delete" && dryRun)
            throw new Error("Cannot dry-run delete");
        if (op !== "create")
            args.push(`--wait=${opts.wait}`);
        try {
            return await kubectl(args, { kubeconfig: configPath });
        }
        catch (e) {
            if (e.all)
                e.message = `${e.shortMessage}\n${e.all}`;
            throw e;
        }
    });
}
exports.kubectlOpManifest = kubectlOpManifest;
const proxyDefaults = {};
async function firstLine(stream) {
    return new Promise((res, rej) => {
        const lines = readline.createInterface({
            input: stream,
            crlfDelay: Infinity
        });
        let done = false;
        lines.prependOnceListener("line", (text) => {
            if (!done)
                res({ first: text, rest: lines });
            done = true;
        });
        lines.prependOnceListener("error", (e) => {
            if (!done)
                rej(e);
            done = true;
        });
        lines.prependOnceListener("close", () => {
            if (!done)
                rej(new Error("Stream closed before first line was complete"));
            done = true;
        });
    });
}
function extractHostPort(line) {
    const match = /^Starting to serve on (.+)$/.exec(line);
    if (match === null)
        throw new Error("Cannot parse host line");
    if (match[1] === undefined || match[1] === "")
        throw new Error("No host/port information found");
    return match[1];
}
/**  @internal */
async function kubectlProxy(options) {
    const opts = Object.assign(Object.assign({}, proxyDefaults), options);
    const kubeconfig = opts.kubeconfig;
    return utils_1.withTmpDir(async (tmpDir) => {
        const configPath = kubeconfig && await getKubeconfigPath(tmpDir, kubeconfig);
        const kubectlPath = await getKubectl();
        const args = [];
        if (configPath)
            args.push("--kubeconfig", configPath);
        args.push("proxy", "--port=0");
        const child = execa(kubectlPath, args, { all: true });
        const kill = () => child.kill();
        let hostPort;
        try {
            const { first: proxyInfoStr, rest } = await firstLine(child.stdout);
            rest.on("line", () => { return; }); //Eat extra lines, just in case
            hostPort = extractHostPort(proxyInfoStr);
        }
        catch (e) {
            if (common_1.isExecaError(e)) {
                if (e.all)
                    e.message = `${e.shortMessage}\n${e.all}`;
            }
            else {
                kill();
                e.message = `Failed to extract proxy host from command: ${kubectlPath} ${args.join(" ")} ` + e.message;
            }
            throw e;
        }
        const url = `http://${hostPort}`;
        return {
            url,
            child,
            kill
        };
    });
}
exports.kubectlProxy = kubectlProxy;
//# sourceMappingURL=kubectl.js.map