@adpt/cloud
Version:
AdaptJS cloud component library
323 lines • 12 kB
JavaScript
;
/*
* 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