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

Commit b95cbdb

Browse files
committed
feat(plugins/plugin-kubectl): add support for kustomize apply/delete/create
Fixes #4203
1 parent 8576458 commit b95cbdb

File tree

16 files changed

+383
-24
lines changed

16 files changed

+383
-24
lines changed

plugins/plugin-ibmcloud/plugin/src/controller/available.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default async function getAvailablePlugins(
3030
tab: Tab,
3131
url = defaultURL
3232
): Promise<{ plugins: AvailablePluginRaw[] }> {
33-
return JSON.parse((await fetchFileString(tab, `${url}/plugins`))[0])
33+
return JSON.parse((await fetchFileString(tab.REPL, `${url}/plugins`))[0])
3434
}
3535

3636
/**
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"Raw Data": "Raw Data"
3+
}

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

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,54 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Registrar } from '@kui-shell/core'
17+
import { Arguments, ParsedOptions, Registrar, REPL } from '@kui-shell/core'
1818

1919
import commandPrefix from './command-prefix'
2020
import { fetchFileString } from '../lib/util/fetch-file'
2121

22+
interface Options extends ParsedOptions {
23+
kustomize?: boolean
24+
}
25+
26+
async function isFile(filepath: string): Promise<boolean> {
27+
const { lstat } = await import('fs')
28+
return new Promise((resolve, reject) => {
29+
lstat(filepath, (err, stats) => {
30+
if (err) {
31+
if (err.code === 'ENOENT') {
32+
resolve(false)
33+
} else {
34+
reject(err)
35+
}
36+
} else {
37+
resolve(stats.isFile())
38+
}
39+
})
40+
})
41+
}
42+
43+
async function fetchKustomizeString(repl: REPL, uri: string): Promise<{ data: string; dir?: string }> {
44+
const [isFile0, { join }] = await Promise.all([isFile(uri), import('path')])
45+
46+
if (isFile0) {
47+
return { data: await fetchFileString(repl, uri)[0] }
48+
} else {
49+
const k1 = join(uri, 'kustomization.yml')
50+
const k2 = join(uri, 'kustomization.yaml')
51+
const k3 = join(uri, 'Kustomization')
52+
53+
const [isFile1, isFile2, isFile3] = await Promise.all([isFile(k1), isFile(k2), isFile(k3)])
54+
const dir = uri // if we are here, then `uri` is a directory
55+
if (isFile1) {
56+
return { data: (await fetchFileString(repl, k1))[0], dir }
57+
} else if (isFile2) {
58+
return { data: (await fetchFileString(repl, k2))[0], dir }
59+
} else if (isFile3) {
60+
return { data: (await fetchFileString(repl, k3))[0], dir }
61+
}
62+
}
63+
}
64+
2265
/**
2366
* A server-side shim to allow browser-based clients to fetch `-f`
2467
* file content.
@@ -27,10 +70,15 @@ import { fetchFileString } from '../lib/util/fetch-file'
2770
export default (registrar: Registrar) => {
2871
registrar.listen(
2972
`/${commandPrefix}/_fetchfile`,
30-
async ({ argvNoOptions, tab }) => {
73+
async ({ argvNoOptions, parsedOptions, REPL }: Arguments<Options>) => {
3174
const uri = argvNoOptions[argvNoOptions.indexOf('_fetchfile') + 1]
32-
return fetchFileString(tab, uri)
75+
76+
if (!parsedOptions.kustomize) {
77+
return fetchFileString(REPL, uri)
78+
} else {
79+
return { mode: 'raw', content: await fetchKustomizeString(REPL, uri) }
80+
}
3381
},
34-
{ requiresLocal: true }
82+
{ requiresLocal: true, flags: { boolean: ['kustomize'] } }
3583
)
3684
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929

3030
import RawResponse from './response'
3131
import commandPrefix from '../command-prefix'
32-
import { KubeOptions, getNamespaceForArgv, getContextForArgv, fileOf } from './options'
32+
import { KubeOptions, getNamespaceForArgv, getContextForArgv, getFileForArgv } from './options'
3333

3434
import { FinalState } from '../../lib/model/states'
3535
import { stringToTable, KubeTableResponse } from '../../lib/view/formatTable'
@@ -48,8 +48,7 @@ export type PrepareForStatus<O extends KubeOptions> = (cmd: string, args: Argume
4848
/** Standard status preparation */
4949
function DefaultPrepareForStatus<O extends KubeOptions>(cmd: string, args: Arguments<O>) {
5050
const rest = args.argvNoOptions.slice(args.argvNoOptions.indexOf(cmd) + 1).join(' ')
51-
const file = fileOf(args)
52-
return file ? `-f ${fileOf(args)} ${rest}` : rest
51+
return `${getFileForArgv(args, true)}${rest}`
5352
}
5453

