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

Commit 6d601ef

Browse files
committed
feat: kubectl apply view-last-applied
Fixes #4822 this includes a newly exported type from the core: ViewTransformer
1 parent cc27c4b commit 6d601ef

File tree

10 files changed

+187
-6
lines changed

10 files changed

+187
-6
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
ParsedOptions,
4141
EvaluatorArgs as Arguments,
4242
Event,
43+
ViewTransformer,
4344
CommandRegistrar as Registrar
4445
} from './models/command'
4546
export { optionsToString as unparse } from './core/utility'

packages/core/src/models/command.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export enum ExecType {
4343
Nested
4444
}
4545

46+
export type ViewTransformer<T extends KResponse, O extends ParsedOptions> = (
47+
args: EvaluatorArgs<O>,
48+
response: T
49+
) => Promise<T> | void | Promise<void>
50+
4651
export interface CommandOptions extends CapabilityRequirements {
4752
/** does this command accept no arguments of any sort (neither positional nor optional)? */
4853
noArgs?: boolean
@@ -86,10 +91,7 @@ export interface CommandOptions extends CapabilityRequirements {
8691
alwaysViewIn?: 'Terminal'
8792

8893
/** model to view transformer */
89-
viewTransformer?<T extends KResponse, O extends ParsedOptions>(
90-
args: EvaluatorArgs<O>,
91-
response: T
92-
): Promise<T> | void | Promise<void>
94+
viewTransformer?: ViewTransformer<KResponse, ParsedOptions>
9395
}
9496

9597
export interface Event {

plugins/plugin-kubectl/i18n/resources_en_US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"createResource": "Create this resource",
1515
"This resource has been deleted": "This resource has been deleted",
1616
"Error fetching resource": "Error fetching resource",
17+
"This resource has no last applied configuration": "This resource has no last applied configuration",
1718
"Unknown": "Unknown",
1819
"summary": "Summary",
1920
"resources": "Resources",

plugins/plugin-kubectl/oc/src/controller/kubectl/delegates.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
describer,
2626
registerConfig,
2727
registerEdit,
28+
registerApplySubcommands,
2829
doRun
2930
} from '@kui-shell/plugin-kubectl'
3031
import { doLogs } from '@kui-shell/plugin-kubectl/logs'
@@ -42,4 +43,5 @@ export default (registrar: Registrar) => {
4243
describer(registrar, command)
4344
registerConfig(registrar, command)
4445
registerEdit(registrar, command)
46+
registerApplySubcommands(registrar, command)
4547
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
/**
18+
* Notes: this file covers the three (as of this writing) subcommands
19+
* of apply:
20+
* - view-last-applied
21+
* - set-last-applied
22+
* - edit-last-applied
23+
*
24+
*/
25+
26+
import { Arguments, CodedError, CommandOptions, ViewTransformer, Registrar, i18n } from '@kui-shell/core'
27+
28+
import { flags } from './flags'
29+
import commandPrefix from '../command-prefix'
30+
import { isUsage, doHelp } from '../../lib/util/help'
31+
import { getCommandFromArgs } from '../../lib/util/util'
32+
import { viewTransformer as getTransformer } from './get'
33+
import { KubeOptions, getNamespaceForArgv } from './options'
34+
import KubeResource, { isKubeResource } from '../../lib/model/resource'
35+
import { hasLastApplied, mode as lastAppliedMode } from '../../lib/view/modes/last-applied'
36+
37+
const strings = i18n('plugin-kubectl')
38+
39+
/** View Transformer for view-last-applied */
40+
export async function viewLastApplied(args: Arguments<KubeOptions>, response: KubeResource) {
41+
if (isKubeResource(response)) {
42+
if (hasLastApplied(response)) {
43+
const baseView = await getTransformer(args, response)
44+
return Object.assign(baseView, { defaultMode: lastAppliedMode })
45+
} else {
46+
const error: CodedError = new Error(strings('This resource has no last applied configuration'))
47+
error.code = 404
48+
throw error
49+
}
50+
}
51+
}
52+
53+
function get(subcommand: string, args: Arguments<KubeOptions>) {
54+
const command = getCommandFromArgs(args)
55+
56+
if (isUsage(args)) {
57+
// special case: --help/-h
58+
return doHelp(command === 'k' ? 'kubectl' : command, args)
59+
}
60+
61+
const idx = args.argvNoOptions.indexOf(subcommand)
62+
const kind = args.argvNoOptions[idx + 1]
63+
const name = args.argvNoOptions[idx + 2]
64+
65+
return args.REPL.qexec<KubeResource>(`${command} get ${kind} ${name} ${getNamespaceForArgv(args)} -o yaml`)
66+
}
67+
68+
function withTransformer(viewTransformer: ViewTransformer<KubeResource, KubeOptions>): CommandOptions {
69+
return Object.assign({}, flags, { viewTransformer })
70+
}
71+
72+
export function registerApplySubcommands(registrar: Registrar, cmd: string) {
73+
registrar.listen(
74+
`/${commandPrefix}/${cmd}/apply/view-last-applied`,
75+
get.bind(undefined, 'view-last-applied'),
76+
withTransformer(viewLastApplied)
77+
)
78+
}
79+
80+
export default function registerForKubectl(registrar: Registrar) {
81+
registerApplySubcommands(registrar, 'kubectl')
82+
registerApplySubcommands(registrar, 'k')
83+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ export async function getNamespace(args: Arguments<KubeOptions>): Promise<string
138138
return args.parsedOptions.n || args.parsedOptions.namespace || (await getCurrentDefaultNamespace(args))
139139
}
140140

141+
/**
142+
* A variant of getNamespace where you *only* want to use what was
143+
* provided by the user in their command line.
144+
*/
145+
export function getNamespaceForArgv(args: Arguments<KubeOptions>): string {
146+
const ns = args.parsedOptions.n || args.parsedOptions.namespace
147+
return !ns ? '' : `-n ${ns}`
148+
}
149+
141150
export function getContext(args: Arguments<KubeOptions>) {
142151
return args.parsedOptions.context
143152
}

plugins/plugin-kubectl/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export { doCreate } from './controller/kubectl/create'
9797
export { doDelete } from './controller/kubectl/delete'
9898
export { describer } from './controller/kubectl/describe'
9999
export { register as registerConfig } from './controller/kubectl/config'
100+
export { registerApplySubcommands } from './controller/kubectl/apply-subcommands'
100101

101102
export { viewTransformer as getTransformer } from './controller/kubectl/get'
102103

plugins/plugin-kubectl/src/lib/view/modes/last-applied.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { KubeResource } from '../../model/resource'
2020

2121
const strings = i18n('plugin-kubectl')
2222

23+
/** Mode identifier. Not a UI string. Only for internal referencing. */
24+
export const mode = 'last applied'
25+
2326
/**
2427
* @return The last-applied-configuration annotation, as a raw string
2528
*
@@ -33,7 +36,7 @@ function getLastAppliedRaw(resource: KubeResource): string {
3336
* @return whether the given resource has a last applied configuration annotation
3437
*
3538
*/
36-
function hasLastApplied(resource: KubeResource): boolean {
39+
export function hasLastApplied(resource: KubeResource): boolean {
3740
return !resource.isSimulacrum && getLastAppliedRaw(resource) !== undefined
3841
}
3942

@@ -60,7 +63,7 @@ const renderLastApplied = async (tab: Tab, resource: KubeResource) => {
6063
export default {
6164
when: hasLastApplied,
6265
mode: {
63-
mode: 'last applied',
66+
mode,
6467
label: strings('lastApplied'),
6568
content: renderLastApplied
6669
}

plugins/plugin-kubectl/src/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import run from './controller/kubectl/run'
3131
import status from './controller/kubectl/status'
3232
import top from './controller/kubectl/top'
3333
import config from './controller/kubectl/config'
34+
import applySubcommands from './controller/kubectl/apply-subcommands'
3435

3536
import fetchFile from './controller/fetch-file'
3637
import catchall from './controller/kubectl/catchall'
@@ -51,6 +52,7 @@ export default async (registrar: Registrar) => {
5152
status(registrar)
5253
top(registrar)
5354
config(registrar)
55+
applySubcommands(registrar)
5456

5557
fetchFile(registrar)
5658
catchall(registrar)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 { Common, CLI, ReplExpect, SidecarExpect, Selectors } from '@kui-shell/test'
18+
import { waitForGreen, createNS, allocateNS, deleteNS } from '@kui-shell/plugin-kubectl/tests/lib/k8s/utils'
19+
20+
import { mode as lastAppliedMode } from '../../lib/view/modes/last-applied'
21+
22+
const commands = ['kubectl']
23+
if (process.env.NEEDS_OC) {
24+
commands.push('oc')
25+
}
26+
27+
const podName = 'nginx'
28+
const sourceFile = 'https://raw.githubusercontent.com/kubernetes/examples/master/staging/pod'
29+
30+
commands.forEach(command => {
31+
describe(`${command} apply view-last-applied ${process.env.MOCHA_RUN_TARGET || ''}`, function(this: Common.ISuite) {
32+
before(Common.before(this))
33+
after(Common.after(this))
34+
35+
const ns: string = createNS()
36+
const inNamespace = `-n ${ns}`
37+
38+
allocateNS(this, ns, command)
39+
40+
const doCreate = (verb: 'create' | 'apply') => {
41+
it(`should ${verb === 'create' ? 'create' : 'update'} sample pod from URL via ${command} ${verb}`, async () => {
42+
try {
43+
const selector = await CLI.command(`${command} ${verb} -f ${sourceFile} ${inNamespace}`, this.app).then(
44+
ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(podName) })
45+
)
46+
47+
// wait for the badge to become green
48+
await waitForGreen(this.app, selector)
49+
} catch (err) {
50+
await Common.oops(this, true)(err)
51+
}
52+
})
53+
}
54+
55+
doCreate('create')
56+
57+
// no last applied configuration, yet
58+
it(`view last applied configuration and expect 404 via ${command}`, () =>
59+
CLI.command(`${command} apply view-last-applied pod ${podName} ${inNamespace}`, this.app)
60+
.then(ReplExpect.error(404))
61+
.catch(Common.oops(this, true)))
62+
63+
// now use apply to get us a last applied configuration
64+
doCreate('apply')
65+
66+
// check that we can view this last applied configuration via "apply view-last-applied"
67+
it(`view last applied configuration via ${command} apply view-last-applied`, () =>
68+
CLI.command(`${command} apply view-last-applied pod ${podName} ${inNamespace}`, this.app)
69+
.then(ReplExpect.justOK)
70+
.then(SidecarExpect.open)
71+
.then(SidecarExpect.showing(podName, undefined, undefined, ns))
72+
.then(SidecarExpect.mode(lastAppliedMode))
73+
.catch(Common.oops(this, true)))
74+
75+
deleteNS(this, ns, command)
76+
})
77+
})

0 commit comments

Comments
 (0)