UNPKG

@adpt/cloud

Version:
323 lines 12 kB
"use strict"; /* * Copyright 2019 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 }); 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 node_fetch_1 = tslib_1.__importDefault(require("node-fetch")); 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"); let kubectlLoc; function kubectlPlatform(platform) { switch (platform) { case "linux": case "darwin": return platform; case "win32": return "windows"; default: throw new Error(`Unsupported platform for kubectl: ${platform}`); } } /** * Downloads kubectl and returns path to its location * * @returns path to kubectl on host * @internal */ async function getKubectl() { if (kubectlLoc !== undefined) return kubectlLoc; const loc = path.join(await utils_1.mkdtmp("kubectl"), "kubectl"); const kubeRelease = "v1.15.3"; const platform = kubectlPlatform(os.platform()); const kubectlUrl = `https://storage.googleapis.com/kubernetes-release/release/${kubeRelease}/bin/${platform}/amd64/kubectl`; const kubectlBinResp = await node_fetch_1.default(kubectlUrl); const kubectlBin = fs_extra_1.createWriteStream(loc); if (kubectlBinResp.status !== 200) throw new Error(`Could not get kubectl from ${kubectlUrl}: ${kubectlBinResp.statusText}`); kubectlBinResp.body.pipe(kubectlBin); await new Promise((res, rej) => { let err; kubectlBin.on("close", res); kubectlBin.on("error", (e) => { if (!err) { rej(e); err = e; } else { // tslint:disable-next-line: no-console console.error(`Unhandled error writing kubectl:`, e); } }); kubectlBinResp.body.on("error", (e) => { if (!err) { rej(e); err = e; } else { // tslint:disable-next-line: no-console console.error(`Unhandled error downloading kubectl:`, e); } }); }); await fs_extra_1.chmod(loc, "755"); kubectlLoc = loc; return loc; } exports.getKubectl = getKubectl; const kubectlDefaults = {}; async function kubectl(args, options) { const kubectlPath = await getKubectl(); const opts = 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({}, 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({}, 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 += "\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; e.message += e.all ? "\n" + e.all : ""; return e; } } const lastApplied = "kubectl.kubernetes.io/last-applied-configuration"; /** @internal */ async function kubectlDiff(options) { const opts = 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, { 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.stderr === "exit status 1") { 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({}, 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 ("all" in e) e.message += "\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({}, 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)) { e.message += e.all ? "\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