UNPKG

kui-shell

Version:

This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool

567 lines 25.5 kB
"use strict"; 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