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

Commit 9d62eeb

Browse files
myan9starpit
authored andcommitted
feat: initial popeye support
Fixes #5265
1 parent f4ba8e6 commit 9d62eeb

File tree

8 files changed

+217
-5
lines changed

8 files changed

+217
-5
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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, Row } from '@kui-shell/core'
18+
19+
import { doHelp, isUsage, getNamespace, getKind, doExecWithStdout } from '@kui-shell/plugin-kubectl'
20+
21+
interface PopeyeReport {
22+
score: number
23+
grade: string
24+
sanitizers: Sanitizer[]
25+
}
26+
27+
interface Sanitizer {
28+
sanitizer: string
29+
tally: {
30+
ok: number
31+
info: number
32+
warning: number
33+
error: number
34+
score: number
35+
}
36+
issues: Record<string, Issue[]>
37+
}
38+
39+
interface Issue {
40+
group: string
41+
level: number
42+
message: string
43+
}
44+
45+
function bodyToHeader(body: Row[]) {
46+
if (body.length > 0) {
47+
return {
48+
key: body[0].key,
49+
name: body[0].key,
50+
attributes: body[0].attributes.map(({ key }) => ({ key, value: key }))
51+
}
52+
}
53+
}
54+
55+
function formatIssueRow(group: string, message: string, level: number) {
56+
return {
57+
key: 'Group',
58+
name: group,
59+
attributes: [
60+
{
61+
key: 'Message',
62+
value: message,
63+
tag: 'badge',
64+
css: level === 3 ? 'red-background' : level === 2 ? 'yellow-background' : 'green-background'
65+
}
66+
]
67+
}
68+
}
69+
70+
function formatSectionRow(
71+
displayName: string,
72+
resourceName: string,
73+
command: string,
74+
sanitizer: string,
75+
maxLevel: number,
76+
ns: string
77+
) {
78+
return {
79+
key: 'Name',
80+
name: displayName, // e.g. split out the namespace in resourceName
81+
onclick: `${command} popeye -s ${sanitizer} ${resourceName} -n ${ns}`,
82+
attributes: [
83+
{
84+
key: 'Max Level',
85+
value: maxLevel.toString(),
86+
tag: 'badge',
87+
css: maxLevel === 3 ? 'red-background' : maxLevel === 2 ? 'yellow-background' : 'green-background'
88+
}
89+
]
90+
}
91+
}
92+
93+
export default (command: string) => async (args: Arguments) => {
94+
if (isUsage(args)) {
95+
return doHelp(command, args)
96+
} else {
97+
const userAskForSection = (args.parsedOptions['s'] || args.parsedOptions['sections']) as string
98+
let userAskResourceName
99+
100+
/**
101+
* override the popeye command to force json output
102+
* , and add support for fetching report by resource name
103+
*
104+
*/
105+
const prepareArgsForPopeye = (args: Arguments) => {
106+
userAskResourceName = userAskForSection && args.argvNoOptions[args.argvNoOptions.indexOf('popeye') + 1]
107+
108+
/** popeye doesn't support showing report for a single resource name
109+
* e.g. popeye -s pod nginx
110+
* so we fetch the report of pod and filter the result
111+
*
112+
*/
113+
return userAskResourceName
114+
? `${args.command.replace(userAskResourceName, '')} -o json`
115+
: `${args.command} -o json`
116+
}
117+
118+
const stdout = await doExecWithStdout(args, prepareArgsForPopeye, command)
119+
const fullReport = JSON.parse(stdout).popeye as PopeyeReport
120+
// console.error('fullReport', fullReport)
121+
const ns = await getNamespace(args)
122+
123+
const formatTable = (body: Row[], title: string, footer?: string[]) => {
124+
return {
125+
header: bodyToHeader(body),
126+
body,
127+
gridableColumn: 1,
128+
breadcrumbs: [{ label: 'Popeye' }, { label: ns }],
129+
title,
130+
footer
131+
}
132+
}
133+
134+
/**
135+
* full report issued by command kubectl popeye
136+
*
137+
*/
138+
const fullReportTable = () => {
139+
const body = fullReport.sanitizers.map(({ sanitizer, tally }) => {
140+
return {
141+
key: 'Name',
142+
name: sanitizer,
143+
onclick: `${command} popeye -s ${sanitizer} -n ${ns}`,
144+
attributes: [
145+
{
146+
key: 'Score',
147+
value: tally.score.toString(),
148+
tag: 'badge',
149+
css: tally.score === 100 ? 'green-background' : 'red-background'
150+
},
151+
{ key: 'Error', value: tally.error.toString() },
152+
{ key: 'Warning', value: tally.warning.toString() },
153+
{ key: 'Info', value: tally.info.toString() },
154+
{ key: 'Ok', value: tally.ok.toString() }
155+
]
156+
}
157+
})
158+
159+
return formatTable(body, 'All resources', [`Overall score: ${fullReport.score} ${fullReport.grade}`])
160+
}
161+
162+
/**
163+
* section report issued by command e.g. kubectl popeye -s pod
164+
*
165+
*/
166+
const sectionTable = async () => {
167+
const body = []
168+
fullReport.sanitizers.forEach(({ issues, sanitizer }) => {
169+
if (issues) {
170+
Object.entries(issues).forEach(([resourceName, issueList]) => {
171+
if (resourceName) {
172+
let maxLevel = 0
173+
issueList.forEach(({ level }) => (maxLevel = level > maxLevel ? level : maxLevel))
174+
body.push(formatSectionRow(resourceName.split(/\//)[1], resourceName, command, sanitizer, maxLevel, ns))
175+
} else {
176+
issueList.forEach(_ => body.push(formatIssueRow(_.group, _.message, _.level)))
177+
}
178+
})
179+
}
180+
})
181+
182+
return formatTable(body, await getKind(command, args, userAskForSection), [
183+
`Overall score: ${fullReport.score} ${fullReport.grade}`
184+
])
185+
}
186+
187+
/**
188+
* all issues in e.g. pod nginx
189+
*
190+
*/
191+
const resourceNameTable = () => {
192+
const body = fullReport.sanitizers[0].issues[userAskResourceName].map(({ group, level, message }) =>
193+
formatIssueRow(group, message, level)
194+
)
195+
return formatTable(body, userAskResourceName.split(/\//)[1])
196+
}
197+
198+
if (userAskResourceName) {
199+
return resourceNameTable()
200+
} else if (userAskForSection) {
201+
return sectionTable()
202+
} else {
203+
return fullReportTable()
204+
}
205+
}
206+
}

plugins/plugin-kubectl/krew/src/plugin.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import { Registrar } from '@kui-shell/core'
1818
import { defaultFlags, commandPrefix } from '@kui-shell/plugin-kubectl'
1919

20-
import help from './controller/help'
21-
import info from './controller/info'
22-
import list from './controller/list'
23-
import genericTable from './controller/generic-table'
20+
import help from './controller/krew/help'
21+
import info from './controller/krew/info'
22+
import list from './controller/krew/list'
23+
import genericTable from './controller/krew/generic-table'
24+
25+
import popeye from './controller/popeye'
2426

2527
const aliases = ['k', 'kubectl']
2628
const canonical = 'kubectl'
@@ -32,5 +34,7 @@ export default async (registrar: Registrar) => {
3234
registrar.listen(`/${commandPrefix}/${command}/krew/list`, list(canonical), defaultFlags)
3335
registrar.listen(`/${commandPrefix}/${command}/krew/search`, genericTable(canonical), defaultFlags)
3436
registrar.listen(`/${commandPrefix}/${command}/krew/version`, genericTable(canonical), defaultFlags)
37+
38+
registrar.listen(`/${commandPrefix}/${command}/popeye`, popeye(canonical), defaultFlags)
3539
})
3640
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export async function exec<O extends KubeOptions>(
195195
content: {
196196
code: 0,
197197
stdout: await doExecWithPty(args, prepare),
198-
stderr: '',
198+
stderr: undefined,
199199
wasSentToPty: true
200200
}
201201
})

plugins/plugin-kubectl/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export { fetchFileString } from './lib/util/fetch-file'
8585

8686
export { fqnOf, fqn } from './controller/kubectl/fqn'
8787

88+
export { getKind } from './controller/kubectl/explain'
89+
8890
/**
8991
* Exports for future delegation; e.g. `oc get pods` is mostly just
9092
* `kubectl get pods`

0 commit comments

Comments
 (0)