Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit 9a2bc74

Browse files
committed
feat: Extend kubectl optimizations to table watching
Fixes #6449
1 parent 0a575f2 commit 9a2bc74

File tree

12 files changed

+522
-37
lines changed

12 files changed

+522
-37
lines changed

packages/core/src/webapp/models/table.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ export class Table<RowType extends Row = Row> {
135135
/** Markdown cells? */
136136
markdown?: boolean
137137

138+
/** This field helps with watching/paginating */
139+
resourceVersion?: number | string
140+
138141
/** Default presentation? */
139142
defaultPresentation?: PresentationStyle
140143

plugins/plugin-bash-like/src/pty/server.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { IncomingMessage } from 'http'
3030
import { Channel } from './channel'
3131
import { StdioChannelKuiSide } from './stdio-channel'
3232

33-
import { CodedError, ExecOptions, Registrar, expandHomeDir } from '@kui-shell/core'
33+
import { Abortable, CodedError, ExecOptions, FlowControllable, Registrar, expandHomeDir } from '@kui-shell/core'
3434

3535
const debug = Debug('plugins/bash-like/pty/server')
3636

@@ -237,6 +237,9 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
237237
// @see https://github.com/microsoft/TypeScript/issues/22445
238238
const shells: Record<string, Promise<import('node-pty-prebuilt-multiarch').IPty>> = {}
239239

240+
/** Active streaming jobs */
241+
const jobs: Record<string, Abortable & FlowControllable> = {}
242+
240243
// For all websocket data send it to the shell
241244
ws.on('message', async (data: string) => {
242245
try {
@@ -251,6 +254,9 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
251254
rows?: number
252255
cols?: number
253256

257+
/** for type:request, execute a command, and stream back the output; the default behavior is request/respo;nse */
258+
stream?: boolean
259+
254260
uuid?: string // for request-response
255261
execOptions?: ExecOptions
256262
} = JSON.parse(data)
@@ -262,6 +268,11 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
262268
const RESUME = '\x11' // this is XON
263269
shell.write(RESUME)
264270
}
271+
272+
const job = msg.uuid && jobs[msg.uuid]
273+
if (job) {
274+
job.xon()
275+
}
265276
break
266277
}
267278

@@ -271,6 +282,11 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
271282
const PAUSE = '\x13' // this is XOFF
272283
shell.write(PAUSE)
273284
}
285+
286+
const job = msg.uuid && jobs[msg.uuid]
287+
if (job) {
288+
job.xoff()
289+
}
274290
break
275291
}
276292

@@ -280,6 +296,12 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
280296
shell.kill(msg.signal || 'SIGHUP')
281297
return exitNow(msg.exitCode || 0)
282298
}
299+
300+
const job = msg.uuid && jobs[msg.uuid]
301+
debug(`kill requested. hasShell=${shell !== undefined} hasJob=${job !== undefined}`)
302+
if (job) {
303+
job.abort()
304+
}
283305
break
284306
}
285307

@@ -297,8 +319,25 @@ export const onConnection = (exitNow: ExitHandler, uid?: number, gid?: number) =
297319
// ws.send(`___kui_exit___ ${msg.uuid}`)
298320
}
299321

