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

Commit d4fb765

Browse files
committed
feat(plugins/plugin-kubectl): Add simple kubectl delete to the list of direct optimizations
Fixes #6468
1 parent 3cc9fe9 commit d4fb765

File tree

10 files changed

+237
-110
lines changed

10 files changed

+237
-110
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Debug from 'debug'
18+
import { Arguments } from '@kui-shell/core'
19+
20+
import { fetchFile } from '../../../lib/util/fetch-file'
21+
import { getKindAndVersion } from '../../kubectl/explain'
22+
23+
import { KubeOptions, fileOf, getLabel } from '../../kubectl/options'
24+
25+
import handleErrors from './errors'
26+
import { urlFormatterFor } from './url'
27+
import { status } from '../../kubectl/exec'
28+
import { FinalState } from '../../../lib/model/states'
29+
import { getCommandFromArgs } from '../../../lib/util/util'
30+
import { headersForPlainRequest as headers } from './headers'
31+
32+
const debug = Debug('plugin-kubectl/controller/client/direct/delete')
33+
34+
export default async function deleteDirect(
35+
args: Arguments<KubeOptions>,
36+
_kind = getKindAndVersion(
37+
getCommandFromArgs(args),
38+
args,
39+
args.argvNoOptions[args.argvNoOptions.indexOf('delete') + 1]
40+
)
41+
) {
42+
// For now, we only handle delete-by-name
43+
if (!fileOf(args) && !getLabel(args) && !args.parsedOptions['dry-run'] && !args.parsedOptions['field-selector']) {
44+
const explainedKind = await _kind
45+
const { kind } = explainedKind
46+
const formatUrl = await urlFormatterFor(args, explainedKind)
47+
48+
const kindIdx = args.argvNoOptions.indexOf('delete') + 1
49+
const names = args.argvNoOptions.slice(kindIdx + 1)
50+
if (names.length > 0) {
51+
const urls = names.map(formatUrl.bind(undefined, true, true)).join(',')
52+
debug('attempting delete direct', urls)
53+
54+
const responses = await fetchFile(args.REPL, urls, { method: 'delete', headers, returnErrors: true })
55+
56+
// then dissect it into errors and non-errors
57+
const { errors, ok } = await handleErrors(responses, formatUrl, kind, args.REPL)
58+
if (ok.length === 0) {
59+
// all errors
60+
return errors.map(_ => _.message).join('\n')
61+
} else {
62+
return status(args, 'delete', getCommandFromArgs(args), FinalState.OfflineLike)
63+
}
64+
}
65+
}
66+
}

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

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

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

19-
import { URLFormatter } from './get'
19+
import URLFormatter from './url'
2020
import { Status, isStatus } from '../../../lib/model/resource'
2121
import { fetchFile, FetchedFile, isReturnedError } from '../../../lib/util/fetch-file'
2222

