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

Commit 8bf8ebe

Browse files
committed
fix: kubectl kustomize can have bogus output
This also fixes a long-standing issue in the test util `expectError`. It was doing incomplete validation of the error code. Fixes #6114
1 parent fc0873b commit 8bf8ebe

File tree

11 files changed

+69
-136
lines changed

11 files changed

+69
-136
lines changed

packages/test/src/api/repl-expect.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,23 +171,30 @@ export const ok = async (res: AppAndCount) =>
171171
.then(elts => assert.strictEqual(elts.length, 0))
172172
.then(() => res)
173173

174-
export const error = (statusCode: number | string, expect?: string) => async (res: AppAndCount) =>
175-
expectOK(res, {
176-
selfSelector: `.oops[data-status-code="${statusCode || 0}"]`,
177-
expectError: true,
178-
expect: expect
179-
}).then(() => res.app)
180-
181-
export const errorWithPassthrough = (statusCode: number | string, expect?: string) => async (
182-
res: AppAndCount
183-
): Promise<number> =>
184-
expectOK(res, {
185-
selector: `.oops[data-status-code="${statusCode || 0}"]`,
186-
expectError: true,
187-
expect: expect,
188-
passthrough: true
174+
export const error = (statusCode: number | string, expect?: string) => async (res: AppAndCount) => {
175+
await expectOK(res, {
176+
expectError: true
189177
})
190178

179+
if (expect) {
180+
const selfSelector = `.oops[data-status-code="${statusCode || 0}"]`
181+
const selector = `${Selectors.OUTPUT_N(res.count, res.splitIndex || 1)}${selfSelector || ''}`
182+
let idx = 0
183+
await res.app.client.waitUntil(
184+
async () => {
185+
const actualText = await res.app.client.$(selector).then(_ => _.getText())
186+
if (++idx > 5) {
187+
console.error(`still waiting for actualText=${actualText} expectedText=${expect}`)
188+
}
189+
return actualText.indexOf(expect) >= 0
190+
},
191+
{ timeout: waitTimeout }
192+
)
193+
}
194+
195+
return res.app
196+
}
197+
191198
export const blankWithOpts = (opts = {}) => async (res: AppAndCount) =>
192199
expectOK(res, Object.assign({ selector: '', expectError: true }, opts))
193200

packages/test/src/api/util.ts

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -241,78 +241,6 @@ export const expectText = (app: Application, expectedText: string, exact = true)
241241
return app
242242
}
243243

244-
/**
245-
*
246-
* - Type the command
247-
* - Expect a command not found
248-
* - Expect the given list of available related commands
249-
* - Optionally click on a given "click" index of the available list
250-
* - If so, then either: expect the subsequent command output to have the given terminal breadcrumb in its usage message
251-
* or: expect the sidecar icon to be "sidecar"
252-
*
253-
*/
254-
export function expectSuggestionsFor(
255-
this: Common.ISuite,
256-
cmd: string,
257-
expectedAvailable: string[],
258-
{
259-
click = undefined,
260-
expectedBreadcrumb = undefined,
261-
sidecar: expectedIcon = undefined,
262-
expectedString = undefined
263-
}: { click?: number; expectedBreadcrumb?: string; sidecar?: string; expectedString?: string } = {}
264-
) {
265-
return CLI.command(cmd, this.app)
266-
.then(ReplExpect.errorWithPassthrough(404))
267-
.then(N => {
268-
const base = `${Selectors.OUTPUT_N(N)} .user-error-available-commands .log-line`
269-
const availableItems = `${base} .clickable`
270-
271-
return this.app.client
272-
.$(availableItems)
273-
.then(_ => _.getText())
274-
.then(expectArray(expectedAvailable, false, true))
275-
.then(async () => {
276-
if (click !== undefined) {
277-
// then click on the given index; note that nth-child is 1-indexed, hence the + 1 part
278-
const clickOn = `${base}:nth-child(${click + 1}) .clickable`
279-
280-
await this.app.client.$(clickOn).then(_ => _.click())
281-
282-
if (expectedBreadcrumb) {
283-
//
284-
// then expect the next command to have the given terminal breadcrumb
285-
//
286-
const breadcrumb = `${Selectors.OUTPUT_N(N + 1)} .bx--breadcrumb-item:last-child .bx--no-link`
287-
return this.app.client
288-
.$(breadcrumb)
289-
.then(_ => _.getText())
290-
.then(actualBreadcrumb => assert.strictEqual(actualBreadcrumb, expectedBreadcrumb))
291-
} else if (expectedIcon) {
292-
//
293-
// then wait for the sidecar to be open and showing the expected sidecar icon text
294-
//
295-
const icon = `${Selectors.SIDECAR(N)} .sidecar-header-icon-wrapper .sidecar-header-icon`
296-
return SidecarExpect.open({ app: this.app, count: N })
297-
.then(() => this.app.client.$(icon))
298-
.then(_ => _.getText())
299-
.then(actualIcon => actualIcon.toLowerCase())
300-
.then(actualIcon => assert.strictEqual(actualIcon, expectedIcon))
301-
} else if (expectedString) {
302-
//
303-
// then wait for the given command output
304-
//
305-
return this.app.client.waitUntil(async () => {
306-
const text = await this.app.client.$(Selectors.OUTPUT_N(N + 1)).then(_ => _.getText())
307-
return text === expectedString
308-
})
309-
}
310-
}
311-
})
312-
})
313-
.catch(Common.oops(this))
314-
}
315-
316244
/** @return the current number of tabs */
317245
export async function tabCount(app: Application): Promise<number> {
318246
const topTabs = await app.client.$$(Selectors.TOP_TAB)

plugins/plugin-client-common/src/components/Content/Table/kui2carbon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function kuiRow2carbonRow(headers: DataTableHeader[], justUpdated = false
8080

8181
if (!row.attributes) row.attributes = []
8282
row.attributes.forEach((attr, cidx) => {
83-
const kkey = attr.key || headers[cidx + 1] ? headers[cidx + 1].key : undefined
83+
const kkey = headers[cidx + 1] ? headers[cidx + 1].key : attr.key
8484
if (!attr.key && kkey) {
8585
attr.key = kkey
8686
}

plugins/plugin-core-support/src/test/core/command-not-found-suggestions.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Arguments, Table, isTable, is404or409 } from '@kui-shell/core'
2020
import { fetchFile } from '../../../lib/util/fetch-file'
2121
import { Explained, getKindAndVersion } from '../../kubectl/explain'
2222

23-
import { KubeOptions, fileOf, getLabel, getNamespace } from '../../kubectl/options'
23+
import { KubeOptions, getFileFromArgv, getLabel, getNamespace } from '../../kubectl/options'
2424

2525
import status from './status'
2626
import handleErrors from './errors'
@@ -48,7 +48,12 @@ export default async function createDirect(
4848
_kind?: Promise<Explained>
4949
) {
5050
// For now, we only handle create-by-name
51-
if (!fileOf(args) && !getLabel(args) && !args.parsedOptions['dry-run'] && !args.parsedOptions['field-selector']) {
51+
if (
52+
!getFileFromArgv(args) &&
53+
!getLabel(args) &&
54+
!args.parsedOptions['dry-run'] &&
55+
!args.parsedOptions['field-selector']
56+
) {
5257
const explainedKind = await (_kind ||
5358
getKindAndVersion(getCommandFromArgs(args), args, args.argvNoOptions[args.argvNoOptions.indexOf(verb) + 1]))
5459
const { kind, version } = explainedKind

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Arguments, is404 } from '@kui-shell/core'
2020
import { fetchFile } from '../../../lib/util/fetch-file'
2121
import { Explained, getKindAndVersion } from '../../kubectl/explain'
2222

23-
import { KubeOptions, fileOf, getLabel, getNamespace } from '../../kubectl/options'
23+
import { KubeOptions, getFileFromArgv, getLabel, getNamespace } from '../../kubectl/options'
2424

2525
import status from './status'
2626
import handleErrors from './errors'
@@ -33,7 +33,12 @@ const debug = Debug('plugin-kubectl/controller/client/direct/delete')
3333

3434
export default async function deleteDirect(args: Arguments<KubeOptions>, _kind?: Promise<Explained>) {
3535
// For now, we only handle delete-by-name
36-
if (!fileOf(args) && !getLabel(args) && !args.parsedOptions['dry-run'] && !args.parsedOptions['field-selector']) {
36+
if (
37+
!getFileFromArgv(args) &&
38+
!getLabel(args) &&
39+
!args.parsedOptions['dry-run'] &&
40+
!args.parsedOptions['field-selector']
41+
) {
3742
const explainedKind = await (_kind ||
3843
getKindAndVersion(getCommandFromArgs(args), args, args.argvNoOptions[args.argvNoOptions.indexOf('delete') + 1]))
3944
const { kind } = explainedKind

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ import { toKuiTable, withNotFound } from '../../../lib/view/formatTable'
2626
import { doStatus } from '../../kubectl/status'
2727
import {
2828
KubeOptions,
29-
fileOf,
3029
formatOf,
31-
kustomizeOf,
30+
getFileFromArgv,
3231
getNamespace,
3332
isEntityFormat,
3433
isTableRequest,
@@ -130,7 +129,7 @@ export async function get(
130129
format: string,
131130
args: Arguments<KubeOptions>
132131
) {
133-
const isFileRequest = fileOf(args) || kustomizeOf(args)
132+
const isFileRequest = getFileFromArgv(args)
134133
const isEntityAndNameFormat = isEntityFormat(format) || format === 'name'
135134

136135
/** 1. file request with table output, e.g. `kubectl get -f` and `kubectl get -k` */

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ export function urlFormatterFor(
4545
// e.g. "apis/apps/v1" for deployments
4646
const apiOnPath = apiOnPathFor(version)
4747

48+
if (!namespace) {
49+
console.error('Internal oddity with namespace, falling back on "default", and was given:', namespace)
50+
namespace = 'default'
51+
}
52+
4853
// a bit complex: "kubectl get ns", versus "kubectl get ns foo"
4954
// the "which" is "foo" in the second case
5055
const namespaceOnPath = isForAllNamespaces(parsedOptions)

plugins/plugin-kubectl/src/controller/kubectl/contexts.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export async function getAllContexts({ REPL }: { REPL: REPLType }): Promise<Kube
6666
return (await REPL.rexec<KubeContext[]>('contexts')).content
6767
}
6868

69-
export async function getCurrentContextName({ REPL }: { REPL: REPLType }) {
69+
export async function getCurrentContextName({ REPL }: { REPL: REPLType }): Promise<string> {
7070
const context = await REPL.qexec<string>('kubectl config current-context')
7171
return context ? context.trim() : context
7272
}
@@ -75,20 +75,30 @@ export async function getCurrentContextName({ REPL }: { REPL: REPLType }) {
7575
let currentDefaultNamespaceCache: string
7676
onKubectlConfigChangeEvents((type, namespace) => {
7777
if (type === 'SetNamespaceOrContext') {
78-
if (typeof namespace === 'string') {
78+
if (typeof namespace === 'string' && namespace.length > 0) {
7979
currentDefaultNamespaceCache = namespace
8080
} else {
8181
// invalidate cache
8282
currentDefaultNamespaceCache = undefined
8383
}
8484
}
8585
})
86-
export async function getCurrentDefaultNamespace({ REPL }: { REPL: REPLType }) {
86+
export async function getCurrentDefaultNamespace({ REPL }: { REPL: REPLType }): Promise<string> {
8787
if (currentDefaultNamespaceCache) {
8888
return currentDefaultNamespaceCache
8989
}
9090

91-
const ns = await REPL.qexec<string>(`kubectl config view --minify --output "jsonpath={..namespace}"`)
91+
const cmdline = `kubectl config view --minify --output "jsonpath={..namespace}"`
92+
const ns = await REPL.qexec<string>(cmdline)
93+
.then(ns => {
94+
if (typeof ns !== 'string' || ns.length === 0) {
95+
// e.g. microk8s
96+
console.error('Suspicious return value for current namespace', ns, cmdline)
97+
return 'default'
98+
} else {
99+
return ns
100+
}
101+
})
92102
.then(ns => {
93103
currentDefaultNamespaceCache = ns
94104
return ns
@@ -100,12 +110,7 @@ export async function getCurrentDefaultNamespace({ REPL }: { REPL: REPLType }) {
100110
return 'default'
101111
})
102112

103-
if (typeof ns !== 'string') {
104-
// e.g. microk8s
105-
return 'default'
106-
} else {
107-
return ns ? ns.trim() : ns
108-
}
113+
return ns ? ns.trim() : ns
109114
}
110115

111116
/**

plugins/plugin-kubectl/src/controller/kubectl/kustomize.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ const strings = i18n('plugin-kubectl', 'kustomize')
3535
function prepare(args: Arguments<KubeOptions>): string {
3636
const idx = args.argvNoOptions.indexOf('kustomize')
3737
const filepath = args.argvNoOptions[idx + 1]
38-
return args.command.replace(new RegExp(filepath, 'g'), expandHomeDir(filepath))
38+
if (!filepath) {
39+
return args.command
40+
} else {
41+
return args.command.replace(new RegExp(filepath, 'g'), expandHomeDir(filepath))
42+
}
3943
}
4044

4145
function groupByKind(resources: KubeResource[], rawFull: string): Menu[] {

0 commit comments

Comments
 (0)