UNPKG

@kui-shell/plugin-kubectl

Version:

Kubernetes visualization plugin for kubernetes

530 lines (528 loc) 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.doStatus = void 0; exports.isResourceReady = isResourceReady; var _debug = _interopRequireDefault(require("debug")); var _pluginKubectlCore = require("@kui-shell/plugin-kubectl-core"); var _core = require("@kui-shell/core"); var _source = _interopRequireDefault(require("./source")); var _explain = require("./explain"); var _jobs = require("./dashboard/jobs"); var _fqn = require("./fqn"); var _formatTable = require("../../lib/view/formatTable"); var _status = _interopRequireDefault(require("../client/direct/status")); var _options = require("./options"); var _getWatch = require("./watch/get-watch"); var _fetchFile = require("../../lib/util/fetch-file"); var _states = require("../../lib/model/states"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* * Copyright 2018 The Kubernetes Authors * * 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. */ var __awaiter = void 0 && (void 0).__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()); }); }; const strings = (0, _core.i18n)('plugin-kubectl'); const debug = (0, _debug.default)('plugin-kubectl/controller/kubectl/status'); /** * @param file an argument to `-f` or `-k`; e.g. `kubectl -f <file>` * */ function getResourcesReferencedByFile(file, args, namespaceFromCommandLine) { return __awaiter(this, void 0, void 0, function* () { const { isFor } = (0, _options.fileOfWithDetail)(args); const [{ loadAll }, raw] = yield Promise.all([Promise.resolve().then(() => require('js-yaml')), (0, _fetchFile.fetchFilesVFS)(args, file, true)]); const models = _core.Util.flatten(raw.map(_ => loadAll(_.data))); const resourcesToWaitFor = models.filter(_ => _.metadata).map(({ apiVersion, kind, metadata: { name, namespace = namespaceFromCommandLine } }) => { const { group, version } = (0, _fqn.versionOf)(apiVersion); return { group, version, kind, name, namespace }; }); return { resourcesToWaitFor, kuiSourceRef: (0, _source.default)(raw, isFor) }; }); } function getResourcesReferencedByKustomize(kusto, args, namespace) { return __awaiter(this, void 0, void 0, function* () { const [kuiSourceRef, { load }] = yield Promise.all([(0, _fetchFile.fetchKusto)(args, kusto), Promise.resolve().then(() => require('js-yaml'))]); const resourcesToWaitFor = yield Promise.all(kuiSourceRef.templates.map(raw => load(raw.data)).map(resource => __awaiter(this, void 0, void 0, function* () { const { apiVersion, kind, metadata } = resource; const { group, version } = (0, _fqn.versionOf)(apiVersion); return { group, version, kind, name: metadata.name, namespace: metadata.namespace || namespace }; }))); return { kuiSourceRef, resourcesToWaitFor }; }); } /** * @param argvRest the argv after `kubectl status`, with options stripped off * */ function getResourcesReferencedByCommandLine(verb, argvRest, namespace, finalState) { return __awaiter(this, void 0, void 0, function* () { // Notes: kubectl create secret <generic> <name> <-- the name is in a different slot :( const [kind, nameGroupVersion, nameAlt] = argvRest; const isDelete = finalState === _states.FinalState.OfflineLike; if (isDelete) { // kubectl delete (ns [m1 m2 m2 m3]) // ^ argvRest ^ // ^ slice(1) ^ return { resourcesToWaitFor: argvRest.slice(1).map(nameGroupVersion => nameGroupVersion.split(/\./)).map(([name, group, version]) => ({ group, version, kind, name, namespace })) }; } else if (verb === 'create') { const isCreateSecret = /secret(s)?/i.test(kind); if (isCreateSecret) { // ugh, the phrasing is different for creating secrets, // e.g. "create <kind> default <name>", rather than the usual // "create <kind> <name>" return { resourcesToWaitFor: [{ name: nameAlt, kind, namespace }] }; } else { return { resourcesToWaitFor: argvRest.slice(1).map(nameGroupVersion => nameGroupVersion.split(/\./)).map(([name, group, version]) => ({ group, version, kind, name, namespace })) }; } } const [name, group, version] = nameGroupVersion.split(/\./); return { resourcesToWaitFor: [{ group, version, kind, name, namespace }] }; }); } /** * Has the resource represented by the given table Row reached its * desired final state? * */ function isResourceReady(row, finalState) { const status = row.attributes.find(_ => /STATUS/i.test(_.key)); if (status !== undefined) { // primary plan: use the STATUS column return (0, _states.isDone)(status.value, finalState); } else { // backup plan: use the READY column, of the form nReady/nTotal const ready = row.attributes.find(_ => /READY/i.test(_.key)); if (ready !== undefined) { const [nReady, nTotal] = ready.value.split(/\//); return nReady && nTotal && nReady === nTotal; } } // heuristic: if we find neither a STATUS nor a READY column, // then assume it's ready; e.g. configmaps have this property return true; } /** * The table push notification `update` routine assumes it has a copy of the rows. * */ function clone(row) { const copy = Object.assign({}, row); copy.attributes = row.attributes.map(_ => Object.assign({}, _)); return copy; } class StatusPoller { constructor(tab, ref, row, finalState, done, pusher, contextArgs, command, pollInterval = 1000, ladder = StatusPoller.calculateLadder(pollInterval)) { this.tab = tab; this.ref = ref; this.row = row; this.finalState = finalState; this.done = done; this.pusher = pusher; this.contextArgs = contextArgs; this.command = command; this.ladder = ladder; this.pollOnce(0); } pollOnce(iter) { return __awaiter(this, void 0, void 0, function* () { const sleepTime = iter < this.ladder.length ? this.ladder[iter] : this.ladder[this.ladder.length - 1]; debug('pollOnce', this.ref, sleepTime, (0, _fqn.fqnOfRef)(this.ref)); try { const table = yield this.tab.REPL.qexec(`${this.command} get ${(0, _fqn.fqnOfRef)(this.ref)} ${this.contextArgs} -o wide`); debug('pollOnce table', table); if (table && table.body && table.body.length === 1) { const row = table.body[0]; const isReady = isResourceReady(row, this.finalState); const newStatusAttr = row.attributes.find(_ => /STATUS/i.test(_.key)) || row.attributes.find(_ => /READY/i.test(_.key)); const rowForUpdate = clone(this.row); const statusAttr = rowForUpdate.attributes.find(({ key }) => /STATUS/i.test(key)); statusAttr.value = newStatusAttr ? newStatusAttr.value : 'Ready'; if (isReady) { statusAttr.css = this.finalState === _states.FinalState.OnlineLike ? _pluginKubectlCore.TrafficLight.Green : _pluginKubectlCore.TrafficLight.Red; } this.pusher.update(rowForUpdate); if (isReady) { debug('resource is ready', this.ref, this.row, newStatusAttr); this.done(); return; } } else { console.error('unexpected tabular response in poller', table); } } catch (error) { const err = error; if (err.code === 404 && this.finalState === _states.FinalState.OfflineLike) { this.pusher.offline(this.ref.name); this.done(); return; } } this.timer = setTimeout(() => this.pollOnce(iter + 1), sleepTime); }); } /** * calculate the polling ladder * */ static calculateLadder(initial) { // final polling rate (do not increase the interval beyond this!) let finalPolling = 5000; try { finalPolling = require('@kui-shell/client/config.d/limits.json').tablePollingInterval; } catch (err) { debug('using default tablePollingInterval', err); } const ladder = [initial]; let current = initial; // increment the polling interval while (current < finalPolling) { if (current < 1000) { current = current + 250 < 1000 ? current + 250 : 1000; ladder.push(current); } else { ladder.push(current); current = current + 2000 < finalPolling ? current + 2000 : finalPolling; ladder.push(current); } } // debug('ladder', ladder) return ladder; } /** * Our impl of `Abortable` * */ abort() { if (this.timer) { clearTimeout(this.timer); } } } class StatusWatcher { // eslint-disable-next-line no-useless-constructor constructor(args, tab, resourcesToWaitFor, finalState, contextArgs, command) { this.args = args; this.tab = tab; this.resourcesToWaitFor = resourcesToWaitFor; this.finalState = finalState; this.contextArgs = contextArgs; this.command = command; this.pollers = []; this.ptyJob = []; } abortEventWatchers() { if (this.ptyJob) { this.ptyJob.forEach(job => job.abort()); this.ptyJob = []; } } /** * Our impl of `Abortable` for use by the table view * */ abort() { this.pollers.forEach(poller => { if (poller) { // be careful: the done() method below may nullify // this.pollers[] entries poller.abort(); } }); if (this.ptyJob) { this.ptyJob.forEach(job => job.abort()); this.ptyJob = []; } } /** * Our impl of the `Watcher` API. This is the callback we will * receive from the table UI when it is ready for us to start * injecting updates to the table. * */ init(pusher) { let countdown = this.resourcesToWaitFor.length; const done = () => { if (--countdown === 0) { debug('all resources are ready'); pusher.done(); for (let idx = 0; idx < this.pollers.length; idx++) { this.pollers[idx] = undefined; } if (this.ptyJob) { this.ptyJob.forEach(job => job.abort()); this.ptyJob = []; } } }; this.resourcesToWaitFor.map((_, idx) => { const { kind, name, namespace } = _; const eventWatcher = new _getWatch.EventWatcher(this.args, this.command, kind, [name], namespace, true, pusher); eventWatcher.init(); this.ptyJob.push(eventWatcher); const row = this.initialBody[idx]; return new StatusPoller(this.tab, _, row, this.finalState, done, pusher, this.contextArgs, this.command); }).forEach(_ => { this.pollers.push(_); }); } /** * We only display a NAMESPACE column if at least one of the * resources as a non-default namespace. * */ nsAttr(ns, anyNonDefaultNamespaces) { return !anyNonDefaultNamespaces ? [] : [{ key: 'NAMESPACE', value: ns }]; } /** * Formulate an initial response for the REPL * */ initialTable() { const dryRun = (0, _options.isDryRun)(this.args); const anyNonDefaultNamespaces = this.resourcesToWaitFor.some(({ namespace }) => namespace !== 'default'); this.initialBody = this.resourcesToWaitFor.map(ref => { const { group = '', version = '', kind, name, namespace } = ref; return { name, onclick: `${this.command} get ${(0, _fqn.fqnOfRef)(ref)} -o yaml`, onclickIdempotent: true, attributes: this.nsAttr(namespace, anyNonDefaultNamespaces).concat([{ key: 'KIND', value: kind + (group.length > 0 ? `.${version}.${group}` : ''), outerCSS: '', css: '' }, { key: 'STATUS', tag: 'badge', value: dryRun ? strings('Dry Run') : strings('Pending'), outerCSS: '', css: _pluginKubectlCore.TrafficLight.Yellow } // { key: 'MESSAGE', value: '', outerCSS: 'hide-with-sidecar' } ]) }; }); const initialHeader = { key: 'NAME', name: 'Name', attributes: this.initialBody[0].attributes.map(({ key, outerCSS }) => ({ key, value: (0, _formatTable.initialCapital)(key), outerCSS })) }; return { header: initialHeader, body: this.initialBody, watch: dryRun ? undefined : this }; } } const doStatus = (args, verb, command, initialCrudResponse, finalState, statusArgs, isWatchRequest = true) => __awaiter(void 0, void 0, void 0, function* () { const namespace = yield (0, _options.getNamespace)(args); const file = (0, _options.fileOf)(args); const kusto = (0, _options.kustomizeOf)(args); try { const { resourcesToWaitFor, kuiSourceRef } = file ? yield getResourcesReferencedByFile(file, args, namespace) : kusto ? yield getResourcesReferencedByKustomize(kusto, args, namespace) : yield getResourcesReferencedByCommandLine(verb, statusArgs || args.argvNoOptions.slice(args.argvNoOptions.indexOf(verb) + 1), namespace, finalState); debug('resourcesToWaitFor', verb, resourcesToWaitFor); if (finalState === _states.FinalState.OnlineLike && resourcesToWaitFor.every(_jobs.isDashboardableJob)) { return args.REPL.qexec(`kubectl dashboard jobs ${resourcesToWaitFor.map(_ => _.name).join(' ')} -n ${namespace} ${isWatchRequest ? ' -w' : ''}`); } // try handing off to direct/status try { const explainedResources = yield Promise.all(resourcesToWaitFor.map(_ => __awaiter(void 0, void 0, void 0, function* () { return Object.assign(_, { explainedKind: yield (0, _explain.getKindAndVersion)(command || 'kubectl', args, _.kind) }); }))); const groups = explainedResources.reduce((groups, resource) => { const group = groups.find(_ => _.namespace === resource.namespace && _.explainedKind.kind === resource.explainedKind.kind && _.explainedKind.version === resource.explainedKind.version); if (!group) { groups.push({ names: [resource.name], namespace: resource.namespace, explainedKind: resource.explainedKind }); } else if (!group.names.includes(resource.name)) { // potentially inefficient group.names.push(resource.name); } return groups; }, []); const everyGroupHasKind = groups.every(({ explainedKind }) => explainedKind && explainedKind.kind); if (everyGroupHasKind) { const response = yield (0, _status.default)(args, groups, finalState, command, file, isWatchRequest); if (response) { // then direct/status obliged! debug('using direct/status response'); if ((0, _core.isTable)(response)) { if (verb !== 'delete') { return Object.assign(response, { kuiSourceRef }); } else { // no source refs for deletes return response; } } else { return response.join('\n'); } } } } catch (err) { console.error('Error with direct/status. Falling back on polling implementation.', err); } // if we got here, then direct/status either failed, or refused to // handle this use case; fall back to the old polling impl debug('backup plan: using old status poller'); if (isWatchRequest) { return Object.assign(new StatusWatcher(args, args.tab, resourcesToWaitFor, finalState, `-n ${namespace} ${(0, _options.getContextForArgv)(args)}`, command).initialTable(), { kuiSourceRef: verb !== 'delete' ? kuiSourceRef : undefined } // see: for now no source refs, above ); } } catch (err) { console.error('error constructing StatusWatcher', err); // the text that the create or delete emitted, i.e. the command that // initiated this status request if (isWatchRequest) { return initialCrudResponse; } } }); exports.doStatus = doStatus;