diff --git a/packages/test/src/api/table.ts b/packages/test/src/api/table.ts index a23d9b65600..1998e2f2ae3 100644 --- a/packages/test/src/api/table.ts +++ b/packages/test/src/api/table.ts @@ -208,7 +208,7 @@ export class TestTable { selector: Selectors.TABLE_HEADER_CELL(expectTable.header.name) }) ) - .catch(Common.oops(ctx))) + .catch(Common.oops(ctx, true))) if (validation) { if (validation.cells) { @@ -251,7 +251,8 @@ export class TestTable { const cell = await ctx.app.client.$( `${Selectors.OUTPUT_N(self.cmdIdx)} ${Selectors.TABLE_CELL(row.name, expectTable.header.name)}` ) - await cell.waitForExist() + await cell.waitForExist({ timeout: 10000 }) + await cell.scrollIntoView() await cell.click() await CLI.expectPriorInput(prompt, command)(ctx.app) } catch (err) { diff --git a/plugins/plugin-bash-like/fs/src/lib/glob.ts b/plugins/plugin-bash-like/fs/src/lib/glob.ts index 086ccf18457..b1da4577cab 100644 --- a/plugins/plugin-bash-like/fs/src/lib/glob.ts +++ b/plugins/plugin-bash-like/fs/src/lib/glob.ts @@ -94,7 +94,7 @@ async function isDir(filepath: string): Promise { return new Promise((resolve, reject) => { stat(filepath, (err, stats) => { if (err) { - if (err.code === 'ENOENT') { + if (err.code === 'ENOENT' || err.code === 'ENOTDIR') { resolve(undefined) } else { reject(err) @@ -168,14 +168,16 @@ export async function kuiglob({ const globbedEntries = toGlob.length === 0 && inputs.length > 0 ? [] - : ((await globby(toGlob.length === 0 ? '*' : toGlob, { - followSymbolicLinks: false, + : (((await globby(toGlob.length === 0 ? ['*'] : toGlob, { onlyFiles: false, + suppressErrors: true, + expandDirectories: false, // see https://github.com/sindresorhus/globby/issues/166 + followSymbolicLinks: false, dot: parsedOptions.a || parsedOptions.all, stats: needStats, objectMode: !needStats, cwd: isHeadless() ? process.cwd() : tab.state.cwd - })) as RawGlobStats[]) + })) as any) as RawGlobStats[]) // ^^^^^^ re: type conversion; globby type declaration issue #139 // handle -d; fast-glob doesn't seem to handle this very well on its diff --git a/plugins/plugin-client-common/web/scss/components/Table/badges.scss b/plugins/plugin-client-common/web/scss/components/Table/badges.scss index 239764f653d..8874c0c9172 100644 --- a/plugins/plugin-client-common/web/scss/components/Table/badges.scss +++ b/plugins/plugin-client-common/web/scss/components/Table/badges.scss @@ -169,8 +169,8 @@ $grid-cell-size: 1.25rem; } &.red-background { - /* if it's red, then blink around 6 times */ - animation: var(--animation-medium-repeating-pulse); + /* if it's red, then blink around 3 times */ + animation: var(--animation-short-repeating-pulse); } } } diff --git a/plugins/plugin-ibmcloud/plugin/src/controller/available.ts b/plugins/plugin-ibmcloud/plugin/src/controller/available.ts index 03b892bc581..9f5da0e112a 100644 --- a/plugins/plugin-ibmcloud/plugin/src/controller/available.ts +++ b/plugins/plugin-ibmcloud/plugin/src/controller/available.ts @@ -30,7 +30,7 @@ export default async function getAvailablePlugins( tab: Tab, url = defaultURL ): Promise<{ plugins: AvailablePluginRaw[] }> { - return JSON.parse((await fetchFileString(tab.REPL, `${url}/plugins`))[0]) + return JSON.parse((await fetchFileString(tab.REPL, `${url}/plugins`))[0] || '{ "plugins": [] }') } /** diff --git a/plugins/plugin-kubectl-flow-views/tekton/src/lib/read.ts b/plugins/plugin-kubectl-flow-views/tekton/src/lib/read.ts index 23b6d9b56f3..9240712b96f 100644 --- a/plugins/plugin-kubectl-flow-views/tekton/src/lib/read.ts +++ b/plugins/plugin-kubectl-flow-views/tekton/src/lib/read.ts @@ -38,7 +38,7 @@ export const parse = async (raw: string | PromiseLike): Promise => { const data = await fetchFileString(tab.REPL, filepath) - if (data.length === 1) { + if (data.length === 1 && data[0]) { return data[0] } else { throw new Error(`Failed to fetch ${filepath}`) diff --git a/plugins/plugin-kubectl/src/controller/client/direct/404.ts b/plugins/plugin-kubectl/src/controller/client/direct/404.ts new file mode 100644 index 00000000000..cfef8696e63 --- /dev/null +++ b/plugins/plugin-kubectl/src/controller/client/direct/404.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 IBM Corporation + * + * 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. + */ + +import { Row, Table } from '@kui-shell/core' + +import { rowWith, standardStatusHeader } from './unify' +import TrafficLight from '../../../lib/model/traffic-light' + +/** + * @return a Row for the given name in `names` with an Offline status. + * + */ +function fabricate404Row(name: string, kind: string): Row { + return rowWith(name, kind, 'Offline', TrafficLight.Red) +} + +/** + * @return a Table with one row per given name in `names`, each row + * with an Offline status. + * + */ +export default function fabricate404Table(names: string[], kind: string): Table { + return { + header: standardStatusHeader, + body: names.map(name => fabricate404Row(name, kind)) + } +} diff --git a/plugins/plugin-kubectl/src/controller/client/direct/create.ts b/plugins/plugin-kubectl/src/controller/client/direct/create.ts index abd8426f98a..6b4fbdc74b2 100644 --- a/plugins/plugin-kubectl/src/controller/client/direct/create.ts +++ b/plugins/plugin-kubectl/src/controller/client/direct/create.ts @@ -98,7 +98,9 @@ export default async function createDirect( ] const watchPart = await status(args, groups, FinalState.OnlineLike) - return withErrors(watchPart, errors) + if (watchPart) { + return withErrors(watchPart, errors) + } } } } diff --git a/plugins/plugin-kubectl/src/controller/client/direct/status.ts b/plugins/plugin-kubectl/src/controller/client/direct/status.ts index 09b8ac8d356..bb3e5333491 100644 --- a/plugins/plugin-kubectl/src/controller/client/direct/status.ts +++ b/plugins/plugin-kubectl/src/controller/client/direct/status.ts @@ -15,10 +15,12 @@ */ import Debug from 'debug' -import { Abortable, Arguments, CodedError, Row, Table, Watchable, Watcher, WatchPusher } from '@kui-shell/core' +import { Abortable, Arguments, CodedError, Row, Table, Watchable, Watcher, WatchPusher, flatten } from '@kui-shell/core' import { getTable } from './get' +import fabricate404Table from './404' import URLFormatter, { urlFormatterFor } from './url' +import { unifyHeaders, unifyRow, unifyRows } from './unify' import makeWatchable, { DirectWatcher, SingleKindDirectWatcher } from './watch' import { Explained } from '../../kubectl/explain' @@ -26,7 +28,6 @@ import { KubeOptions } from '../../kubectl/options' import { isResourceReady } from '../../kubectl/status' import { FinalState } from '../../../lib/model/states' -import TrafficLight from '../../../lib/model/traffic-light' import { getCommandFromArgs } from '../../../lib/util/util' const debug = Debug('plugin-kubectl/controller/client/direct/status') @@ -49,6 +50,9 @@ class MultiKindWatcher implements Abortable, Watcher { /** The current watchers, one per explainedKind */ private watchers: DirectWatcher[] + /** Number of sub-tables not done */ + private nNotDone = 0 + // eslint-disable-next-line no-useless-constructor public constructor( private readonly drilldownCommand: string, @@ -57,104 +61,126 @@ class MultiKindWatcher implements Abortable, Watcher { private readonly resourceVersion: Table['resourceVersion'][], private readonly formatUrl: URLFormatter[], private readonly finalState: FinalState, - private nNotReady: number[], // number of resources to wait on - private readonly monitorEvents = true + private readonly initialRowKeys: string[][], + private readonly nNotReady: number[], // number of resources to wait on + private readonly monitorEvents = false ) {} public abort() { debug('abort requested', this.watchers.length) - this.watchers.forEach(_ => _.abort()) + this.watchers.forEach(watcher => { + if (watcher) { + watcher.abort() + } + }) } public init(pusher: WatchPusher) { this.pusher = pusher this.watchers = this.resourceVersion.map((resourceVersion, idx) => { - const watcher = new SingleKindDirectWatcher( - this.drilldownCommand, - this.args, - this.kind[idx].kind, - resourceVersion, - this.formatUrl[idx], - this.finalState, - this.nNotReady[idx], - this.monitorEvents - ) - watcher.init(pusher) - return watcher - }) - } -} - -interface WithIndex { - idx: number - table: T -} - -function isStringWithIndex(response: WithIndex): response is WithIndex { - return typeof response.table === 'string' -} + if (!resourceVersion || this.nNotReady[idx] === 0) { + return undefined + } else { + const watcher = new SingleKindDirectWatcher( + this.drilldownCommand, + this.args, + this.kind[idx].kind, + resourceVersion, + this.formatUrl[idx], + this.finalState, + this.initialRowKeys[idx], + this.nNotReady[idx], + this.monitorEvents, + true // yes, make sure there is a status column + ) -function isTableWithIndex(response: WithIndex): response is WithIndex { - return !isStringWithIndex(response) -} + if (this.nNotReady[idx] > 0) { + this.nNotDone++ + } + return watcher + } + }) -function fabricate404Row(name: string): Row { - return { - name, - attributes: [ - { - key: 'Status', - value: 'Offline', - tag: 'badge', - css: TrafficLight.Red + this.watchers.forEach((watcher, idx) => { + if (watcher) { + watcher.init(this.myPusher(idx)) } - ] + }) } -} -function fabricate404Table(names: string[]) { - // return names - // .map(name => `${kindPart(explainedKind.version, explainedKind.kind)} "${name}" deleted`) - // .join('\n') - return { - header: { - name: 'Name', - attributes: [ - { - key: 'Status', - value: 'Status' + private myPusher(idx: number): WatchPusher { + const overrides: Pick = { + header: (header: Row) => { + this.pusher.header(unifyHeaders([header])) + }, + update: (row: Row, batch?: boolean, changed?: boolean) => { + debug('update of unified row', row.rowKey, this.kind[idx].kind, row.attributes[1].value) + this.pusher.update(unifyRow(row, this.kind[idx].kind), batch, changed) + }, + done: () => { + debug('one sub-table is done', this.kind[idx], this.nNotDone) + if (--this.nNotDone <= 0) { + debug('all sub-tables are done') + this.pusher.done() } - ] - }, - body: names.map(fabricate404Row) + } + } + return Object.assign({}, this.pusher, overrides) } } -/** for apply/delete use cases, the caller may identify a FinalState and a countdown goal */ +interface WithIndex { + idx: number + table: T +} + +/** + * If at least one resource is not in the given `finalState`, return a + * single unified `Table & Watchable` that will monitor the given + * `groups` of resources until they have all reached the given + * `FinalState`. + * + * If the desired final state is `OfflineLike`, and all of the + * resources are already offline, return a plain `Table` with all rows + * marked as Offline. + * + * If none of the groups is valid (e.g. by specifying a bogus Kind), + * return a `string[]` that conveys the error messages. + * + * Finally, this function may refuse to handle the request, in which case + * it will retur `void`. + * + */ export default async function watchMulti( args: Arguments, groups: Group[], finalState: FinalState, drilldownCommand = getCommandFromArgs(args) -) { - if (groups.length !== 1) { +): Promise { + if (groups.length === 0) { return } const myArgs = { REPL: args.REPL, execOptions: { type: args.execOptions.type }, parsedOptions: {} } - // const nResources = groups.reduce((N, group) => N + group.names.length, 0) - - const tables: WithIndex[] = await Promise.all( + const tables: WithIndex
[] = await Promise.all( groups.map(async (_, idx) => ({ idx, - table: await getTable(drilldownCommand, _.namespace, _.names, _.explainedKind, 'default', myArgs, true).catch( - (err: CodedError) => { + table: await getTable(drilldownCommand, _.namespace, _.names, _.explainedKind, 'default', myArgs, true) + .then(response => { + if (typeof response === 'string') { + // turn the string parts into 404 tables + return fabricate404Table(_.names, _.explainedKind.kind) + } else { + return response + } + }) + .catch((err: CodedError) => { if (err.code === 404) { // This means every single name in _.names is missing if (finalState === FinalState.OfflineLike) { // Then that's what we want! we are done! - return fabricate404Table(_.names) + return fabricate404Table(_.names, _.explainedKind.kind) } else { // Otherwise, we are waiting till they are online, so // return an empty table. This table will subsequently @@ -164,31 +190,18 @@ export default async function watchMulti( } as Table } } - } - ) + }) })) ) - const { stringParts, tableParts } = tables.reduce( - (pair, response) => { - if (isStringWithIndex(response)) { - pair.stringParts.push(response) - } else if (isTableWithIndex(response)) { - pair.tableParts.push(response) - } - return pair - }, - { stringParts: [] as WithIndex[], tableParts: [] as WithIndex
[] } - ) - - if (tableParts.length > 0) { - if (groups.length === 1 && tableParts.length === 1) { + if (tables.length > 0) { + if (groups.length === 1 && tables.length === 1) { // HOMOGENEOUS CASE - const nNotReady = countNotReady(tableParts[0].table, finalState) + const nNotReady = countNotReady(tables[0].table, finalState) if (nNotReady === 0) { // sub-case 1: nothing to watch, as everything is already "ready" debug('special case: single-group watching, all-ready all ready!', nNotReady, groups[0]) - return tableParts[0].table + return tables[0].table } else { // sub-case 2: a subset may be done, but we need to fire up a // watcher to monitor the rest @@ -197,9 +210,10 @@ export default async function watchMulti( drilldownCommand, myArgs, groups[0].explainedKind.kind, - tableParts[0].table, + tables[0].table, urlFormatterFor(groups[0].namespace, myArgs, groups[0].explainedKind), finalState, + tables[0].table.body.map(_ => _.rowKey), nNotReady, false, // no events true // yes, make sure there is a status column @@ -207,39 +221,37 @@ export default async function watchMulti( } } - // If we get here, then this impl does not yet handle this use - // case. It is up to the caller to figure out a backup plan. - return - // HETEROGENEOUS CASE // we need to assemble a unifiedTable facade - // NOT YET DONE. for now, we handle only the single-kind/homogeneous cases - // eslint-disable-next-line no-unreachable - const firstTableWithHeader = tableParts.find(_ => _.table.header) - // eslint-disable-next-line no-unreachable,@typescript-eslint/no-unused-vars const unifiedTable: Table & Watchable = { - header: firstTableWithHeader ? firstTableWithHeader.table.header : undefined, - body: [].concat(...tableParts.map(_ => _.table.body)) as Table['body'], + header: unifyHeaders([].concat(...tables.map(_ => _.table.header))), + body: unifyRows( + [].concat(...tables.map(_ => _.table.body)), + flatten( + groups.map(_ => + Array(_.names.length) + .fill(0) + .map(() => _.explainedKind.kind) + ) + ) + ), watch: new MultiKindWatcher( drilldownCommand, args, - tableParts.map(_ => groups[_.idx].explainedKind), - tableParts.map(_ => _.table.resourceVersion), + tables.map(_ => groups[_.idx].explainedKind), + tables.map(_ => _.table.resourceVersion), await Promise.all( - tableParts.map(_ => { + tables.map(_ => { const group = groups[_.idx] return urlFormatterFor(group.namespace, myArgs, group.explainedKind) }) ), finalState, - tableParts.map(_ => countNotReady(_.table, finalState)) + tables.map(_ => _.table.body.map(_ => _.rowKey)), + tables.map(_ => countNotReady(_.table, finalState)) ) } - // eslint-disable-next-line no-unreachable - throw new Error('Unsupported use case') - } else if (stringParts.length > 0) { - // all strings, which will be the messages from the apiServer, e.g. conveying the state "this resource that you asked to watch until it was deleted... well it is already deleted" - return stringParts.map(_ => _.table) + return unifiedTable } else { throw new Error('nothing to watch') } diff --git a/plugins/plugin-kubectl/src/controller/client/direct/unify.ts b/plugins/plugin-kubectl/src/controller/client/direct/unify.ts new file mode 100644 index 00000000000..31afb491080 --- /dev/null +++ b/plugins/plugin-kubectl/src/controller/client/direct/unify.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2020 IBM Corporation + * + * 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. + */ + +import { Table, Row } from '@kui-shell/core' +import TrafficLight, { toTrafficLight } from '../../../lib/model/traffic-light' + +/** Do not i18n! */ +export const Kind = 'Kind' +export const Status = 'Status' + +export const standardStatusHeader: Table['header'] = { + name: 'Name', + attributes: [ + { + key: Kind, + value: Kind + }, + { + key: Status, + value: Status + } + ] +} + +export function rowWith( + name: string, + kind: string, + status: string, + trafficLight: TrafficLight, + originalRow?: Row +): Row { + const overlay: Row = { + name, + attributes: [ + { + key: Kind, + value: kind + }, + { + key: Status, + value: status, + tag: 'badge', + css: trafficLight + } + ] + } + + if (originalRow) { + return Object.assign({}, originalRow, overlay) + } else { + return overlay + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function unifyHeaders(headers: Table['header'][]): Table['header'] { + return standardStatusHeader +} + +export function unifyRow(row: Row, kind: string): Row { + const name = row.name + + const badgeColumnIdx = row.attributes.findIndex(_ => _.tag === 'badge') + const status = badgeColumnIdx >= 0 ? row.attributes[badgeColumnIdx].value : 'Unknown' + const trafficLight = badgeColumnIdx >= 0 ? toTrafficLight(row.attributes[badgeColumnIdx].css) : TrafficLight.Gray + + return rowWith(name, kind, status, trafficLight, row) +} + +export function unifyRows(rows: Table['body'], kinds: string | string[]): Table['body'] { + return rows.map((row, idx) => { + const kind = Array.isArray(kinds) ? kinds[idx] : kinds + return unifyRow(row, kind) + }) +} + +export function unifyTable(table: Table, kind: string): Table { + const overlay: Table = { + header: unifyHeaders([table.header]), + body: unifyRows(table.body, kind) + } + return Object.assign({}, table, overlay) +} diff --git a/plugins/plugin-kubectl/src/controller/client/direct/watch.ts b/plugins/plugin-kubectl/src/controller/client/direct/watch.ts index 689f5fc3a02..e35150ddd39 100644 --- a/plugins/plugin-kubectl/src/controller/client/direct/watch.ts +++ b/plugins/plugin-kubectl/src/controller/client/direct/watch.ts @@ -38,13 +38,15 @@ interface WatchUpdate { object: MetaTable } -export abstract class DirectWatcher { +export abstract class DirectWatcher implements Watcher, Abortable, Omit { /** The table push API */ protected pusher: WatchPusher /** The current stream jobs. These will be aborted/flow-controlled as directed by the associated view. */ protected jobs: (Abortable & FlowControllable)[] = [] + abstract init(pusher: WatchPusher): void + /** This will be called by the view when it wants the underlying streamer to resume flowing updates */ public xon() { this.jobs.forEach(job => job.xon()) @@ -99,13 +101,19 @@ export class SingleKindDirectWatcher extends DirectWatcher implements Abortable, private readonly resourceVersion: Table['resourceVersion'], private readonly formatUrl: URLFormatter, private readonly finalState?: FinalState, + initialRowKeys?: string[], private nNotReady?: number, // number of resources to wait on private readonly monitorEvents = true, private readonly needsStatusColumn = false ) { super() if (finalState) { - this.readyDebouncer = {} + this.readyDebouncer = initialRowKeys + ? initialRowKeys.reduce((M, rowKey) => { + M[rowKey] = false + return M + }, {} as Record) + : undefined } } @@ -275,6 +283,12 @@ export class SingleKindDirectWatcher extends DirectWatcher implements Abortable, } table.body.forEach((row, idx) => { + const rowNeverSeenBefore = this.readyDebouncer && this.readyDebouncer[row.rowKey] === undefined + if (rowNeverSeenBefore) { + debug('dropping untracked row', row.rowKey) + return + } + if (update.type === 'ADDED' || update.type === 'MODIFIED') { this.pusher.update(row, true) } else { @@ -296,14 +310,22 @@ export class SingleKindDirectWatcher extends DirectWatcher implements Abortable, */ private checkIfReady(row: Row, idx: number, update: WatchUpdate) { if (this.finalState && this.nNotReady > 0) { - debug('checking if resource is ready', this.finalState, this.nNotReady, row, update.type, update.object.rows[idx]) - const isReady = (update.type === 'ADDED' && this.finalState === FinalState.OnlineLike) || (update.type === 'DELETED' && this.finalState === FinalState.OfflineLike) || (update.type === 'MODIFIED' && isResourceReady(row, this.finalState)) - if (isReady && !this.readyDebouncer[row.rowKey]) { + debug( + 'checking if resource is ready', + isReady, + this.finalState, + this.nNotReady, + row, + update.type, + update.object.rows[idx] + ) + + if (this.readyDebouncer && isReady && !this.readyDebouncer[row.rowKey]) { debug('A resource is in its final state', row.name, this.nNotReady) this.readyDebouncer[row.rowKey] = true if (--this.nNotReady <= 0) { @@ -329,6 +351,7 @@ export default async function makeWatchable( table: Table, formatUrl: URLFormatter, finalState?: FinalState, + initialRowKeys?: string[], nNotReady?: number, monitorEvents = true, needsStatusColumn = false @@ -347,6 +370,7 @@ export default async function makeWatchable( table.resourceVersion, formatUrl, finalState, + initialRowKeys, nNotReady, monitorEvents, needsStatusColumn diff --git a/plugins/plugin-kubectl/src/controller/fetch-file.ts b/plugins/plugin-kubectl/src/controller/fetch-file.ts index 4fb55992b0c..f7ec9fdac73 100644 --- a/plugins/plugin-kubectl/src/controller/fetch-file.ts +++ b/plugins/plugin-kubectl/src/controller/fetch-file.ts @@ -56,11 +56,11 @@ async function fetchKustomizeString(repl: REPL, uri: string): Promise<{ data: st const [isFile1, isFile2, isFile3] = await Promise.all([isFile(k1), isFile(k2), isFile(k3)]) const dir = uri // if we are here, then `uri` is a directory if (isFile1) { - return { data: (await fetchFileString(repl, k1))[0], dir } + return { data: (await fetchFileString(repl, k1))[0] || '', dir } } else if (isFile2) { - return { data: (await fetchFileString(repl, k2))[0], dir } + return { data: (await fetchFileString(repl, k2))[0] || '', dir } } else if (isFile3) { - return { data: (await fetchFileString(repl, k3))[0], dir } + return { data: (await fetchFileString(repl, k3))[0] || '', dir } } } } diff --git a/plugins/plugin-kubectl/src/controller/kubectl/source.ts b/plugins/plugin-kubectl/src/controller/kubectl/source.ts index 0f2ac3237ed..7cfac0e0840 100644 --- a/plugins/plugin-kubectl/src/controller/kubectl/source.ts +++ b/plugins/plugin-kubectl/src/controller/kubectl/source.ts @@ -31,9 +31,11 @@ export default async function withSourceRefs( if (filepath && isTableRequest(args)) { try { - const data = (await fetchFile(args.REPL, filepath))[0] - return { - templates: [{ filepath, data, isFor, kind: 'source', contentType: 'yaml' }] + const data = (await fetchFile(args.REPL, filepath)).filter(_ => _)[0] + if (data) { + return { + templates: [{ filepath, data, isFor, kind: 'source', contentType: 'yaml' }] + } } } catch (err) { console.error('Error fetching source ref', err) diff --git a/plugins/plugin-kubectl/src/controller/kubectl/status.ts b/plugins/plugin-kubectl/src/controller/kubectl/status.ts index fd9964295ed..4003bd8c31c 100644 --- a/plugins/plugin-kubectl/src/controller/kubectl/status.ts +++ b/plugins/plugin-kubectl/src/controller/kubectl/status.ts @@ -15,6 +15,7 @@ */ import Debug from 'debug' +import { DirEntry } from '@kui-shell/plugin-bash-like/fs' import { Abortable, Arguments, @@ -27,6 +28,8 @@ import { Watchable, Watcher, WatchPusher, + encodeComponent, + flatten, isTable, i18n } from '@kui-shell/core' @@ -132,11 +135,16 @@ const usage = (command: string) => ({ * */ async function getResourcesReferencedByFile(file: string, args: Arguments): Promise { - const [{ safeLoadAll }, raw] = await Promise.all([import('js-yaml'), fetchFile(args.REPL, file)]) - - const namespaceFromCommandLine = await getNamespace(args) + const files = ( + await args.REPL.rexec(`vfs ls ${encodeComponent(file)} ${encodeComponent(file)}/**/*.{yaml,yml}`) + ).content + const [{ safeLoadAll }, namespaceFromCommandLine, raw] = await Promise.all([ + import('js-yaml'), + getNamespace(args), + fetchFile(args.REPL, files.length === 0 ? file : files.map(_ => _.path).join(',')) + ]) - const models: KubeResource[] = safeLoadAll(raw[0]) + const models = flatten(raw.map(_ => safeLoadAll(_) as KubeResource[])) return models .filter(_ => _.metadata) .map(({ apiVersion, kind, metadata: { name, namespace = namespaceFromCommandLine } }) => { @@ -595,6 +603,7 @@ const doStatus = (command: string) => async (args: Arguments) } return groups }, [] as Group[]) + const response = await statusDirect(args, groups, finalState, commandArg) if (response) { // then direct/status obliged! diff --git a/plugins/plugin-kubectl/src/lib/model/traffic-light.ts b/plugins/plugin-kubectl/src/lib/model/traffic-light.ts index 7f26af583b0..221968a32d8 100644 --- a/plugins/plugin-kubectl/src/lib/model/traffic-light.ts +++ b/plugins/plugin-kubectl/src/lib/model/traffic-light.ts @@ -26,4 +26,18 @@ enum TrafficLight { Green = 'green-background' } +export function toTrafficLight(str: string): TrafficLight { + if (str.indexOf(TrafficLight.Red) >= 0) { + return TrafficLight.Red + } else if (str.indexOf(TrafficLight.Yellow) >= 0) { + return TrafficLight.Yellow + } else if (str.indexOf(TrafficLight.Blue) >= 0) { + return TrafficLight.Blue + } else if (str.indexOf(TrafficLight.Green) >= 0) { + return TrafficLight.Green + } else { + return TrafficLight.Gray + } +} + export default TrafficLight diff --git a/plugins/plugin-kubectl/src/lib/util/fetch-file.ts b/plugins/plugin-kubectl/src/lib/util/fetch-file.ts index 876107efdca..9ca5f8ffb6c 100644 --- a/plugins/plugin-kubectl/src/lib/util/fetch-file.ts +++ b/plugins/plugin-kubectl/src/lib/util/fetch-file.ts @@ -234,11 +234,15 @@ export async function fetchFile( } /** same as fetchFile, but returning a string rather than a Buffer */ -export async function fetchFileString(repl: REPL, url: string, headers?: Record): Promise { +export async function fetchFileString( + repl: REPL, + url: string, + headers?: Record +): Promise<(void | string)[]> { const files = await fetchFile(repl, url, { headers }) return files.map(_ => { try { - return _.toString() + return _ ? _.toString() : undefined } catch (err) { console.error('Unable to convert fetched file to string', err, _) return ''