@kui-shell/plugin-kubectl
Version:
Kubernetes visualization plugin for kubernetes
530 lines (528 loc) • 17.7 kB
JavaScript
"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;