@@ -55,11 +55,12 @@ export default async function handleErrors(
5555
const nsUrl = formatUrl(false)
5656
const kindUrl = formatUrl(true) + '?limit=1'
5757

58+
const opts = { headers: { accept: 'application/json' } }
5859
const [nsData, kindData] = await Promise.all([
59-
fetchFile(repl, nsUrl, { accept: 'application/json' })
60+
fetchFile(repl, nsUrl, opts)
6061
.then(_ => _[0])
6162
.catch(err => JSON.parse(err.message)),
62-
fetchFile(repl, kindUrl, { accept: 'application/json' })
63+
fetchFile(repl, kindUrl, opts)
6364
.then(_ => _[0])
6465
.catch(err => JSON.parse(err.message))
6566
])

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

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -21,70 +21,13 @@ import { Explained } from '../../kubectl/explain'
2121
import { fetchFile } from '../../../lib/util/fetch-file'
2222
import { toKuiTable, withNotFound } from '../../../lib/view/formatTable'
2323

24-
import {
25-
KubeOptions,
26-
formatOf,
27-
getLabel,
28-
getNamespace,
29-
isForAllNamespaces,
30-
isTableRequest,
31-
isWatchRequest
32-
} from '../../kubectl/options'
24+
import { KubeOptions, formatOf, getNamespace, isTableRequest, isWatchRequest } from '../../kubectl/options'
3325

3426
import handleErrors from './errors'
27+
import { urlFormatterFor } from './url'
28+
import { headersForTableRequest } from './headers'
3529
import { isStatus, KubeItems, MetaTable } from '../../../lib/model/resource'
3630

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> {
45-
const namespace = await getNamespace(args)
46-
47-
const kindOnPath = `/${encodeURIComponent(kind.toLowerCase() + (/s$/.test(kind) ? '' : 's'))}`
48-
49-
// e.g. "apis/apps/v1" for deployments
50-
const apiOnPath = version === 'v1' ? 'api/v1' : `apis/${encodeURIComponent(version)}`
51-
52-
// a bit complex: "kubectl get ns", versus "kubectl get ns foo"
53-
// the "which" is "foo" in the second case
54-
const namespaceOnPath = isForAllNamespaces(args.parsedOptions)
55-
? ''
56-
: kind === 'Namespace'
57-
? ''
58-
: isClusterScoped
59-
? ''
60-
: `/namespaces/${encodeURIComponent(namespace)}`
61-
62-
// we will accumulate queries
63-
const queries: string[] = []
64-
65-
// labelSelector query
66-
const label = getLabel(args)
67-
if (label) {
68-
const push = (query: string) => queries.push(`labelSelector=${encodeURIComponent(query)}`)
69-
if (Array.isArray(label)) {
70-
label.forEach(push)
71-
} else {
72-
push(label)
73-
}
74-
}
75-
76-
// limit query
77-
if (typeof args.parsedOptions.limit === 'number') {
78-
queries.push(`limit=${args.parsedOptions.limit}`)
79-
}
80-
81-
// format a url
82-
return (includeKind = false, includeQueries = false, name?: string) =>
83-
`kubernetes:///${apiOnPath}${namespaceOnPath}${!includeKind ? '' : kindOnPath}${
84-
!name ? '' : `/${encodeURIComponent(name)}`
85-
}${!includeQueries || queries.length === 0 ? '' : '?' + queries.join('&')}`
86-
}
87-
8831
export default async function getDirect(args: Arguments<KubeOptions>, _kind: Promise<Explained>) {
8932
const explainedKind = _kind ? await _kind : { kind: undefined, version: undefined, isClusterScoped: false }
9033
const { kind } = explainedKind
@@ -99,7 +42,7 @@ export default async function getDirect(args: Arguments<KubeOptions>, _kind: Pro
9942
const fmt = format || 'default'
10043
if (fmt === 'wide' || fmt === 'default') {
10144
// first, fetch the data
102-
const responses = await fetchFile(args.REPL, urls, headersForTableRequest, true)
45+
const responses = await fetchFile(args.REPL, urls, { headers: headersForTableRequest, returnErrors: true })
10346

10447
// then dissect it into errors and non-errors
10548
const { errors, ok } = await handleErrors(responses, formatUrl, kind, args.REPL)
@@ -144,7 +87,7 @@ export default async function getDirect(args: Arguments<KubeOptions>, _kind: Pro
14487
) {
14588
let response: string | Buffer | object
14689
try {
147-
response = (await fetchFile(args.REPL, urls, { accept: 'application/json' }))[0]
90+
response = (await fetchFile(args.REPL, urls, { headers: { accept: 'application/json' } }))[0]
14891
} catch (err) {
14992
response = JSON.parse(err.message)
15093
if (!isStatus(response)) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export const headersForPlainRequest = { accept: 'application/json' }
18+
export const headersForTableRequest = { accept: 'application/json;as=Table;g=meta.k8s.io;v=v1' }
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Arguments } from '@kui-shell/core'
18+
19+
import { Explained } from '../../kubectl/explain'
20+
import { KubeOptions, getLabel, getNamespace, isForAllNamespaces } from '../../kubectl/options'
21+
22+
export type URLFormatter = (includeKind?: boolean, includeQueries?: boolean, name?: string) => string
23+
24+
export async function urlFormatterFor(
25+
args: Arguments<KubeOptions>,
26+
{ kind, version, isClusterScoped }: Explained
27+
): Promise<URLFormatter> {
28+
const namespace = await getNamespace(args)
29+
30+
const kindOnPath = `/${encodeURIComponent(kind.toLowerCase() + (/s$/.test(kind) ? '' : 's'))}`
31+
32+
// e.g. "apis/apps/v1" for deployments
33+
const apiOnPath = version === 'v1' ? 'api/v1' : `apis/${encodeURIComponent(version)}`
34+
35+
// a bit complex: "kubectl get ns", versus "kubectl get ns foo"
36+
// the "which" is "foo" in the second case
37+
const namespaceOnPath = isForAllNamespaces(args.parsedOptions)
38+
? ''
39+
: kind === 'Namespace'
40+
? ''
41+
: isClusterScoped
42+
? ''
43+
: `/namespaces/${encodeURIComponent(namespace)}`
44+
45+
// we will accumulate queries
46+
const queries: string[] = []
47+
48+
// labelSelector query
49+
const label = getLabel(args)
50+
if (label) {
51+
const push = (query: string) => queries.push(`labelSelector=${encodeURIComponent(query)}`)
52+
if (Array.isArray(label)) {
53+
label.forEach(push)
54+
} else {
55+
push(label)
56+
}
57+
}
58+
59+
// limit query
60+
if (typeof args.parsedOptions.limit === 'number') {
61+
queries.push(`limit=${args.parsedOptions.limit}`)
62+
}
63+
64+
// format a url
65+
return (includeKind = false, includeQueries = false, name?: string) =>
66+
`kubernetes:///${apiOnPath}${namespaceOnPath}${!includeKind ? '' : kindOnPath}${
67+
!name ? '' : `/${encodeURIComponent(name)}`
68+
}${!includeQueries || queries.length === 0 ? '' : '?' + queries.join('&')}`
69+
}
70+
71+
export default URLFormatter

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { toKuiTable } from '../../../lib/view/formatTable'
2121
import { fetchFile, openStream } from '../../../lib/util/fetch-file'
2222
import { KubeOptions, withKubeconfigFrom } from '../../kubectl/options'
2323

24-
import { headersForTableRequest, URLFormatter } from './get'
24+
import URLFormatter from './url'
25+
import { headersForTableRequest } from './headers'
2526
import { MetaTable, isMetaTable } from '../../../lib/model/resource'
2627

2728
const debug = Debug('plugin-kubectl/client/direct/watch')
@@ -173,7 +174,9 @@ class DirectWatcher implements Abortable, Watcher {
173174
/** Initialize the streamer for table footer updates */
174175
private async initFooterUpdates() {
175176
// first: we need to fetch the initial table (so that we have a resourceVersion)
176-
const events = (await fetchFile(this.args.REPL, this.formatEventUrl(), headersForTableRequest))[0] as MetaTable
177+
const events = (
178+
await fetchFile(this.args.REPL, this.formatEventUrl(), { headers: headersForTableRequest })
179+
)[0] as MetaTable
177180
if (isMetaTable(events)) {
178181
this.onEventData({ object: events })
179182

plugins/plugin-kubectl/src/controller/fetch-file.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,11 @@ export default (registrar: Registrar) => {
116116
`/${commandPrefix}/_fetchfile`,
117117
async ({ argvNoOptions, parsedOptions, REPL, execOptions }: Arguments<Options>) => {
118118
const uri = argvNoOptions[argvNoOptions.indexOf('_fetchfile') + 1]
119-
const headers =
120-
typeof execOptions.data === 'object' && !Buffer.isBuffer(execOptions.data)
121-
? execOptions.data.headers
122-
: undefined
119+
const opts =
120+
typeof execOptions.data === 'object' && !Buffer.isBuffer(execOptions.data) ? execOptions.data : undefined
123121

124122
if (!parsedOptions.kustomize) {
125-
return { mode: 'raw', content: await fetchFile(REPL, uri, headers) }
123+
return { mode: 'raw', content: await fetchFile(REPL, uri, opts) }
126124
} else {
127125
return { mode: 'raw', content: await fetchKustomizeString(REPL, uri) }
128126
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { KubeOptions } from './options'
2121
import { doExecWithStatus } from './exec'
2222
import commandPrefix from '../command-prefix'
2323

24+
import deleteDirect from '../client/direct/delete'
2425
import { FinalState } from '../../lib/model/states'
2526
import { isUsage, doHelp } from '../../lib/util/help'
2627

@@ -42,6 +43,19 @@ export const doDelete = (command = 'kubectl') => async (args: Arguments<KubeOpti
4243
if (isUsage(args)) {
4344
return doHelp(command, args, prepareArgsForDelete)
4445
} else {
46+
try {
47+
const directResponse = await deleteDirect(args)
48+
if (directResponse) {
49+
return directResponse
50+
}
51+
} catch (err) {
52+
if (err.code === 404) {
53+
throw err
54+
} else {
55+
console.error('Error in direct delete. Falling back to CLI delete.', err.code, err)
56+
}
57+
}
58+
4559
return doExecWithStatus('delete', FinalState.OfflineLike, command, prepareArgsForDelete)(args)
4660
}
4761
}

0 commit comments

Comments
 (0)