@kui-shell/plugin-kubectl
Version:
Kubernetes visualization plugin for kubernetes
325 lines (281 loc) • 10.4 kB
JavaScript
/*
* Copyright 2019 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { v4: uuid } = require('uuid')
const assert = require('assert')
const { dirname, join } = require('path')
const { Common, CLI, ReplExpect, Selectors, SidecarExpect, Util } = require('@kui-shell/test')
const { makeCLI } = require('@kui-shell/core/tests/lib/headless')
const ROOT = process.env.TEST_ROOT
/** should contain kubectl-kui */
const bindir = process.env.KUI ? dirname(process.env.KUI) : join(ROOT, '../../bin')
/** the default tab we expect to see on "get" */
exports.defaultModeForGet = 'summary'
/** Do singleton tables have a title decoration? */
exports.singletonTablesHaveTitle = false
/**
* Wait for a green badge
*
*/
exports.waitForGreen = async (app, selector) => {
const notRepeatingPulse = 'td:not(.repeating-pulse)'
const badge = `${selector} [data-tag="badge"]`
const greenBadge = `${selector} ${notRepeatingPulse} [data-tag="badge"].green-background`
const yellowBadge = `${selector} [data-tag="badge"].yellow-background`
try {
await app.client.$(yellowBadge).then(_ => _.waitForExist({ timeout: CLI.waitTimeout, reverse: true }))
} catch (err) {
console.log(`Creation is still yellow after ${CLI.waitTimeout} ${selector}`)
const text = await app.client.$(badge).then(_ => _.getText())
console.log(`Creation status ${text}`)
}
await app.client.$(greenBadge).then(_ => _.waitForExist({ timeout: CLI.waitTimeout }))
return greenBadge
}
/**
* Wait for a red badge
*
*/
exports.waitForRed = async (app, selector) => {
const notRepeatingPulse = 'td:not(.repeating-pulse)'
const badge = `${selector} ${notRepeatingPulse} [data-tag="badge"].red-background`
const yellowBadge = `${selector} [data-tag="badge"].yellow-background`
// the green badge should disappear, wait for 5 seconds at max
try {
await app.client.$(badge.replace('red', 'green')).then(_ => _.waitForExist({ timeout: 5000, reverse: true }))
} catch (err) {
console.log('Deletion is still green after 5000 ms')
}
try {
await app.client.$(yellowBadge).then(_ => _.waitForExist({ timeout: CLI.waitTimeout, reverse: true }))
} catch (err) {
console.log(`Deletion is still yellow after ${CLI.waitTimeout}`)
const text = await app.client.$(yellowBadge).then(_ => _.getText())
console.log(`Deletion status ${text}`)
}
await app.client.$(badge).then(_ => _.waitForExist({ timeout: CLI.waitTimeout }))
return badge
}
exports.createNS = (prefix = '') => `${prefix}${uuid()}-kui`
exports.allocateNS = (ctx, ns, command = 'kubectl', theCli = CLI) => {
it(`should create a namespace ${ns} for test: ${ctx.title}`, () => {
return theCli
.command(`${command} create namespace ${ns}`, ctx.app)
.then(ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(ns) }))
.then(selector => exports.waitForGreen(ctx.app, selector))
.catch(Common.oops(ctx, true))
})
}
exports.deleteNS = (ctx, ns, command = 'kubectl', theCli = CLI) => {
if (!process.env.TRAVIS_JOB_ID) {
// to save travis test time
const namespaces = Array.isArray(ns) ? ns : [ns]
it(`should delete the namespaces ${namespaces}`, async () => {
try {
const res = await theCli.command(`${command} delete namespace ${namespaces.join(' ')}`, ctx.app)
await Promise.all(
namespaces.map(ns =>
ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(ns) })(res).then(selector =>
exports.waitForRed(ctx.app, selector)
)
)
)
} catch (err) {
await Common.oops(ctx, true)(err)
}
})
}
}
/**
* Execute a `command` to show a table;
* wait for `name` to be green
*
*/
exports.list = async (ctx, command, name, wait = true) => {
const selector = await Util.doList(ctx, command, name)
if (wait) {
// wait for the badge to become green
await exports.waitForGreen(ctx.app, selector)
}
return selector
}
/**
* Execute a `command` to show a table;
* wait for `name` to be green;
* click the `name` to open a sdiecar
*
*/
exports.openSidecarByList = async (ctx, command, name, wait = true, mode = exports.defaultModeForGet) => {
const selector = await exports.list(ctx, command, name, wait)
return Util.openSidecarByClick(ctx, `${selector} .clickable`, name, mode)
}
exports.deletePodByName = (ctx, pod, ns, command = 'kubectl', theCli = CLI) => {
it(`should delete the pod ${pod} by name via ${command}`, () => {
return theCli
.command(`${command} delete pod ${pod} -n ${ns}`, ctx.app)
.then(ReplExpect.okWithCustom({ selector: Selectors.BY_NAME(pod) }))
.then(selector => exports.waitForRed(ctx.app, selector))
.catch(Common.oops(ctx, true))
})
}
/**
* Keep poking the given kind till no more such entities exist
*
*/
exports.waitTillNone =
(kind, theCli = makeCLI('kubectl'), name = '', okToSurvive, inNamespace = '') =>
app =>
new Promise(resolve => {
// fetch the entities
const fetch = async () => {
const response = await theCli.command(
`get "${kind}" ${typeof name === 'string' ? name : name.join(' ')} ${inNamespace}`,
app,
{
errOk: 1
}
)
return response
}
// verify the entities
const verify = okToSurvive
? theCli === ReplExpect
? theCli.expectOKWith(okToSurvive)
: theCli.expectOK(okToSurvive)
: theCli.expectError(1, 'not found')
const iter = () => {
return fetch()
.then(verify)
.then(resolve)
.catch(() => setTimeout(iter, 3000))
}
iter()
})
/**
* Keep poking the given kind till no more such entities exist
*
*/
exports.waitTillTerminating =
(kind, theCli = CLI, name, inNamespace) =>
app =>
new Promise(resolve => {
// fetch the entities
const fetch = () =>
theCli
.do(`kubectl get "${kind}" ${name} ${inNamespace}`, app, {
errOk: theCli.exitCode(404)
})
.then(res => {
return /Terminating/.test(res.output)
})
const iter = async () => {
const isTerminating = await fetch()
if (isTerminating) {
resolve()
} else {
setTimeout(iter, 3000)
}
}
iter()
})
/**
* Confirm that the table title matches
*
*/
exports.assertTableTitleMatches = async function (self, tableSelector, expectedTitle) {
// getHTML rather than getText, in case the title is not visible in this client
const tableTitle = (await self.app.client.$(`${tableSelector} .result-table-title`).then(_ => _.getHTML())).replace(
/<div.*>(.*)<\/div>/,
'$1'
)
assert.strictEqual(tableTitle.toLowerCase(), expectedTitle)
}
/**
* Type slowly, this helps with some odd webpack+proxy issues
*
*/
exports.typeSlowly = async (app, txt) => {
for (let idx = 0; idx < txt.length; idx++) {
await new Promise(resolve => setTimeout(resolve, 20))
await app.client.keys(txt.charAt(idx))
}
await new Promise(resolve => setTimeout(resolve, 20))
}
/** kubectl kui impl */
exports.kubectl = makeCLI('kubectl kui', bindir)
/** kubectl kui --ui impl */
// exports.kubectlElectron = make CLI('kubectl kui', bindir, true) // the last true requests teeToFile mode
/**
* Test Usage
*
*/
exports.doHelp = function doHelp(cmd, breadcrumbs, modes, content = '') {
it(`should give help for known outer command=${cmd} breadcrumbs=${breadcrumbs}`, async () => {
try {
const res = await CLI.command(cmd, this.app).then(ReplExpect.ok).then(SidecarExpect.open)
await this.app.client.$(Selectors.SIDECAR_BREADCRUMBS(res.count)).then(_ => _.waitForDisplayed())
await this.app.client.$(Selectors.SIDECAR_BREADCRUMBS(res.count)).then(_ => _.getText())
await expectArray(breadcrumbs.map(_ => _.label))
await Promise.all(
modes.map(_ =>
this.app.client.$(Selectors.SIDECAR_MODE_BUTTON_V2(res.count, _)).then(_ => _.waitForDisplayed())
)
)
if (content) {
return this.app.client.waitUntil(async () => {
const text = await this.app.client
.$(`${Selectors.SIDECAR(res.count)} .kui--sidecar-text-content`)
.then(_ => _.getText())
return text.include(content)
}, CLI.waitTimeout)
}
} catch (err) {
await Common.oops(this, true)
}
})
}
/** Selector to extract the Terminal rows */
const terminalRows = (N, splitIndex) => `${Selectors.SIDECAR_TAB_CONTENT(N, splitIndex)} .xterm-rows`
/** Get text from a Terminal-oriented tab */
exports.getTerminalText = async function (res) {
await this.app.client.$(terminalRows(res.count, res.splitIndex)).then(_ => _.waitForExist())
return this.app.client.$(terminalRows(res.count, res.splitIndex)).then(_ => _.getText())
}
/** Wait for the given checker to be true, w.r.t. the log text in the view */
exports.waitForTerminalText = async function (res, checker) {
let idx = 0
const get = exports.getTerminalText.bind(this, res)
return this.app.client.waitUntil(async () => {
const text = await get()
if (++idx > 5) {
console.error(`still waiting for terminal text actualText=${text}`)
}
return typeof checker === 'string'
? text.indexOf(checker) >= 0
: typeof checker === 'function'
? checker(text)
: checker.test(text)
}, CLI.waitTimeout)
}
/** URL of remote pod yaml */
exports.remotePodYaml =
'https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/pods/simple-pod.yaml'
/** Name of remote pod */
exports.remotePodName = 'nginx'
/** URL of remote pod yaml */
exports.remotePodYaml2 =
'https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/pods/lifecycle-events.yaml'
/** Name of remote pod2 */
exports.remotePodName2 = 'lifecycle-demo'