kui-shell
Version:
This is the monorepo for Kui, the hybrid command-line/GUI electron-based Kubernetes tool
270 lines (238 loc) • 7.93 kB
JavaScript
/*
* Copyright 2018 IBM Corporation
*
* 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 debug = require('debug')('test.headless')
const assert = require('assert')
const { fileSync: makeTempFile } = require('tmp')
const { readFile } = require('fs')
const { exec } = require('child_process')
const { dirname, join } = require('path')
const ROOT = process.env.TEST_ROOT
const kui = process.env.KUI || join(ROOT, '../../bin/kui')
const bindir = process.env.KUI ? dirname(process.env.KUI) : join(ROOT, '../../bin') // should contain kubectl-kui
const { Util } = require('@kui-shell/test')
const { expectStruct, expectSubset } = Util
/**
* For tee to file mode, the core obliges us, and writes any electron
* output to a file we provide; we can also request that it write an
* end marker when it is done.
*
* @see app/src/webapp/cli.ts
*
*/
const KUI_TEE_TO_FILE_END_MARKER = 'XXX_KUI_END_MARKER'
/**
* readFile that returns a promise
*
*/
const readFileAsync = (fd /* : number */) =>
new Promise((resolve, reject) => {
readFile(fd, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
/**
* Poll a given file descriptor `fd` for the end marker
*
*/
const pollForEndMarker = async (fd /*: number */) /* : Promise<string> */ => {
return new Promise((resolve, reject) => {
const iter = async (idx = 0) => {
try {
const maybe = await readFileAsync(fd)
if (maybe.indexOf(KUI_TEE_TO_FILE_END_MARKER) >= 0) {
resolve(maybe.toString().replace(KUI_TEE_TO_FILE_END_MARKER, ''))
} else {
if (idx > 10) {
console.error(
'pollForEndMarker still waiting',
maybe.length,
maybe.slice(Math.max(0, maybe.length - 20)).toString()
)
}
setTimeout(() => iter(idx + 1), 1000)
}
} catch (err) {
reject(err)
}
}
iter()
})
}
/**
* Prepend to a PATH env; note this is not a filesystem path, but
* rather the executable PATH environment variable.
*
*/
const prependPATH = (path, extra) => {
if (!extra) {
return path
} else {
const sep = process.platform === 'win32' ? ';' : ':'
return `${extra}${sep}${path}`
}
}
/**
* The default CLI impl
*
*/
class CLI {
constructor(exe = kui, pathEnv, teeToFile = false) {
this.exe = exe
this.pathEnv = pathEnv
this.teeToFile = teeToFile
}
/**
* Execute a command
*
*/
command(cmd, env = {}, { errOk = undefined } = {}) {
return new Promise(resolve => {
const command = `${this.exe} ${cmd} --no-color`
debug('executing command', command)
const ourEnv = Object.assign({}, process.env, env, {
PATH: prependPATH(env.PATH || process.env.PATH, this.pathEnv)
})
// for headless-to-electron tests, we leverage the
// KUI_TEE_TO_FILE support offered by the core.
let tmpobj = { removeCallback: () => true }
if (this.teeToFile) {
tmpobj = makeTempFile()
ourEnv.KUI_TEE_TO_FILE = tmpobj.name
ourEnv.KUI_TEE_TO_FILE_END_MARKER = KUI_TEE_TO_FILE_END_MARKER
ourEnv.KUI_TEE_TO_FILE_EXIT_ON_END_MARKER = true
// ourEnv.DEBUG = '*' // be careful with this one, as it is incompatible with child_process.exec
}
exec(command, { env: ourEnv }, async (err, stdout, stderr) => {
if (this.teeToFile) debug('tee done', command)
const stdoutPromise = this.teeToFile ? pollForEndMarker(tmpobj.fd) : Promise.resolve(stdout)
if (err) {
// command failed miserably; collect all of the output of
// the command, so we can log this information
const output = stdout.trim().concat(stderr)
// delete any temporary files that supported the teeToFile capability
tmpobj.removeCallback()
if (err['code'] !== errOk) {
console.error('Error in command execution', err['code'], output)
}
resolve({ code: err['code'], output })
} else {
// command execution got somewhere, now we can inspect the
// output
debug('stdout', stdout)
debug('stderr', stderr)
const output = await stdoutPromise
resolve({ code: 0, output, stderr })
}
})
})
}
/**
* Exit code code for the given http status code.
* See ui.js for the analogous electron implementation.
*/
exitCode(statusCode) {
return statusCode - 256
}
expectJustOK() {
return args => {
return this.expectOK('', { exect: true })(args)
}
}
expectOKWithAny() {
return () => {
return this.expectOK()
}
}
expectOK(expectedOutput, { exact = false, skipLines = 0, squish = false } = {}) {
return ({ code: actualCode, output: actualOutput }) => {
assert.strictEqual(actualCode, 0)
if (expectedOutput) {
if (typeof expectedOutput === 'object') {
// json check
if (exact) {
return expectStruct(expectedOutput)(actualOutput)
} else {
return expectSubset(expectedOutput)(actualOutput)
}
} else {
// string check
let checkAgainst = actualOutput
// skip a number of initial lines?
if (skipLines > 0) {
checkAgainst = checkAgainst
.split(/\n/)
.slice(1)
.join('\n')
}
// squish whitespace?
if (squish) {
checkAgainst = checkAgainst
.split(/\n/)
.map(_ => _.replace(/\s+/g, ' ').trim())
.join('\n')
.trim()
}
if (exact) {
if (checkAgainst !== expectedOutput) {
console.error(`mismatch; actual='${actualOutput}'; expected='${checkAgainst}'`)
}
assert.strictEqual(checkAgainst, expectedOutput)
} else {
debug('expectedOutput', expectedOutput)
debug('actualOutput', actualOutput)
debug('checkAgainst', checkAgainst)
const ok = checkAgainst.indexOf(expectedOutput) >= 0
if (!ok) {
console.error(
`mismatch; actual='${actualOutput}' checkAgainst='${checkAgainst}'; expected='${expectedOutput}'`
)
}
assert.ok(ok)
}
}
}
return actualOutput
}
}
expectError(expectedCode, expectedOutput) {
return ({ code: actualCode, output: actualOutput }) => {
assert.strictEqual(actualCode, expectedCode)
if (expectedOutput) {
const ok =
typeof expectedOutput === 'string'
? actualOutput.indexOf(expectedOutput) >= 0
: expectedOutput.test(actualOutput) // expectedOutput is a RegExp
if (!ok) {
console.error(`mismatch; actual='${actualOutput}'; expected='${expectedOutput}'`)
}
assert.ok(ok)
}
return actualOutput
}
}
}
/** bin/kui impl */
exports.cli = new CLI()
/** bin/kui --ui impl */
exports.kuiElectron = new CLI(kui, undefined, true) // the last true requests teeToFile mode
/** kubectl kui impl */
exports.kubectl = new CLI('kubectl kui', bindir)
/** kubectl kui --ui impl */
exports.kubectlElectron = new CLI('kubectl kui', bindir, true) // the last true requests teeToFile mode