From 2c70a8fbdb5ba1a8a4cb3af29b660f353c7cf4bb Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Wed, 1 Nov 2023 15:50:45 +1100 Subject: [PATCH 1/5] feat: encapsulate helm and kubectl commands This PR also applies colors to the console messages for user for clarity Signed-off-by: Lenin Mehedy --- .../src/commands/cluster.mjs | 154 ++++++++++++++---- fullstack-network-manager/src/core/helm.mjs | 43 +++++ fullstack-network-manager/src/core/index.mjs | 5 +- .../src/core/kubectl.mjs | 72 ++++++++ .../src/core/shell_runner.mjs | 2 +- fullstack-network-manager/src/index.mjs | 9 +- .../test/commands/base.test.js | 7 +- 7 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 fullstack-network-manager/src/core/helm.mjs create mode 100644 fullstack-network-manager/src/core/kubectl.mjs diff --git a/fullstack-network-manager/src/commands/cluster.mjs b/fullstack-network-manager/src/commands/cluster.mjs index dc7ff3bad..a56049e13 100644 --- a/fullstack-network-manager/src/commands/cluster.mjs +++ b/fullstack-network-manager/src/commands/cluster.mjs @@ -11,7 +11,14 @@ export class ClusterCommand extends BaseCommand { constructor(opts) { super(opts); - this.kind = new Kind(opts) + + if (!opts || !opts.kind) throw new Error('An instance of core/Kind is required') + if (!opts || !opts.helm) throw new Error('An instance of core/Helm is required') + if (!opts || !opts.kubectl) throw new Error('An instance of core/Kubectl is required') + + this.kind = opts.kind + this.helm = opts.helm + this.kubectl = opts.kubectl } /** @@ -29,6 +36,40 @@ export class ClusterCommand extends BaseCommand { return [] } + showList(itemType, items = []) { + this.logger.showUser(chalk.green(`\n *** List of available ${itemType} ***`)) + this.logger.showUser(chalk.green(`---------------------------------------`)) + if (items.length > 0) { + items.forEach(name => this.logger.showUser(chalk.yellow(` - ${name}`))) + } else { + this.logger.showUser(chalk.blue(`[ None ]`)) + } + + this.logger.showUser("\n") + return true + } + + async showClusterList(argv) { + this.showList("clusters", await this.getClusters()) + return true + } + + /** + * List available namespaces + * @returns {Promise} + */ + async getNameSpaces() { + try { + return await this.kubectl.getNamespace(`--no-headers`, `-o name`) + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return [] + } + + /** * Get cluster-info for the given cluster name * @param argv arguments containing cluster name @@ -40,8 +81,9 @@ export class ClusterCommand extends BaseCommand { let cmd = `kubectl cluster-info --context kind-${clusterName}` let output = await this.run(cmd) - this.logger.showUser(`\nCluster information (${clusterName})\n---------------------------------------`) + this.logger.showUser(`Cluster information (${clusterName})\n---------------------------------------`) output.forEach(line => this.logger.showUser(line)) + this.logger.showUser("\n") return true } catch (e) { this.logger.error("%s", e) @@ -51,11 +93,30 @@ export class ClusterCommand extends BaseCommand { return false } - async showClusterList(argv) { - let clusters = await this.getClusters() - this.logger.showUser("\nList of available clusters \n---------------------------------------") - clusters.forEach(name => this.logger.showUser(name)) + async createNamespace(argv) { + try { + let namespace = argv.namespace + let namespaces = await this.getNameSpaces() + this.logger.showUser(chalk.cyan('> checking namespace:'), chalk.yellow(`${namespace}`)) + if (!namespaces.includes(`namespace/${namespace}`)) { + this.logger.showUser(chalk.cyan('> creating namespace:'), chalk.yellow(`${namespace} ...`)) + await this.kubectl.createNamespace(namespace) + this.logger.showUser(chalk.green('OK'), `namespace '${namespace}' is created`) + } { + this.logger.showUser(chalk.green('OK'), `namespace '${namespace}' already exists`) + } + + this.showList("namespaces", await this.getNameSpaces()) + + return true + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return false } + /** * Create a cluster * @param argv @@ -66,13 +127,16 @@ export class ClusterCommand extends BaseCommand { let clusterName = argv.clusterName let clusters = await this.getClusters() + this.logger.showUser(chalk.cyan('> checking cluster:'), chalk.yellow(`${clusterName}`)) if (!clusters.includes(clusterName)) { - this.logger.showUser(chalk.cyan('Creating cluster:'), chalk.yellow(`${clusterName}...`)) + this.logger.showUser(chalk.cyan('> creating cluster:'), chalk.yellow(`${clusterName} ...`)) await this.kind.createCluster( `-n ${clusterName}`, `--config ${core.constants.RESOURCES_DIR}/dev-cluster.yaml`, ) - this.logger.showUser(chalk.green('Created cluster:'), chalk.yellow(clusterName)) + this.logger.showUser(chalk.green('OK'), `cluster '${clusterName}' is created`) + } else { + this.logger.showUser(chalk.green('OK'), `cluster '${clusterName}' already exists`) } // show all clusters and cluster-info @@ -80,6 +144,29 @@ export class ClusterCommand extends BaseCommand { await this.getClusterInfo(argv) + await this.createNamespace(argv) + + return true + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return false + } + + async deleteNamespace(argv) { + try { + let namespace = argv.namespace + let namespaces = await this.getNameSpaces() + if (namespaces.includes(namespace)) { + this.logger.showUser(chalk.cyan('Deleting namespace:'), chalk.yellow(`${namespaces}...`)) + await this.kubectl.deleteNamespace(namespace) + this.logger.showUser(chalk.green('Deleted namespace:'), chalk.yellow(namespaces)) + } + + this.showList("namespaces", await this.getNameSpaces()) + return true } catch (e) { this.logger.error("%s", e) @@ -98,14 +185,16 @@ export class ClusterCommand extends BaseCommand { try { let clusterName = argv.clusterName let clusters = await this.getClusters() + this.logger.showUser(chalk.cyan('> checking cluster:'), chalk.yellow(`${clusterName}`)) if (clusters.includes(clusterName)) { - this.logger.showUser(chalk.cyan('Deleting cluster:'), chalk.yellow(`${clusterName}...`)) + this.logger.showUser(chalk.cyan('> deleting cluster:'), chalk.yellow(`${clusterName} ...`)) await this.kind.deleteCluster(clusterName) - await this.showClusterList() + this.logger.showUser(chalk.green('OK'), `cluster '${clusterName}' is deleted`) } else { - this.logger.showUser(`Cluster '${clusterName}' does not exist`) + this.logger.showUser(chalk.green('OK'), `cluster '${clusterName}' is already deleted`) } + this.showList('clusters', await this.getClusters()) return true } catch (e) { @@ -123,12 +212,7 @@ export class ClusterCommand extends BaseCommand { async getInstalledCharts(argv) { try { let namespaceName = argv.namespace - let cmd = `helm list -n ${namespaceName} -q` - - let output = await this.run(cmd) - this.logger.showUser("\nList of installed charts\n--------------------------\n%s", output) - - return output.split(/\r?\n/) + return await this.helm.list(`-n ${namespaceName}`, '-q') } catch (e) { this.logger.error("%s", e) this.logger.showUser(e.message) @@ -137,6 +221,10 @@ export class ClusterCommand extends BaseCommand { return [] } + async showInstalledChartList(argv) { + this.showList("charts installed", await this.getInstalledCharts(argv) ) + } + /** * Setup cluster with shared components * @param argv @@ -145,27 +233,32 @@ export class ClusterCommand extends BaseCommand { async setup(argv) { try { let clusterName = argv.clusterName - let releaseName = "fullstack-cluster-setup" + + // create cluster + await this.create(argv) + + let chartName = "fullstack-cluster-setup" let namespaceName = argv.namespace - let chartPath = `${core.constants.FST_HOME_DIR}/full-stack-testing/charts/fullstack-cluster-setup` + let chartPath = `${core.constants.FST_HOME_DIR}/full-stack-testing/charts/${chartName}` - this.logger.showUser(chalk.cyan(`Setting up cluster ${clusterName}...`)) + this.logger.showUser(chalk.cyan('> setting up cluster:'), chalk.yellow(`${clusterName}`)) let charts= await this.getInstalledCharts(argv) - - if (!charts.includes(releaseName)) { + if (!charts.includes(chartName)) { // install fullstack-cluster-setup chart - let cmd = `helm install -n ${namespaceName} ${releaseName} ${chartPath}` - this.logger.showUser(chalk.cyan("Installing fullstack-cluster-setup chart")) - this.logger.debug(`Invoking '${cmd}'...`) - - let output = await this.run(cmd) - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is installed`) + this.logger.showUser(chalk.cyan('> installing chart:'), chalk.yellow(`${chartName}`)) + await this.helm.dependency('update', chartPath) + await this.helm.install( + `-n ${namespaceName}`, + chartName, + chartPath, + ) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is installed`) } else { - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is already installed`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already installed`) } - this.logger.showUser(chalk.yellow("Chart setup complete")) + await this.showInstalledChartList(argv) return true } catch (e) { @@ -191,6 +284,7 @@ export class ClusterCommand extends BaseCommand { desc: 'Create a cluster', builder: yargs => { yargs.option('cluster-name', flags.clusterNameFlag) + yargs.option('namespace', flags.namespaceFlag) }, handler: argv => { clusterCmd.logger.debug("==== Running 'cluster create' ===") diff --git a/fullstack-network-manager/src/core/helm.mjs b/fullstack-network-manager/src/core/helm.mjs new file mode 100644 index 000000000..46409fde1 --- /dev/null +++ b/fullstack-network-manager/src/core/helm.mjs @@ -0,0 +1,43 @@ +import {ShellRunner} from "./shell_runner.mjs"; + +export class Helm extends ShellRunner { + /** + * Prepare a `helm` shell command string + * @param action represents a helm command (e.g. create | install | get ) + * @param args args of the command + * @returns {string} + */ + prepareCommand(action, ...args) { + let cmd = `helm ${action} ` + args.forEach(arg => {cmd += ` ${arg}`}) + return cmd + } + + /** + * Invoke `helm install` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async install(...args) { + return this.run(this.prepareCommand('install', ...args)) + } + + /** + * Invoke `helm list` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async list(...args) { + return this.run(this.prepareCommand('list', ...args)) + } + + /** + * Invoke `helm dependency` command + * @param subCommand sub-command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async dependency(subCommand,...args) { + return this.run(this.prepareCommand('dependency', subCommand, ...args)) + } +} \ No newline at end of file diff --git a/fullstack-network-manager/src/core/index.mjs b/fullstack-network-manager/src/core/index.mjs index a509cff01..f75019e41 100644 --- a/fullstack-network-manager/src/core/index.mjs +++ b/fullstack-network-manager/src/core/index.mjs @@ -1,5 +1,8 @@ import * as logging from './logging.mjs' import {constants} from './constants.mjs' +import {Kind} from './kind.mjs' +import {Helm} from './helm.mjs' +import {Kubectl} from "./kubectl.mjs"; // Expose components from the core module -export {logging, constants} +export {logging, constants, Kind, Helm, Kubectl} diff --git a/fullstack-network-manager/src/core/kubectl.mjs b/fullstack-network-manager/src/core/kubectl.mjs new file mode 100644 index 000000000..1ffe5bdb3 --- /dev/null +++ b/fullstack-network-manager/src/core/kubectl.mjs @@ -0,0 +1,72 @@ +import {ShellRunner} from "./shell_runner.mjs"; + +export class Kubectl extends ShellRunner { + /** + * Prepare a `kubectl` shell command string + * @param action represents a helm command (e.g. create | install | get ) + * @param args args of the command + * @returns {string} + */ + prepareCommand(action, ...args) { + let cmd = `kubectl ${action} ` + args.forEach(arg => {cmd += ` ${arg}`}) + return cmd + } + + /** + * Invoke `kubectl create` command + * @param resource a kubernetes resource type (e.g. pod | svc etc.) + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async create(resource, ...args) { + return this.run(this.prepareCommand('create', resource, ...args)) + } + + /** + * Invoke `kubectl create ns` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async createNamespace(...args) { + return this.run(this.prepareCommand('create', 'ns', ...args)) + } + + /** + * Invoke `kubectl delete` command + * @param resource a kubernetes resource type (e.g. pod | svc etc.) + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async delete(resource, ...args) { + return this.run(this.prepareCommand('delete', resource, ...args)) + } + + /** + * Invoke `kubectl delete ns` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async deleteNamespace(...args) { + return this.run(this.prepareCommand('delete', 'ns', ...args)) + } + + /** + * Invoke `kubectl get` command + * @param resource a kubernetes resource type (e.g. pod | svc etc.) + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async get(resource, ...args) { + return this.run(this.prepareCommand('get', resource, ...args)) + } + + /** + * Invoke `kubectl get ns` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async getNamespace(...args) { + return this.run(this.prepareCommand('get', 'ns', ...args)) + } +} \ No newline at end of file diff --git a/fullstack-network-manager/src/core/shell_runner.mjs b/fullstack-network-manager/src/core/shell_runner.mjs index cfacd628d..f1e3349ff 100644 --- a/fullstack-network-manager/src/core/shell_runner.mjs +++ b/fullstack-network-manager/src/core/shell_runner.mjs @@ -2,7 +2,7 @@ import {spawn} from "child_process"; export class ShellRunner { constructor(opts) { - if (!opts || !opts.logger === undefined) throw new Error("logger must be provided") + if (!opts || !opts.logger === undefined) throw new Error("An instance of core/Logger is required") this.logger = opts.logger } diff --git a/fullstack-network-manager/src/index.mjs b/fullstack-network-manager/src/index.mjs index d36e630dc..f13457434 100644 --- a/fullstack-network-manager/src/index.mjs +++ b/fullstack-network-manager/src/index.mjs @@ -5,8 +5,15 @@ import * as core from './core/index.mjs' export function main(argv) { const logger = core.logging.NewLogger('debug') + const kind = new core.Kind({logger: logger}) + const helm = new core.Helm({logger: logger}) + const kubectl= new core.Kubectl({logger: logger}) + const opts = { - logger: logger + logger: logger, + kind: kind, + helm: helm, + kubectl: kubectl, } logger.debug("Constants: %s", JSON.stringify(core.constants)) diff --git a/fullstack-network-manager/test/commands/base.test.js b/fullstack-network-manager/test/commands/base.test.js index 7f4474723..0bfb99dfc 100644 --- a/fullstack-network-manager/test/commands/base.test.js +++ b/fullstack-network-manager/test/commands/base.test.js @@ -2,11 +2,16 @@ import {test, expect, it, describe} from "@jest/globals"; import {logging} from "../../src/core/index.mjs"; import {BaseCommand} from "../../src/commands/base.mjs"; import * as core from "../../src/core/index.mjs" +import {Kind} from "../../src/core/kind.mjs"; const testLogger = logging.NewLogger("debug") describe('BaseCommand', () => { - const baseCmd = new BaseCommand({logger: testLogger}) + const kind = new Kind({logger: testLogger}) + const baseCmd = new BaseCommand({ + logger: testLogger, + kind: kind, + }) describe('runShell', () => { it('should fail during invalid program check', async() => { From 5de115c95baee3a1d1d05d1547396b62463f4a39 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Wed, 1 Nov 2023 16:55:40 +1100 Subject: [PATCH 2/5] fix: cleanup charts command Signed-off-by: Lenin Mehedy --- .../src/commands/base.mjs | 95 +++++++------------ .../src/commands/chart.mjs | 2 +- .../src/commands/cluster.mjs | 80 +++------------- fullstack-network-manager/src/core/helm.mjs | 18 ++++ .../src/core/logging.mjs | 5 + .../src/core/shell_runner.mjs | 25 ++++- 6 files changed, 93 insertions(+), 132 deletions(-) diff --git a/fullstack-network-manager/src/commands/base.mjs b/fullstack-network-manager/src/commands/base.mjs index 3e91b9064..765de5746 100644 --- a/fullstack-network-manager/src/commands/base.mjs +++ b/fullstack-network-manager/src/commands/base.mjs @@ -10,12 +10,13 @@ export class BaseCommand extends ShellRunner { this.logger.debug(cmd) await this.run(cmd) } catch (e) { - this.logger.error("%s", e) + this.logger.showUserError(err) return false } return true } + /** * Check if 'kind' CLI program is installed or not * @returns {Promise} @@ -77,105 +78,79 @@ export class BaseCommand extends ShellRunner { */ async getInstalledCharts(namespaceName) { try { - let cmd = `helm list -n ${namespaceName} -q` - - let output = await this.run(cmd) - this.logger.showUser("\nList of installed charts\n--------------------------\n%s", output) - - return output.split(/\r?\n/) + return await this.helm.list(`-n ${namespaceName}`, '-q') } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return [] } - async chartInstall(namespaceName, releaseName, chartPath, valuesArg) { + async chartInstall(namespaceName, chartName, chartPath, valuesArg = '') { try { - this.logger.showUser(chalk.cyan(`Setting up FST network...`)) - - let charts= await this.getInstalledCharts(namespaceName) - if (!charts.includes(releaseName)) { - let cmd = `helm install -n ${namespaceName} ${releaseName} ${chartPath} ${valuesArg}` - this.logger.showUser(chalk.cyan(`Installing ${releaseName} chart`)) - - let output = await this.run(cmd) - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is installed`) + this.logger.showUser(chalk.cyan('> checking chart:'), chalk.yellow(`${chartName}`)) + let charts = await this.getInstalledCharts(namespaceName) + if (!charts.includes(chartName)) { + this.logger.showUser(chalk.cyan('> installing chart:'), chalk.yellow(`${chartName}`)) + this.helm.install(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is installed`) } else { - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is already installed`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already installed`) } - this.logger.showUser(chalk.yellow("Chart setup is complete")) - return true } catch (e) { - this.logger.error("%s", e.stack) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false } - async chartUninstall(namespaceName, releaseName) { + async chartUninstall(namespaceName, chartName) { try { - this.logger.showUser(chalk.cyan(`Uninstalling FST network ...`)) - - let charts= await this.getInstalledCharts(namespaceName) - if (charts.includes(releaseName)) { - let cmd = `helm uninstall ${releaseName} -n ${namespaceName}` - this.logger.showUser(chalk.cyan(`Uninstalling ${releaseName} chart`)) - - let output = await this.run(cmd) - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is uninstalled`) - await this.getInstalledCharts(namespaceName) + this.logger.showUser(chalk.cyan('> checking chart:'), chalk.yellow(`${chartName}`)) + let charts = await this.getInstalledCharts(namespaceName) + if (charts.includes(chartName)) { + this.logger.showUser(chalk.cyan('> uninstalling chart:'), chalk.yellow(`${chartName}`)) + this.helm.uninstall(`-n ${namespaceName} ${chartName}`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is uninstalled`) } else { - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is already uninstalled`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already uninstalled`) } - this.logger.showUser(chalk.yellow("Chart uninstallation is complete")) - return true } catch (e) { - this.logger.error("%s", e.stack) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false } - async chartUpgrade(namespaceName, releaseName, chartPath, valuesArg) { + async chartUpgrade(namespaceName, chartName, chartPath, valuesArg = '') { try { - this.logger.showUser(chalk.cyan(`Upgrading FST network deployment chart ...`)) - - let charts= await this.getInstalledCharts(namespaceName) - if (charts.includes(releaseName)) { - let cmd = `helm upgrade ${releaseName} -n ${namespaceName} ${chartPath} ${valuesArg}` - this.logger.showUser(chalk.cyan(`Upgrading ${releaseName} chart`)) - - let output = await this.run(cmd) - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is upgraded`) - await this.getInstalledCharts(namespaceName) - - this.logger.showUser(chalk.yellow("Chart upgrade is complete")) - } else { - this.logger.showUser(chalk.green('OK'), `chart '${releaseName}' is not installed`) - return false - } + this.logger.showUser(chalk.cyan('> upgrading chart:'), chalk.yellow(`${chartName}`)) + this.helm.upgrade(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) + this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is upgraded`) return true } catch (e) { - this.logger.error("%s", e.stack) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false } - constructor(opts) { super(opts); + if (!opts || !opts.kind) throw new Error('An instance of core/Kind is required') + if (!opts || !opts.helm) throw new Error('An instance of core/Helm is required') + if (!opts || !opts.kubectl) throw new Error('An instance of core/Kubectl is required') + + this.kind = opts.kind + this.helm = opts.helm + this.kubectl = opts.kubectl + // map of dependency checks this.checks = new Map() .set(core.constants.KIND, () => this.checkKind()) diff --git a/fullstack-network-manager/src/commands/chart.mjs b/fullstack-network-manager/src/commands/chart.mjs index 9018f1446..517309ac0 100644 --- a/fullstack-network-manager/src/commands/chart.mjs +++ b/fullstack-network-manager/src/commands/chart.mjs @@ -25,7 +25,7 @@ export class ChartCommand extends BaseCommand { let namespace = argv.namespace let valuesArg = this.prepareValuesArg(argv) - await this.run(`helm dependency update ${this.chartPath}`) + await this.helm.dependency('update', this.chartPath) return await this.chartInstall(namespace, this.releaseName, this.chartPath, valuesArg) } diff --git a/fullstack-network-manager/src/commands/cluster.mjs b/fullstack-network-manager/src/commands/cluster.mjs index a56049e13..cd5658533 100644 --- a/fullstack-network-manager/src/commands/cluster.mjs +++ b/fullstack-network-manager/src/commands/cluster.mjs @@ -8,19 +8,6 @@ import {Kind} from "../core/kind.mjs"; * Define the core functionalities of 'cluster' command */ export class ClusterCommand extends BaseCommand { - - constructor(opts) { - super(opts); - - if (!opts || !opts.kind) throw new Error('An instance of core/Kind is required') - if (!opts || !opts.helm) throw new Error('An instance of core/Helm is required') - if (!opts || !opts.kubectl) throw new Error('An instance of core/Kubectl is required') - - this.kind = opts.kind - this.helm = opts.helm - this.kubectl = opts.kubectl - } - /** * List available clusters * @returns {Promise} @@ -29,8 +16,7 @@ export class ClusterCommand extends BaseCommand { try { return await this.kind.getClusters('-q') } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return [] @@ -62,8 +48,7 @@ export class ClusterCommand extends BaseCommand { try { return await this.kubectl.getNamespace(`--no-headers`, `-o name`) } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return [] @@ -86,8 +71,7 @@ export class ClusterCommand extends BaseCommand { this.logger.showUser("\n") return true } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false @@ -110,8 +94,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false @@ -148,8 +131,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false @@ -169,8 +151,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false @@ -198,31 +179,15 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.error("%s", e.stack) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false } - /** - * List available clusters - * @returns {Promise} - */ - async getInstalledCharts(argv) { - try { - let namespaceName = argv.namespace - return await this.helm.list(`-n ${namespaceName}`, '-q') - } catch (e) { - this.logger.error("%s", e) - this.logger.showUser(e.message) - } - - return [] - } - async showInstalledChartList(argv) { - this.showList("charts installed", await this.getInstalledCharts(argv) ) + async showInstalledChartList(namespace) { + this.showList("charts installed", await this.getInstalledCharts(namespace) ) } /** @@ -232,38 +197,21 @@ export class ClusterCommand extends BaseCommand { */ async setup(argv) { try { - let clusterName = argv.clusterName - // create cluster await this.create(argv) + let clusterName = argv.clusterName let chartName = "fullstack-cluster-setup" - let namespaceName = argv.namespace + let namespace = argv.namespace let chartPath = `${core.constants.FST_HOME_DIR}/full-stack-testing/charts/${chartName}` this.logger.showUser(chalk.cyan('> setting up cluster:'), chalk.yellow(`${clusterName}`)) - - let charts= await this.getInstalledCharts(argv) - if (!charts.includes(chartName)) { - // install fullstack-cluster-setup chart - this.logger.showUser(chalk.cyan('> installing chart:'), chalk.yellow(`${chartName}`)) - await this.helm.dependency('update', chartPath) - await this.helm.install( - `-n ${namespaceName}`, - chartName, - chartPath, - ) - this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is installed`) - } else { - this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already installed`) - } - - await this.showInstalledChartList(argv) + await this.chartInstall(namespace, chartName, chartPath) + await this.showInstalledChartList(namespace) return true } catch (e) { - this.logger.error("%s", e.stack) - this.logger.showUser(e.message) + this.logger.showUserError(err) } return false diff --git a/fullstack-network-manager/src/core/helm.mjs b/fullstack-network-manager/src/core/helm.mjs index 46409fde1..9812a1094 100644 --- a/fullstack-network-manager/src/core/helm.mjs +++ b/fullstack-network-manager/src/core/helm.mjs @@ -22,6 +22,24 @@ export class Helm extends ShellRunner { return this.run(this.prepareCommand('install', ...args)) } + /** + * Invoke `helm uninstall` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async uninstall(...args) { + return this.run(this.prepareCommand('uninstall', ...args)) + } + + /** + * Invoke `helm upgrade` command + * @param args args of the command + * @returns {Promise} console output as an array of strings + */ + async upgrade(...args) { + return this.run(this.prepareCommand('upgrade', ...args)) + } + /** * Invoke `helm list` command * @param args args of the command diff --git a/fullstack-network-manager/src/core/logging.mjs b/fullstack-network-manager/src/core/logging.mjs index 927d2a4e4..e2093c467 100644 --- a/fullstack-network-manager/src/core/logging.mjs +++ b/fullstack-network-manager/src/core/logging.mjs @@ -2,6 +2,7 @@ import * as winston from 'winston' import {constants} from "./constants.mjs"; import {v4 as uuidv4} from 'uuid'; import * as util from "util"; +import chalk from "chalk"; const customFormat = winston.format.combine( winston.format.label({label: 'FST', message: false}), @@ -92,6 +93,10 @@ const Logger = class { showUser(msg, ...args) { console.log(util.format(msg, ...args)) } + showUserError(err) { + console.log(chalk.red(err.message)) + console.log(err.stack) + } critical(msg, ...args) { this.winsonLogger.crit(msg, ...args, this.prepMeta()) diff --git a/fullstack-network-manager/src/core/shell_runner.mjs b/fullstack-network-manager/src/core/shell_runner.mjs index f1e3349ff..f00c2412b 100644 --- a/fullstack-network-manager/src/core/shell_runner.mjs +++ b/fullstack-network-manager/src/core/shell_runner.mjs @@ -1,4 +1,5 @@ import {spawn} from "child_process"; +import chalk from "chalk"; export class ShellRunner { constructor(opts) { @@ -15,8 +16,6 @@ export class ShellRunner { const self = this return new Promise((resolve, reject) => { - self.logger.debug(cmd) - const child = spawn(cmd, { shell: true, }) @@ -31,16 +30,32 @@ export class ShellRunner { }) }) + let errOutput= [] + child.stderr.on('data', d => { + let items = d.toString().split(/\r?\n/) + items.forEach(item => { + if (item) { + errOutput.push(item) + } + }) + }) + + const errTrace = function(err, messages = []) { + errOutput.forEach(m => self.logger.showUser(chalk.red(m))) + reject(err, errOutput) + } child.on('error', err => { - reject(err) + errTrace(err) }) - child.on('close', (code, signal) => { + child.on('exit', (code, signal) => { if (code) { - reject(new Error(`Command exit with error code: ${code}`)) + let err = new Error(`Command exit with error code: ${code}`) + errTrace(err, errOutput) } + self.logger.debug(cmd, {'commandExitCode': code, 'commandExitSignal': signal, 'commandOutput': output}) resolve(output) }) }) From 83b7f9250445700ed04dd5015d155a4cc4ae0be6 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Wed, 1 Nov 2023 17:08:05 +1100 Subject: [PATCH 3/5] fix: tests Signed-off-by: Lenin Mehedy --- fullstack-network-manager/test/commands/base.test.js | 6 +++++- fullstack-network-manager/test/commands/init.test.js | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/fullstack-network-manager/test/commands/base.test.js b/fullstack-network-manager/test/commands/base.test.js index 0bfb99dfc..559f01617 100644 --- a/fullstack-network-manager/test/commands/base.test.js +++ b/fullstack-network-manager/test/commands/base.test.js @@ -1,5 +1,5 @@ import {test, expect, it, describe} from "@jest/globals"; -import {logging} from "../../src/core/index.mjs"; +import {Helm, Kubectl, logging} from "../../src/core/index.mjs"; import {BaseCommand} from "../../src/commands/base.mjs"; import * as core from "../../src/core/index.mjs" import {Kind} from "../../src/core/kind.mjs"; @@ -8,9 +8,13 @@ const testLogger = logging.NewLogger("debug") describe('BaseCommand', () => { const kind = new Kind({logger: testLogger}) + const helm = new Helm({logger: testLogger}) + const kubectl = new Kubectl({logger: testLogger}) const baseCmd = new BaseCommand({ logger: testLogger, kind: kind, + helm: helm, + kubectl: kubectl, }) describe('runShell', () => { diff --git a/fullstack-network-manager/test/commands/init.test.js b/fullstack-network-manager/test/commands/init.test.js index 3d25005e4..62acbfc20 100644 --- a/fullstack-network-manager/test/commands/init.test.js +++ b/fullstack-network-manager/test/commands/init.test.js @@ -1,10 +1,20 @@ import {InitCommand} from "../../src/commands/init.mjs"; import {expect, describe, it} from "@jest/globals"; import * as core from "../../src/core/index.mjs"; +import {Helm, Kind, Kubectl} from "../../src/core/index.mjs"; +import {BaseCommand} from "../../src/commands/base.mjs"; const testLogger = core.logging.NewLogger('debug') describe('InitCommand', () => { - const initCmd = new InitCommand({logger: testLogger}) + const kind = new Kind({logger: testLogger}) + const helm = new Helm({logger: testLogger}) + const kubectl = new Kubectl({logger: testLogger}) + const initCmd = new InitCommand({ + logger: testLogger, + kind: kind, + helm: helm, + kubectl: kubectl, + }) describe('commands', () => { it('init execution should succeed', async () => { From 8186bc4e40b047b7ae6f6c32c9455fd955a127e4 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Thu, 2 Nov 2023 06:56:49 +1100 Subject: [PATCH 4/5] fix: capture error stack while running shell command Signed-off-by: Lenin Mehedy --- .../src/core/shell_runner.mjs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/fullstack-network-manager/src/core/shell_runner.mjs b/fullstack-network-manager/src/core/shell_runner.mjs index f00c2412b..1705e9312 100644 --- a/fullstack-network-manager/src/core/shell_runner.mjs +++ b/fullstack-network-manager/src/core/shell_runner.mjs @@ -14,6 +14,7 @@ export class ShellRunner { */ async run(cmd) { const self = this + let callStack= new Error().stack // capture the callstack to be included in error return new Promise((resolve, reject) => { const child = spawn(cmd, { @@ -40,19 +41,17 @@ export class ShellRunner { }) }) - const errTrace = function(err, messages = []) { - errOutput.forEach(m => self.logger.showUser(chalk.red(m))) - reject(err, errOutput) - } - - child.on('error', err => { - errTrace(err) - }) child.on('exit', (code, signal) => { if (code) { let err = new Error(`Command exit with error code: ${code}`) - errTrace(err, errOutput) + + // include the callStack to the parent run() instead of from inside this handler. + // this is needed to ensure we capture the proper callstack for easier debugging. + err.stack = callStack + + errOutput.forEach(m => self.logger.showUser(chalk.red(m))) + reject(err) } self.logger.debug(cmd, {'commandExitCode': code, 'commandExitSignal': signal, 'commandOutput': output}) From 2c24fa0c9c00a378764f74f5fde578ee72bb5c63 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Thu, 2 Nov 2023 06:57:19 +1100 Subject: [PATCH 5/5] fix: use correct error variable name and code cleanup Signed-off-by: Lenin Mehedy --- .../src/commands/base.mjs | 20 +++++----- .../src/commands/chart.mjs | 9 ++--- .../src/commands/cluster.mjs | 38 +++++-------------- 3 files changed, 24 insertions(+), 43 deletions(-) diff --git a/fullstack-network-manager/src/commands/base.mjs b/fullstack-network-manager/src/commands/base.mjs index 765de5746..ebd010685 100644 --- a/fullstack-network-manager/src/commands/base.mjs +++ b/fullstack-network-manager/src/commands/base.mjs @@ -10,7 +10,7 @@ export class BaseCommand extends ShellRunner { this.logger.debug(cmd) await this.run(cmd) } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) return false } @@ -80,7 +80,7 @@ export class BaseCommand extends ShellRunner { try { return await this.helm.list(`-n ${namespaceName}`, '-q') } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return [] @@ -88,11 +88,13 @@ export class BaseCommand extends ShellRunner { async chartInstall(namespaceName, chartName, chartPath, valuesArg = '') { try { - this.logger.showUser(chalk.cyan('> checking chart:'), chalk.yellow(`${chartName}`)) let charts = await this.getInstalledCharts(namespaceName) if (!charts.includes(chartName)) { + this.logger.showUser(chalk.cyan('> running helm dependency update for chart:'), chalk.yellow(`${chartName} ...`)) + await this.helm.dependency('update', chartPath) + this.logger.showUser(chalk.cyan('> installing chart:'), chalk.yellow(`${chartName}`)) - this.helm.install(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) + await this.helm.install(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is installed`) } else { this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already installed`) @@ -100,7 +102,7 @@ export class BaseCommand extends ShellRunner { return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -112,7 +114,7 @@ export class BaseCommand extends ShellRunner { let charts = await this.getInstalledCharts(namespaceName) if (charts.includes(chartName)) { this.logger.showUser(chalk.cyan('> uninstalling chart:'), chalk.yellow(`${chartName}`)) - this.helm.uninstall(`-n ${namespaceName} ${chartName}`) + await this.helm.uninstall(`-n ${namespaceName} ${chartName}`) this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is uninstalled`) } else { this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is already uninstalled`) @@ -120,7 +122,7 @@ export class BaseCommand extends ShellRunner { return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -129,12 +131,12 @@ export class BaseCommand extends ShellRunner { async chartUpgrade(namespaceName, chartName, chartPath, valuesArg = '') { try { this.logger.showUser(chalk.cyan('> upgrading chart:'), chalk.yellow(`${chartName}`)) - this.helm.upgrade(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) + await this.helm.upgrade(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is upgraded`) return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false diff --git a/fullstack-network-manager/src/commands/chart.mjs b/fullstack-network-manager/src/commands/chart.mjs index 517309ac0..b7e10c457 100644 --- a/fullstack-network-manager/src/commands/chart.mjs +++ b/fullstack-network-manager/src/commands/chart.mjs @@ -6,7 +6,7 @@ import * as flags from "./flags.mjs"; export class ChartCommand extends BaseCommand { chartPath = `${core.constants.FST_HOME_DIR}/full-stack-testing/charts/fullstack-deployment` - releaseName = "fullstack-deployment" + chartName = "fullstack-deployment" prepareValuesArg(argv) { let {valuesFile, mirrorNode, hederaExplorer} = argv @@ -25,21 +25,20 @@ export class ChartCommand extends BaseCommand { let namespace = argv.namespace let valuesArg = this.prepareValuesArg(argv) - await this.helm.dependency('update', this.chartPath) - return await this.chartInstall(namespace, this.releaseName, this.chartPath, valuesArg) + return await this.chartInstall(namespace, this.chartName, this.chartPath, valuesArg) } async uninstall(argv) { let namespace = argv.namespace - return await this.chartUninstall(namespace, this.releaseName) + return await this.chartUninstall(namespace, this.chartName) } async upgrade(argv) { let namespace = argv.namespace let valuesArg = this.prepareValuesArg(argv) - return await this.chartUpgrade(namespace, this.releaseName, this.chartPath, valuesArg) + return await this.chartUpgrade(namespace, this.chartName, this.chartPath, valuesArg) } static getCommandDefinition(chartCmd) { diff --git a/fullstack-network-manager/src/commands/cluster.mjs b/fullstack-network-manager/src/commands/cluster.mjs index cd5658533..36dac7f9f 100644 --- a/fullstack-network-manager/src/commands/cluster.mjs +++ b/fullstack-network-manager/src/commands/cluster.mjs @@ -16,7 +16,7 @@ export class ClusterCommand extends BaseCommand { try { return await this.kind.getClusters('-q') } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return [] @@ -48,7 +48,7 @@ export class ClusterCommand extends BaseCommand { try { return await this.kubectl.getNamespace(`--no-headers`, `-o name`) } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return [] @@ -71,7 +71,7 @@ export class ClusterCommand extends BaseCommand { this.logger.showUser("\n") return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -86,7 +86,7 @@ export class ClusterCommand extends BaseCommand { this.logger.showUser(chalk.cyan('> creating namespace:'), chalk.yellow(`${namespace} ...`)) await this.kubectl.createNamespace(namespace) this.logger.showUser(chalk.green('OK'), `namespace '${namespace}' is created`) - } { + } else { this.logger.showUser(chalk.green('OK'), `namespace '${namespace}' already exists`) } @@ -94,7 +94,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -131,27 +131,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.showUserError(err) - } - - return false - } - - async deleteNamespace(argv) { - try { - let namespace = argv.namespace - let namespaces = await this.getNameSpaces() - if (namespaces.includes(namespace)) { - this.logger.showUser(chalk.cyan('Deleting namespace:'), chalk.yellow(`${namespaces}...`)) - await this.kubectl.deleteNamespace(namespace) - this.logger.showUser(chalk.green('Deleted namespace:'), chalk.yellow(namespaces)) - } - - this.showList("namespaces", await this.getNameSpaces()) - - return true - } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -179,7 +159,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false @@ -187,7 +167,7 @@ export class ClusterCommand extends BaseCommand { async showInstalledChartList(namespace) { - this.showList("charts installed", await this.getInstalledCharts(namespace) ) + this.showList("charts installed", await this.getInstalledCharts(namespace)) } /** @@ -211,7 +191,7 @@ export class ClusterCommand extends BaseCommand { return true } catch (e) { - this.logger.showUserError(err) + this.logger.showUserError(e) } return false