322+
const execOptions = Object.assign({}, msg.execOptions, { rethrowErrors: true })
323+
if (msg.stream) {
324+
// then we will stream back the output; otherwise, we will use a request/response style of execution
325+
debug('initializing streaming exec')
326+
execOptions.onInit = (job: Abortable & FlowControllable) => {
327+
jobs[msg.uuid] = job
328+
return chunk =>
329+
ws.send(
330+
JSON.stringify({
331+
type: 'chunk',
332+
uuid: msg.uuid,
333+
chunk
334+
})
335+
)
336+
}
337+
}
338+
300339
try {
301-
const response = await exec(msg.cmdline, Object.assign({}, msg.execOptions, { rethrowErrors: true }))
340+
const response = await exec(msg.cmdline, execOptions)
302341
debug('got response')
303342
terminate(
304343
JSON.stringify({

plugins/plugin-client-common/web/scss/components/Table/Events.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ $inter-message-spacing: 0.1875rem;
7171

7272
a {
7373
color: var(--color-base0F);
74-
min-width: 4rem;
74+
font-size: 0.75rem;
7575
display: inline-flex;
7676
}
7777
}

plugins/plugin-kubectl/src/controller/client/direct/errors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
import { CodedError, isCodedError, REPL } from '@kui-shell/core'
1818

19+
import { URLFormatter } from './get'
1920
import { Status, isStatus } from '../../../lib/model/resource'
2021
import { fetchFile, FetchedFile, isReturnedError } from '../../../lib/util/fetch-file'
2122

22-
type URL = (includeKind: boolean) => string
2323
type WithErrors = { errors: CodedError[]; ok: (string | Buffer | object)[] }
2424

2525
/** See if the given error message is a Kubernetes Status object */
@@ -38,7 +38,7 @@ function tryParseAsStatus(message: string): string | Status {
3838

3939
export default async function handleErrors(
4040
responses: FetchedFile[],
41-
formatUrl: URL,
41+
formatUrl: URLFormatter,
4242
kind: string,
4343
repl: REPL
4444
): Promise<WithErrors> {

plugins/plugin-kubectl/src/controller/client/direct/get.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { Arguments, CodedError, ExecType } from '@kui-shell/core'
1818

19+
import makeWatchable from './watch'
1920
import { Explained } from '../../kubectl/explain'
2021
import { fetchFile } from '../../../lib/util/fetch-file'
2122
import { toKuiTable, withNotFound } from '../../../lib/view/formatTable'
@@ -33,12 +34,14 @@ import {
3334
import handleErrors from './errors'
3435
import { isStatus, KubeItems, MetaTable } from '../../../lib/model/resource'
3536

36-
export default async function getDirect(args: Arguments<KubeOptions>, _kind: Promise<Explained>) {
37-
const format = formatOf(args)
38-
const kindIdx = args.argvNoOptions.indexOf('get') + 1
39-
const { kind, version, isClusterScoped } = _kind
40-
? await _kind
41-
: { kind: undefined, version: undefined, isClusterScoped: false }
37+
export type URLFormatter = (includeKind?: boolean, includeQueries?: boolean, name?: string) => string
38+
39+
export const headersForTableRequest = { accept: 'application/json;as=Table;g=meta.k8s.io;v=v1' }
40+
41+
export async function urlFormatterFor(
42+
args: Arguments<KubeOptions>,
43+
{ kind, version, isClusterScoped }: Explained
44+
): Promise<URLFormatter> {
4245
const namespace = await getNamespace(args)
4346

4447
const kindOnPath = `/${encodeURIComponent(kind.toLowerCase() + (/s$/.test(kind) ? '' : 's'))}`
@@ -76,24 +79,27 @@ export default async function getDirect(args: Arguments<KubeOptions>, _kind: Pro
7679
}
7780

7881
// format a url
79-
const formatUrl = (includeKind = false, includeQueries = false, name?: string) =>
82+
return (includeKind = false, includeQueries = false, name?: string) =>
8083
`kubernetes:///${apiOnPath}${namespaceOnPath}${!includeKind ? '' : kindOnPath}${
8184
!name ? '' : `/${encodeURIComponent(name)}`
8285
}${!includeQueries || queries.length === 0 ? '' : '?' + queries.join('&')}`
86+
}
87+
88+
export default async function getDirect(args: Arguments<KubeOptions>, _kind: Promise<Explained>) {
89+
const explainedKind = _kind ? await _kind : { kind: undefined, version: undefined, isClusterScoped: false }
90+
const { kind } = explainedKind
91+
const formatUrl = await urlFormatterFor(args, explainedKind)
8392

93+
const format = formatOf(args)
94+
const kindIdx = args.argvNoOptions.indexOf('get') + 1
8495
const names = args.argvNoOptions.slice(kindIdx + 1)
8596
const urls = names.length === 0 ? formatUrl(true, true) : names.map(formatUrl.bind(undefined, true, true)).join(',')
8697

87-
if (isTableRequest(args) && !isWatchRequest(args)) {
98+
if (isTableRequest(args)) {
8899
const fmt = format || 'default'
89100
if (fmt === 'wide' || fmt === 'default') {
90101
// first, fetch the data
91-
const responses = await fetchFile(
92-
args.REPL,
93-
urls,
94-
{ accept: 'application/json;as=Table;g=meta.k8s.io;v=v1' },
95-
true
96-
)
102+
const responses = await fetchFile(args.REPL, urls, headersForTableRequest, true)
97103

98104
// then dissect it into errors and non-errors
99105
const { errors, ok } = await handleErrors(responses, formatUrl, kind, args.REPL)
@@ -116,12 +122,12 @@ export default async function getDirect(args: Arguments<KubeOptions>, _kind: Pro
116122
}, undefined)
117123

118124
if (args.execOptions.type === ExecType.TopLevel && metaTable.rows.length === 0 && !isWatchRequest(args)) {
119-
return `No resources found in **${namespace}** namespace.`
125+
return `No resources found in **${await getNamespace(args)}** namespace.`
120126
} else {
121127
try {
122128
// withNotFound will add error rows to the table for each error
123129
const table = withNotFound(await toKuiTable(metaTable, kind, args), errors.map(_ => _.message).join('\n'))
124-
return table
130+
return !isWatchRequest(args) ? table : makeWatchable(args, kind, table, formatUrl)
125131
} catch (err) {
126132
console.error('error formatting table', err)
127133
throw new Error('Internal Error')

0 commit comments

Comments
 (0)