5554
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function kindPart(apiVersion: string, kind: string) {
5757
return `${kind}${versionString(apiVersion)}`
5858
}
5959

60-
function kindPartOf(resource: KubeResource) {
60+
export function kindPartOf(resource: KubeResource) {
6161
return kindPart(resource.apiVersion, resource.kind)
6262
}
6363

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2019 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 { resolve, basename } from 'path'
18+
import { Arguments, Menu, Registrar, i18n } from '@kui-shell/core'
19+
20+
import flags from './flags'
21+
import { kindPartOf } from './fqn'
22+
import { KubeOptions } from './options'
23+
import { doExecWithStdout } from './exec'
24+
import commandPrefix from '../command-prefix'
25+
import { KubeResource } from '../../lib/model/resource'
26+
27+
import { isUsage, doHelp } from '../../lib/util/help'
28+
29+
const strings = i18n('plugin-kubectl', 'kustomize')
30+
31+
function groupByKind(resources: KubeResource[], rawFull: string): Menu[] {
32+
const rawSplit = rawFull.split(/---/)
33+
34+
const groups = resources.reduce((groups, resource, idx) => {
35+
const key = kindPartOf(resource)
36+
37+
const group = groups[key]
38+
if (!group) {
39+
groups[key] = {
40+
modes: []
41+
}
42+
}
43+
44+
groups[key].modes.push({
45+
mode: resource.metadata.name,
46+
content: rawSplit[idx].replace(/^\n/, ''),
47+
contentType: 'yaml'
48+
})
49+
50+
return groups
51+
}, {} as Menu)
52+
53+
const rawMenu: Menu = {
54+
[strings('Raw Data')]: {
55+
modes: [
56+
{
57+
mode: 'YAML',
58+
content: rawFull,
59+
contentType: 'yaml'
60+
}
61+
]
62+
}
63+
}
64+
65+
// align to the somewhat odd NavResponse Menu model
66+
return Object.keys(groups)
67+
.map(group => ({
68+
[group]: groups[group]
69+
}))
70+
.concat([rawMenu])
71+
}
72+
73+
export const doKustomize = (command = 'kubectl') => async (args: Arguments<KubeOptions>) => {
74+
if (isUsage(args)) {
75+
return doHelp(command, args)
76+
} else {
77+
const [yaml, { safeLoadAll }] = await Promise.all([doExecWithStdout(args, undefined, command), import('js-yaml')])
78+
try {
79+
const resources = safeLoadAll(yaml)
80+
const inputFile = resolve(args.argvNoOptions[args.argvNoOptions.indexOf('kustomize') + 1])
81+
82+
return {
83+
apiVersion: 'kui-shell/v1',
84+
kind: 'NavResponse',
85+
breadcrumbs: [{ label: 'kustomize' }, { label: basename(inputFile), command: `open ${inputFile}` }],
86+
menus: groupByKind(resources, yaml)
87+
}
88+
} catch (err) {
89+
console.error('error preparing kustomize response', err)
90+
return yaml
91+
}
92+
}
93+
}
94+
95+
export default (registrar: Registrar) => {
96+
registrar.listen(`/${commandPrefix}/kubectl/kustomize`, doKustomize(), flags)
97+
registrar.listen(`/${commandPrefix}/k/kustomize`, doKustomize(), flags)
98+
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ export function fileOf(args: Arguments<KubeOptions>): string {
2626
return args.parsedOptions.f || args.parsedOptions.filename
2727
}
2828

29+
export function kustomizeOf(args: Arguments<KubeOptions>): string {
30+
return args.parsedOptions.k || args.parsedOptions.kustomize
31+
}
32+
33+
export function getFileForArgv(args: Arguments<KubeOptions>, addSpace = false): string {
34+
const file = fileOf(args)
35+
if (file) {
36+
return `-f ${file}${addSpace ? ' ' : ''}`
37+
} else {
38+
const kusto = kustomizeOf(args)
39+
if (kusto) {
40+
return `-k ${kusto}${addSpace ? ' ' : ''}`
41+
}
42+
}
43+
44+
return ''
45+
}
46+
2947
export function formatOf(args: Arguments<KubeOptions>): OutputFormat {
3048
return args.parsedOptions.o || args.parsedOptions.output
3149
}
@@ -161,6 +179,9 @@ export interface KubeOptions extends ParsedOptions {
161179
f?: string
162180
filename?: string
163181

182+
k?: string
183+
kustomize?: string
184+
164185
h?: boolean
165186
help?: boolean
166187
}

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

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ import {
3232

3333
import { flags } from './flags'
3434
import { fqnOfRef, ResourceRef, versionOf } from './fqn'
35-
import { KubeOptions as Options, fileOf, getNamespace, getContextForArgv } from './options'
35+
import { KubeOptions as Options, fileOf, kustomizeOf, getNamespace, getContextForArgv } from './options'
3636
import commandPrefix from '../command-prefix'
3737

38-
import fetchFile from '../../lib/util/fetch-file'
38+
import fetchFile, { fetchFileKustomize } from '../../lib/util/fetch-file'
3939
import KubeResource from '../../lib/model/resource'
4040
import TrafficLight from '../../lib/model/traffic-light'
4141
import { isDone, FinalState } from '../../lib/model/states'
@@ -61,6 +61,12 @@ const usage = (command: string) => ({
6161
file: true,
6262
docs: 'A kubernetes resource file or kind'
6363
},
64+
{
65+
name: '--kustomize',
66+
alias: '-k',
67+
file: true,
68+
docs: 'A kustomize file or directory'
69+
},
6470
{
6571
name: 'resourceName',
6672
positional: true,
@@ -100,11 +106,11 @@ const usage = (command: string) => ({
100106
})
101107

102108
/**
103-
* @param file an argument to `-f`, as in `kubectl -f <file>`
109+
* @param file an argument to `-f` or `-k`; e.g. `kubectl -f <file>`
104110
*
105111
*/
106112
async function getResourcesReferencedByFile(file: string, args: Arguments<FinalStateOptions>): Promise<ResourceRef[]> {
107-
const [{ safeLoadAll }, raw] = await Promise.all([import('js-yaml'), fetchFile(args.tab, file)])
113+
const [{ safeLoadAll }, raw] = await Promise.all([import('js-yaml'), fetchFile(args.REPL, file)])
108114

109115
const namespaceFromCommandLine = getNamespace(args) || 'default'
110116

@@ -121,6 +127,49 @@ async function getResourcesReferencedByFile(file: string, args: Arguments<FinalS
121127
})
122128
}
123129

130+
/**
131+
* @param kusto a kustomize file spec
132+
*
133+
*/
134+
interface Kustomization {
135+
resources?: string[]
136+
}
137+
async function getResourcesReferencedByKustomize(
138+
kusto: string,
139+
args: Arguments<FinalStateOptions>
140+
): Promise<ResourceRef[]> {
141+
const [{ safeLoad }, { join }, raw] = await Promise.all([
142+
import('js-yaml'),
143+
import('path'),
144+
fetchFileKustomize(args.REPL, kusto)
145+
])
146+
147+
const kustomization: Kustomization = safeLoad(raw.data)
148+
if (kustomization.resources) {
149+
const files = await Promise.all(
150+
kustomization.resources.map(resource => {
151+
return fetchFile(args.REPL, raw.dir ? join(raw.dir, resource) : resource)
152+
})
153+
)
154+
155+
return files
156+
.map(raw => safeLoad(raw[0]))
157+
.map(resource => {
158+
const { apiVersion, kind, metadata } = resource
159+
const { group, version } = versionOf(apiVersion)
160+
return {
161+
group,
162+
version,
163+
kind,
164+
name: metadata.name,
165+
namespace: metadata.namespace || getNamespace(args) || 'default'
166+
}
167+
})
168+
}
169+
170+
return []
171+
}
172+
124173
/**
125174
* @param argvRest the argv after `kubectl status`, with options stripped off
126175
*
@@ -385,13 +434,16 @@ const doStatus = (command: string) => async (args: Arguments<FinalStateOptions>)
385434
const rest = args.argvNoOptions.slice(args.argvNoOptions.indexOf('status') + 1)
386435
const commandArg = command || args.parsedOptions.command
387436
const file = fileOf(args)
437+
const kusto = kustomizeOf(args)
388438
const contextArgs = getContextForArgv(args)
389439
// const fileArgs = file ? `-f ${file}` : ''
390440
// const cmd = `${command} get ${rest} --watch ${fileArgs} ${contextArgs}`
391441

392442
try {
393443
const resourcesToWaitFor = file
394444
? await getResourcesReferencedByFile(file, args)
445+
: kusto
446+
? await getResourcesReferencedByKustomize(kusto, args)
395447
: getResourcesReferencedByCommandLine(rest, args)
396448
debug('resourcesToWaitFor', resourcesToWaitFor)
397449

0 commit comments

Comments
 (0)