From 78be8233690249067e986813f36e669613889c01 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Thu, 26 Oct 2023 11:40:11 +1100 Subject: [PATCH] feat(cli): implement cluster create and delete commands (#446) Signed-off-by: Lenin Mehedy --- .../resources/dev-cluster.yaml | 7 ++ .../src/commands/base.mjs | 57 +++------ .../src/commands/cluster.mjs | 116 ++++++++++++++++-- .../src/commands/init.mjs | 18 ++- .../src/core/constants.mjs | 8 ++ .../src/core/logging.mjs | 109 ++++++++++------ fullstack-network-manager/src/index.mjs | 6 +- 7 files changed, 230 insertions(+), 91 deletions(-) create mode 100644 fullstack-network-manager/resources/dev-cluster.yaml diff --git a/fullstack-network-manager/resources/dev-cluster.yaml b/fullstack-network-manager/resources/dev-cluster.yaml new file mode 100644 index 000000000..d28b2dc9a --- /dev/null +++ b/fullstack-network-manager/resources/dev-cluster.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: fst # this is overridden if CLUSTER_NAME env var is set. Check .env file +nodes: + - role: control-plane + labels: + fullstack-scheduling.io/role: network \ No newline at end of file diff --git a/fullstack-network-manager/src/commands/base.mjs b/fullstack-network-manager/src/commands/base.mjs index 669e78652..bdbe52880 100644 --- a/fullstack-network-manager/src/commands/base.mjs +++ b/fullstack-network-manager/src/commands/base.mjs @@ -1,17 +1,12 @@ "use strict" import {exec} from "child_process"; import * as core from "../core/index.mjs" +import chalk from "chalk"; export const BaseCommand = class BaseCommand { - /** - * Check if 'kind' CLI program is installed or not - * @returns {Promise} - */ - async checkKind() { + async checkDep(dep) { try { - this.logger.debug("Checking if 'kind' is installed") - await this.runExec("kind --version") - this.logger.debug("OK: 'kind' is installed") + await this.runExec(dep) } catch (e) { this.logger.error("%s", e) return false @@ -19,22 +14,20 @@ export const BaseCommand = class BaseCommand { return true } + /** + * Check if 'kind' CLI program is installed or not + * @returns {Promise} + */ + async checkKind() { + return this.checkDep(core.constants.KIND) + } /** * Check if 'helm' CLI program is installed or not * @returns {Promise} */ async checkHelm() { - try { - this.logger.debug("Checking if 'helm' is installed") - await this.runExec("helm version") - this.logger.debug("OK: 'helm' is installed") - } catch (e) { - this.logger.error("%s", e) - return false - } - - return true + return this.checkDep(core.constants.HELM) } /** @@ -42,16 +35,7 @@ export const BaseCommand = class BaseCommand { * @returns {Promise} */ async checkKubectl() { - try { - this.logger.debug("Checking if 'kubectl' is installed") - await this.runExec("kubectl version") - this.logger.debug("OK: 'kubectl' is installed") - } catch (e) { - this.logger.error("%s", e) - return false - } - - return true + return this.checkDep(core.constants.KUBECTL) } /** @@ -60,29 +44,28 @@ export const BaseCommand = class BaseCommand { * @returns {Promise} */ async checkDependencies(deps = []) { - this.logger.info("Checking for required dependencies: %s", deps) + this.logger.debug("Checking for required dependencies: %s", deps) for (let i = 0; i < deps.length; i++) { let dep = deps[i] this.logger.debug("Checking for dependency '%s'", dep) + let status = false let check = this.checks.get(dep) - if (!check) { - this.logger.error("FAIL: Dependency '%s' is unknown", dep) - return false + if (check) { + status = await check() } - - let status = await check() if (!status) { - this.logger.error("FAIL: Dependency '%s' is not found", dep) + this.logger.showUser(chalk.red(`FAIL: '${dep}' is not found`)) return false } - this.logger.debug("PASS: Dependency '%s' is found", dep) + this.logger.showUser(chalk.green(`OK: '${dep}' is found`)) } - this.logger.info("PASS: All required dependencies are found: %s", deps) + this.logger.debug("All required dependencies are found: %s", deps) + return true } diff --git a/fullstack-network-manager/src/commands/cluster.mjs b/fullstack-network-manager/src/commands/cluster.mjs index 3f09d78d8..4439a2407 100644 --- a/fullstack-network-manager/src/commands/cluster.mjs +++ b/fullstack-network-manager/src/commands/cluster.mjs @@ -1,5 +1,6 @@ import * as core from '../core/index.mjs' import {BaseCommand} from "./base.mjs"; +import chalk from "chalk"; /** * Flags for 'cluster' command @@ -15,22 +16,94 @@ const clusterNameFlag = { * Define the core functionalities of 'cluster' command */ export const ClusterCommand = class extends BaseCommand { + + /** + * List available clusters + * @returns {Promise} + */ + async getClusters() { + let cmd = `kind get clusters` + + try { + let output = await this.runExec(cmd) + this.logger.showUser("\nList of available clusters \n--------------------------\n%s", output) + return true + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return false + } + + /** + * Get cluster-info for the given cluster name + * @param argv arguments containing cluster name + * @returns {Promise} + */ + async getClusterInfo(argv) { + let cmd = `kubectl cluster-info --context kind-${argv.name}` + + try { + let output = await this.runExec(cmd) + this.logger.showUser(output) + return true + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return false + } + /** * Create a cluster * @param argv - * @returns {Promise} + * @returns {Promise} */ async create(argv) { - this.logger.info("creating cluster '%s'", argv.name) + let cmd = `kind create cluster -n ${argv.name} --config ${core.constants.RESOURCES_DIR}/dev-cluster.yaml` + + try { + this.logger.showUser(chalk.cyan('Creating cluster:'), chalk.yellow(`${argv.name}...`)) + this.logger.debug(`Invoking '${cmd}'...`) + let output = await this.runExec(cmd) + this.logger.debug(output) + this.logger.showUser(chalk.green('Created cluster:'), chalk.yellow(argv.name)) + + // show all clusters and cluster-info + await this.getClusters() + await this.getClusterInfo(argv) + + return true + } catch (e) { + this.logger.error("%s", e) + this.logger.showUser(e.message) + } + + return false } /** * Delete a cluster * @param argv - * @returns {Promise} + * @returns {Promise} */ async delete(argv) { - this.logger.info("deleting cluster '%s'", argv.name, {name: argv.name}) + let cmd = `kind delete cluster -n ${argv.name}` + try { + this.logger.debug(`Invoking '${cmd}'...`) + this.logger.showUser(chalk.cyan('Deleting cluster:'), chalk.yellow(`${argv.name}...`)) + await this.runExec(cmd) + await this.getClusters() + + return true + } catch (e) { + this.logger.error("%s", e.stack) + this.logger.showUser(e.message) + } + + return false } /** @@ -40,27 +113,52 @@ export const ClusterCommand = class extends BaseCommand { static getCommandDefinition(clusterCmd) { return { command: 'cluster', - desc: 'Manager FST cluster', + desc: 'Manage FST cluster', builder: yargs => { return yargs .command({ command: 'create', - desc: 'Create FST cluster', + desc: 'Create a cluster', builder: yargs => { yargs.option('name', clusterNameFlag) }, handler: argv => { - clusterCmd.create(argv).then() + clusterCmd.create(argv).then(r => { + if (!r) process.exit(1) + }) } }) .command({ command: 'delete', - desc: 'Delete FST cluster', + desc: 'Delete a cluster', + builder: yargs => { + yargs.option('name', clusterNameFlag) + }, + handler: argv => { + clusterCmd.delete(argv).then(r => { + if (!r) process.exit(1) + }) + } + }) + .command({ + command: 'list', + desc: 'List all clusters', + handler: argv => { + clusterCmd.getClusters().then(r => { + if (!r) process.exit(1) + }) + } + }) + .command({ + command: 'info', + desc: 'Get cluster info', builder: yargs => { yargs.option('name', clusterNameFlag) }, handler: argv => { - clusterCmd.delete(argv).then() + clusterCmd.getClusterInfo(argv).then(r => { + if (!r) process.exit(1) + }) } }) .demand(1, 'Select a cluster command') diff --git a/fullstack-network-manager/src/commands/init.mjs b/fullstack-network-manager/src/commands/init.mjs index c5e1d5226..66d6a00af 100644 --- a/fullstack-network-manager/src/commands/init.mjs +++ b/fullstack-network-manager/src/commands/init.mjs @@ -1,5 +1,6 @@ import {BaseCommand} from "./base.mjs"; import * as core from "../core/index.mjs" +import chalk from "chalk"; /** * Defines the core functionalities of 'init' command @@ -10,11 +11,20 @@ export const InitCommand = class extends BaseCommand { * @returns {Promise} */ async init() { - return await this.checkDependencies([ + let deps = [ core.constants.HELM, core.constants.KIND, core.constants.KUBECTL, - ]) + ] + + let status = await this.checkDependencies(deps) + if (!status) { + return false + } + + this.logger.showUser(chalk.green("OK: All required dependencies are found: %s"), chalk.yellow(deps)) + + return status } /** @@ -27,7 +37,9 @@ export const InitCommand = class extends BaseCommand { desc: "Perform dependency checks and initialize local environment", builder: {}, handler: (argv) => { - initCmd.init(argv) + initCmd.init(argv).then(r => { + if (!r) process.exit(1) + }) } } } diff --git a/fullstack-network-manager/src/core/constants.mjs b/fullstack-network-manager/src/core/constants.mjs index 280ccca15..b53733ec0 100644 --- a/fullstack-network-manager/src/core/constants.mjs +++ b/fullstack-network-manager/src/core/constants.mjs @@ -1,3 +1,8 @@ +import {dirname, normalize} from "path" +import {fileURLToPath} from "url" + +// directory of this fle +const CUR_FILE_DIR = dirname(fileURLToPath(import.meta.url)) const USER = `${process.env.USER}` export const constants = { USER: `${USER}`, @@ -5,4 +10,7 @@ export const constants = { HELM: 'helm', KIND: 'kind', KUBECTL: 'kubectl', + CWD: process.cwd(), + FST_HOME_DIR: process.env.HOME + "/.fsnetman", + RESOURCES_DIR: normalize(CUR_FILE_DIR + "/../../resources") } diff --git a/fullstack-network-manager/src/core/logging.mjs b/fullstack-network-manager/src/core/logging.mjs index d6497d632..c03de29c5 100644 --- a/fullstack-network-manager/src/core/logging.mjs +++ b/fullstack-network-manager/src/core/logging.mjs @@ -1,4 +1,6 @@ import * as winston from 'winston' +import {constants} from "./constants.mjs"; +import * as util from "util"; const customFormat = winston.format.combine( winston.format.label({label: 'FST', message: false}), @@ -33,46 +35,73 @@ const customFormat = winston.format.combine( })(), ) -/** - * Create a new logger - * @param level logging level as supported by winston library: - * { - * emerg: 0, - * alert: 1, - * crit: 2, - * error: 3, - * warning: 4, - * notice: 5, - * info: 6, - * debug: 7 - * } - * @returns {winston.Logger} - * @constructor - */ -export function NewLogger(level = 'debug') { - let logger = winston.createLogger({ - level: level, - format: winston.format.combine( - customFormat, - winston.format.json(), - ), - // format: winston.format.json(), - // defaultMeta: { service: 'user-service' }, - transports: [ - // - // - Write all logs with importance level of `error` or less to `error.log` - // - Write all logs with importance level of `info` or less to `combined.log` - // - new winston.transports.File({filename: 'combined.log'}), - new winston.transports.File({filename: 'error.log', level: 'error'}), - ], - }); - - if (process.env.NODE_ENV !== 'production') { - logger.add(new winston.transports.Console({ - format: customFormat, - })); +const Logger = class { + /** + * Create a new logger + * @param level logging level as supported by winston library: + * { + * emerg: 0, + * alert: 1, + * crit: 2, + * error: 3, + * warning: 4, + * notice: 5, + * info: 6, + * debug: 7 + * } + * @constructor + */ + constructor(level) { + this.winsonLogger = winston.createLogger({ + level: level, + format: winston.format.combine( + customFormat, + winston.format.json(), + ), + // format: winston.format.json(), + // defaultMeta: { service: 'user-service' }, + transports: [ + // + // - Write all logs with importance level of `error` or less to `error.log` + // - Write all logs with importance level of `info` or less to `fst.log` + // + new winston.transports.File({filename: `${constants.FST_HOME_DIR}/logs/fst.log`}), + // new winston.transports.File({filename: constants.TMP_DIR + "/logs/error.log", level: 'error'}), + // new winston.transports.Console({format: customFormat}) + ], + }); + } + + showUser(msg, ...args) { + console.log(util.format(msg, ...args)) + } + + critical(msg, ...meta) { + this.winsonLogger.crit(msg, ...meta) + } + + error(msg, ...meta) { + this.winsonLogger.error(msg, ...meta) + console.trace() + } + + warn(msg, ...meta) { + this.winsonLogger.warn(msg, ...meta) } - return logger + notice(msg, ...meta) { + this.winsonLogger.notice(msg, ...meta) + } + + info(msg, ...meta) { + this.winsonLogger.info(msg, ...meta) + } + + debug(msg, ...meta) { + this.winsonLogger.debug(msg, ...meta) + } +} + +export function NewLogger(level = 'debug') { + return new Logger(level) } diff --git a/fullstack-network-manager/src/index.mjs b/fullstack-network-manager/src/index.mjs index 17944ca00..d36e630dc 100644 --- a/fullstack-network-manager/src/index.mjs +++ b/fullstack-network-manager/src/index.mjs @@ -9,13 +9,15 @@ export function main(argv) { logger: logger } + logger.debug("Constants: %s", JSON.stringify(core.constants)) + return yargs(hideBin(argv)) - .usage('Usage: $0 [options]') + .usage(`Usage:\n $0 [options]`) .alias('h', 'help') .alias('v', 'version') .command(commands.Initialize(opts)) .strict() - .wrap(80) + .wrap(120) .demand(1, 'Select a command') .parse() }