kui-shell
Version:
This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool
567 lines • 25.5 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const debug_1 = require("debug");
const debug = debug_1.default('k8s/controller/kubectl');
debug('loading');
const capabilities_1 = require("@kui-shell/core/api/capabilities");
const commands_1 = require("@kui-shell/core/api/commands");
const i18n_1 = require("@kui-shell/core/api/i18n");
const tables_1 = require("@kui-shell/core/api/tables");
const util_1 = require("@kui-shell/core/api/util");
const repl_util_1 = require("@kui-shell/core/api/repl-util");
const abbreviations_1 = require("./abbreviations");
const log_parser_1 = require("../util/log-parser");
const help_1 = require("../util/help");
const helm_client_1 = require("../util/discovery/helm-client");
const name_1 = require("../util/name");
const states_1 = require("../model/states");
const registry_1 = require("../view/registry");
const formatTable_1 = require("../view/formatTable");
const status_1 = require("./status");
const get_1 = require("./helm/get");
const strings = i18n_1.i18n('plugin-k8s');
const kubelike = /kubectl|oc/;
const isKubeLike = (command) => kubelike.test(command);
const parseYAML = (str) => __awaiter(void 0, void 0, void 0, function* () {
const { safeLoadAll } = yield Promise.resolve().then(() => require('js-yaml'));
const yamls = safeLoadAll(str);
return yamls.length === 1 ? yamls[0] : yamls;
});
const possiblyExportCredentials = (execOptions, env) => new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
if (!process.env.KUBECONFIG && execOptions && execOptions.credentials && execOptions.credentials.k8s) {
debug('exporting kubernetes credentials');
const { dir } = yield Promise.resolve().then(() => require('tmp'));
dir((err, path) => __awaiter(void 0, void 0, void 0, function* () {
if (err) {
reject(err);
}
else {
const { join } = yield Promise.resolve().then(() => require('path'));
const { writeFile, remove } = yield Promise.resolve().then(() => require('fs-extra'));
const { kubeconfig, ca, cafile } = execOptions.credentials.k8s;
try {
const kubeconfigFilepath = join(path, 'kubeconfig.yml');
yield Promise.all([writeFile(kubeconfigFilepath, kubeconfig), writeFile(join(path, cafile), ca)]);
env.KUBECONFIG = kubeconfigFilepath;
resolve(() => remove(path));
}
catch (err) {
reject(err);
}
}
}));
}
else {
resolve(() => {
});
}
}));
const shouldWeDisplayAsTable = (verb, entityType, output, options) => {
const hasTableVerb = verb === 'ls' ||
verb === 'history' ||
verb === 'search' ||
verb === 'list' ||
verb === 'get' ||
(verb === 'config' && entityType.match(/^get/));
return (!options.help &&
!options.h &&
verb !== 'describe' &&
verb !== 'install' &&
(!output || output === 'wide' || output === 'name' || output.match(/^custom-columns/)) &&
hasTableVerb);
};
const pre = (str) => {
const pre = document.createElement('div');
pre.classList.add('whitespace');
pre.innerText = str;
return pre;
};
const table = (decodedResult, stderr, command, verb, entityType, entity, options, execOptions) => {
const preTables = formatTable_1.preprocessTable(decodedResult.split(/^(?=LAST SEEN|NAMESPACE|NAME\s+)/m));
if (preTables && preTables.length === 1 && preTables[0].length === 0) {
return pre(decodedResult || stderr);
}
else if (preTables && preTables.length >= 1) {
if (preTables.length === 1) {
const T = formatTable_1.formatTable(command, verb, entityType, options, preTables[0]);
if (execOptions.filter) {
T.body = execOptions.filter(T.body);
}
return T;
}
else {
return {
tables: preTables.map(preTable => {
const T = formatTable_1.formatTable(command, verb, entityType, options, preTable);
if (execOptions.filter) {
T.body = execOptions.filter(T.body);
}
return T;
})
};
}
}
else {
return pre(decodedResult);
}
};
const usage = (command, hide = false) => ({
title: command,
command,
configuration: {
'short-option-groups': false
},
hide,
noHelp: true
});
const stripThese = {
'-w': true,
'--watch': true,
'--watch-only': true,
'-w=true': true,
'--watch=true': true,
'--watch-only=true': true
};
const executeLocally = (command) => (opts) => new Promise((resolve, reject) => __awaiter(void 0, void 0, void 0, function* () {
const { REPL, argv: rawArgv, argvNoOptions: argv, execOptions, parsedOptions: options, command: rawCommand } = opts;
const isKube = isKubeLike(command);
const verb = command === 'helm' && argv[1] === 'repo' ? argv[2] : argv[1];
const entityType = command === 'helm' ? command : verb && verb.match(/log(s)?/) ? verb : argv[2];
const entity = command === 'helm' ? argv[2] : entityType === 'secret' ? argv[4] : argv[3];
const output = !options.help &&
(options.output ||
options.o ||
(command === 'helm' && verb === 'get' && 'yaml') ||
(isKube && verb === 'describe' && 'yaml') ||
(isKube && verb === 'logs' && 'latest') ||
(isKube && verb === 'get' && execOptions.raw && 'json'));
if (!execOptions.raw &&
(!capabilities_1.default.isHeadless() || execOptions.isProxied) &&
!execOptions.noDelegation &&
isKube &&
((verb === 'summary' || (verb === 'get' && (output === 'yaml' || output === 'json'))) &&
(execOptions.type !== commands_1.default.ExecType.Nested || execOptions.delegationOk))) {
debug('delegating to summary provider', execOptions.delegationOk, commands_1.default.ExecType[execOptions.type].toString());
const describeImpl = (yield Promise.resolve().then(() => require('./describe'))).default;
opts.argvNoOptions[0] = 'kubectl';
opts.argv[0] = 'kubectl';
opts.command = opts.command.replace(/^_kubectl/, 'kubectl');
return describeImpl(opts)
.then(resolve)
.catch(reject);
}
else if (isKube && (verb === 'status' || verb === 'list')) {
return status_1.status(verb)(opts)
.then(resolve)
.catch(reject);
}
const statusCommand = isKube ? 'k' : command;
if (isKube && verb === 'get' && output === 'json' && execOptions.raw && !options.output) {
debug('forcing json output for raw mode execution', options);
rawArgv.push('-o');
rawArgv.push('json');
}
if (verb === 'logs' && !options.tail && !options.since) {
debug('limiting log lines');
rawArgv.push('--tail');
rawArgv.push('1000');
}
const rawArgvWithoutWatchFlag = options.watch || options.w || options['watch-only']
? rawArgv.filter((argv, index) => index < 2 || !stripThese[argv])
: rawArgv;
const entityTypeWithoutTrailingSuffix = entityType && entityType.replace(/\..*$/, '').replace(/-[a-z0-9]{9}-[a-z0-9]{5}$/, '');
const entityTypeForDisplay = abbreviations_1.default[entityTypeWithoutTrailingSuffix] || entityTypeWithoutTrailingSuffix;
const argvWithFileReplacements = yield Promise.all(rawArgvWithoutWatchFlag.slice(1).map((_) => __awaiter(void 0, void 0, void 0, function* () {
if (_.match(/^!.*/)) {
return '-';
}
else if (_.match(/\.asar\//)) {
debug('copying out of asar', _);
const { copyOut } = yield Promise.resolve().then(() => require('../util/copy'));
return copyOut(_);
}
else if (_.match(/^(@.*$)/)) {
const filepath = util_1.default.findFile(_);
if (filepath.match(/\.asar\//)) {
debug('copying @ file out of asar', filepath);
const { copyOut } = yield Promise.resolve().then(() => require('../util/copy'));
return copyOut(filepath);
}
else {
return filepath;
}
}
else {
return _;
}
})));
if (verb === 'delete' && !Object.prototype.hasOwnProperty.call(options, 'wait') && isKube) {
argvWithFileReplacements.push('--wait=false');
}
const env = Object.assign({}, process.env);
const cleanupCallback = yield possiblyExportCredentials(execOptions, env);
const cleanupAndResolve = (val) => __awaiter(void 0, void 0, void 0, function* () {
yield cleanupCallback();
resolve(val);
});
const { spawn } = yield Promise.resolve().then(() => require('child_process'));
delete env.DEBUG;
const commandForSpawn = command === 'helm' ? yield helm_client_1.default(env) : command;
const child = spawn(commandForSpawn, argvWithFileReplacements, {
env
});
child.on('error', (err) => {
console.error('error spawning kubectl', err);
reject(err);
});
const file = options.f || options.filename;
const hasFileArg = file !== undefined && typeof file !== 'boolean';
const isProgrammatic = hasFileArg && file.charAt(0) === '!';
const programmaticResource = isProgrammatic && execOptions.parameters[file.slice(1)];
if (isProgrammatic) {
const param = file.slice(1);
debug('writing to stdin', param, programmaticResource);
child.stdin.write(programmaticResource + '\n');
child.stdin.end();
}
let out = '';
child.stdout.on('data', data => {
out += data.toString();
});
let err = '';
child.stderr.on('data', data => {
err += data.toString();
});
const status = (command, code, stderr) => __awaiter(void 0, void 0, void 0, function* () {
if (hasFileArg || verb === 'delete' || verb === 'create') {
if (!execOptions.noStatus) {
const expectedState = verb === 'create' || verb === 'apply' ? states_1.FinalState.OnlineLike : states_1.FinalState.OfflineLike;
const finalState = `--final-state ${expectedState.toString()}`;
const resourceNamespace = options.n || options.namespace ? `-n ${repl_util_1.encodeComponent(options.n || options.namespace)}` : '';
debug('about to get status', file, entityType, entity, resourceNamespace);
return REPL.qexec(`${statusCommand} status ${file || entityType} ${entity || ''} ${finalState} ${resourceNamespace} --watch`, undefined, undefined, { parameters: execOptions.parameters }).catch(err => {
if (err.code === 404 && expectedState === states_1.FinalState.OfflineLike) {
debug('resource not found after status check, but that is ok because that is what we wanted');
return out;
}
else {
console.error('error constructing status', err);
return err;
}
});
}
else {
return Promise.resolve(true);
}
}
else if (code && code !== 0) {
const error = new Error(stderr || `${command} exited with an error`);
error.code = code;
return Promise.reject(error);
}
else {
return Promise.resolve(out || true);
}
});
child.on('close', (code) => __awaiter(void 0, void 0, void 0, function* () {
if (err.length > 0 || code !== 0) {
debug('exec has stderr with code %s', code);
debug('exec stderr args', argvWithFileReplacements.join(' '));
debug('exec stderr', err);
}
else if (verb === 'delete') {
debug('exec OK', argvWithFileReplacements.join(' '));
}
const originalCode = code;
const isUsage = code !== 0 && verb === 'config' && !entityType && !entity;
if (isUsage) {
code = 0;
out = err;
}
const noResources = err.match(/no resources found/i);
if (code !== 0 || noResources) {
const message = err;
const fileNotFound = message.match(/error: the path/);
const codeForREPL = noResources || message.match(/not found/i) || message.match(/doesn't have/i)
? 404
: message.match(/already exists/i)
? 409
: fileNotFound
? 412
: 500;
debug('handling non-zero exit code %s', code, codeForREPL, err);
const nope = () => __awaiter(void 0, void 0, void 0, function* () {
if (execOptions.failWithUsage) {
reject(new Error(undefined));
}
else {
const error = new Error(message);
error.code = codeForREPL;
reject(error);
}
});
if (codeForREPL === 404 || codeForREPL === 409 || codeForREPL === 412) {
if (codeForREPL === 404 && originalCode === 0 && verb === 'get' && (options.w || options.watch)) {
debug('return an empty watch table');
return cleanupAndResolve(tables_1.default.formatWatchableTable(new tables_1.default.Table({ body: [] }), {
refreshCommand: rawCommand.replace(/--watch=true|-w=true|--watch-only=true|--watch|-w|--watch-only/g, ''),
watchByDefault: true
}));
}
const error = new Error(err);
error.code = codeForREPL;
debug('rejecting without usage', codeForREPL, error);
reject(error);
}
else if ((verb === 'create' || verb === 'apply' || verb === 'delete') && hasFileArg) {
debug('fetching status after error');
status(command, codeForREPL, err)
.then(cleanupAndResolve)
.catch(reject);
}
else {
nope();
}
}
else if (execOptions.raw ||
(capabilities_1.default.isHeadless() &&
!output &&
!shouldWeDisplayAsTable(verb, entityType, output, options) &&
execOptions.type === commands_1.default.ExecType.TopLevel &&
!execOptions.isProxied)) {
debug('resolving raw', argvWithFileReplacements.join(' '), output);
if (output === 'json') {
try {
const json = JSON.parse(out);
cleanupAndResolve(json.items || json);
}
catch (err) {
console.error(err);
cleanupAndResolve(pre(out));
}
}
else {
cleanupAndResolve(out.trim());
}
}
else if (options.help || options.h || argv.length === 1 || isUsage) {
try {
cleanupAndResolve(help_1.renderHelp(out, command, verb, originalCode));
}
catch (err) {
console.error('error rendering help', err);
reject(out);
}
}
else if (output === 'json' || output === 'yaml' || verb === 'logs') {
debug('formatting structured output', output);
const result = output === 'json' ? JSON.parse(out) : verb === 'logs' ? log_parser_1.formatLogs(out, execOptions) : out;
if (capabilities_1.default.isHeadless() && execOptions.type === commands_1.default.ExecType.TopLevel && !execOptions.isProxied) {
debug('directing resolving', capabilities_1.default.isHeadless());
return cleanupAndResolve(result);
}
const modes = [
{
mode: 'result',
direct: rawCommand,
label: strings(verb === 'describe' ? 'describe' : output === 'json' || output === 'yaml' ? output.toUpperCase() : output),
defaultMode: true
}
];
if (verb === 'logs') {
const directCmd = rawCommand.replace(/^_kubectl(\s)?/, 'kubectl$1').replace(/^_k(\s)?/, 'kubectl$1');
modes.push({
mode: 'previous',
label: strings('previous'),
direct: `${directCmd} --previous`,
execOptions: {
exec: 'pexec'
}
});
if (options.previous) {
modes[0].defaultMode = false;
modes[1].defaultMode = true;
}
}
const yaml = verb === 'get' && (yield parseYAML(out));
if (Array.isArray(yaml)) {
const { safeDump } = yield Promise.resolve().then(() => require('js-yaml'));
cleanupAndResolve({
type: 'custom',
content: safeDump(yaml),
contentType: 'yaml'
});
return;
}
const { name, nameHash } = name_1.default(yaml);
const badges = [];
if (verb === 'describe') {
const getCmd = opts.command.replace(/describe/, 'get').replace(/(-o|--output)[= ](yaml|json)/, '');
modes.push({
mode: 'raw',
label: 'YAML',
direct: `${getCmd} -o yaml`,
order: 999,
leaveBottomStripeAlone: true
});
}
const content = result;
const startTime = yaml && yaml.status && yaml.status.startTime && new Date(yaml.status.startTime);
const endTime = yaml && yaml.status && yaml.status.completionTime && new Date(yaml.status.completionTime);
const duration = startTime && endTime && endTime.getTime() - startTime.getTime();
const version = yaml && yaml.metadata && yaml.metadata.labels && yaml.metadata.labels.version;
const record = {
type: 'custom',
isEntity: verb === 'logs' || verb === 'describe' || (yaml && yaml.metadata !== undefined),
name: name || entity,
nameHash,
packageName: (yaml && yaml.metadata && yaml.metadata.namespace) || '',
namespace: options.namespace || options.n,
duration,
version,
prettyType: (yaml && yaml.kind) || entityTypeForDisplay || command,
noCost: true,
modes,
badges: badges.filter(x => x),
resource: yaml,
content
};
record['contentType'] = output;
debug('exec output json', record);
cleanupAndResolve(record);
}
else if (isKube && verb === 'run' && argv[2]) {
const entity = argv[2];
const namespace = options.namespace || options.n || 'default';
debug('status after kubectl run', entity, namespace);
REPL.qexec(`k status deploy "${entity}" -n "${namespace}" --final-state ${states_1.FinalState.OnlineLike.toString()} --watch`)
.then(cleanupAndResolve)
.catch(reject);
}
else if ((hasFileArg || (isKube && entity)) && (verb === 'create' || verb === 'apply' || verb === 'delete')) {
debug('status after success');
status(command)
.then(cleanupAndResolve)
.catch(reject);
}
else if (registry_1.registry[command] && registry_1.registry[command][verb]) {
debug('using custom formatter');
cleanupAndResolve(registry_1.registry[command][verb].format(command, verb, entityType, options, out, execOptions));
}
else if (shouldWeDisplayAsTable(verb, entityType, output, options)) {
const tableModel = table(out, err, command, verb, command === 'helm' ? '' : entityType, entity, options, execOptions);
if ((options.watch || options.w) && (tables_1.default.isTable(tableModel) || tables_1.default.isMultiTable(tableModel))) {
cleanupAndResolve(tables_1.default.formatWatchableTable(tableModel, {
refreshCommand: rawCommand.replace(/--watch=true|-w=true|--watch-only=true|--watch|-w|--watch-only/g, ''),
watchByDefault: true
}));
}
else {
cleanupAndResolve(tableModel);
}
}
else {
debug('passing through preformatted output');
cleanupAndResolve(pre(out));
}
}));
}));
const _kubectl = executeLocally('kubectl');
exports._helm = executeLocally('helm');
function helm(opts) {
const idx = opts.argvNoOptions.indexOf('helm');
if (opts.argvNoOptions[idx + 1] === 'get') {
return get_1.default(opts);
}
else {
return exports._helm(opts);
}
}
const shouldSendToPTY = (opts) => (opts.argvNoOptions.length > 1 && (opts.argvNoOptions[1] === 'exec' || opts.argvNoOptions[1] === 'edit')) ||
(opts.argvNoOptions[1] === 'logs' &&
(opts.parsedOptions.f !== undefined || (opts.parsedOptions.follow && opts.parsedOptions.follow !== 'false'))) ||
opts.argvNoOptions.includes('|');
function kubectl(opts) {
return __awaiter(this, void 0, void 0, function* () {
const { REPL } = opts;
const semi = yield REPL.semicolonInvoke(opts);
if (semi) {
return semi;
}
if (!capabilities_1.default.isHeadless() && shouldSendToPTY(opts)) {
debug('redirect exec command to PTY');
const commandToPTY = opts.command.replace(/^k(\s)/, 'kubectl$1');
return REPL.qexec(`sendtopty ${commandToPTY}`, opts.block, undefined, Object.assign({}, opts.execOptions, { rawResponse: true }));
}
else if (!capabilities_1.default.inBrowser() || opts.argvNoOptions[1] === 'summary') {
return _kubectl(opts);
}
else {
const command = opts.command.replace(/^kubectl(\s)?/, '_kubectl$1').replace(/^k(\s)?/, '_kubectl$1');
return REPL.qexec(command, opts.block, undefined, {
tab: opts.tab,
raw: opts.execOptions.raw,
noDelegation: opts.execOptions.noDelegation,
delegationOk: opts.execOptions.type !== commands_1.default.ExecType.Nested
});
}
});
}
const flags = {
boolean: [
'w',
'watch',
'watch-only',
'A',
'all-namespaces',
'ignore-not-found',
'no-headers',
'R',
'recursive',
'server-print',
'show-kind',
'show-labels'
]
};
exports.default = (commandTree) => __awaiter(void 0, void 0, void 0, function* () {
yield commandTree.listen('/_kubectl', _kubectl, {
usage: usage('kubectl', true),
flags,
requiresLocal: true
});
const kubectlCmd = yield commandTree.listen('/kubectl', kubectl, {
usage: usage('kubectl'),
flags,
inBrowserOk: true
});
yield commandTree.synonym('/k', kubectl, kubectlCmd, {
usage: usage('kubectl'),
flags,
inBrowserOk: true
});
yield commandTree.listen('/helm', helm, {
usage: usage('helm'),
flags,
requiresLocal: true
});
yield commandTree.listen('/kdebug', ({ argvNoOptions, parsedOptions, execOptions }) => __awaiter(void 0, void 0, void 0, function* () {
const file = argvNoOptions[argvNoOptions.length - 1];
const { readFile } = yield Promise.resolve().then(() => require('fs-extra'));
const out = (yield readFile(file)).toString();
const command = parsedOptions.command || 'kubectl';
const verb = parsedOptions.verb || 'get';
const entityType = parsedOptions.entityType || 'pod';
const tableModel = table(out, '', command, verb, command === 'helm' ? '' : entityType, undefined, {}, execOptions);
return tableModel;
}), { inBrowserOk: true });
});
//# sourceMappingURL=kubectl.js.map