From 598efeb7a18990499462ecb84679281d79589b4e Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Fri, 22 Nov 2024 16:24:05 +0200 Subject: [PATCH 1/6] feat: solo context use Signed-off-by: Ivo Yankov --- src/commands/base.ts | 8 + src/commands/context/flags.ts | 30 ++ src/commands/context/handlers.ts | 49 +++ src/commands/context/index.ts | 54 ++++ src/commands/context/tasks.ts | 88 ++++++ src/commands/flags.ts | 34 +- src/commands/index.ts | 3 + src/commands/node/handlers.ts | 15 +- src/commands/node/tasks.ts | 144 +++++---- src/commands/prompts.ts | 460 ++++++++++++++-------------- src/core/config/local_config.ts | 13 +- src/core/helpers.ts | 33 +- src/core/k8.ts | 342 ++++++++++----------- src/core/task.ts | 10 +- src/core/templates.ts | 30 +- src/types/index.ts | 5 + test/test_util.ts | 26 +- test/unit/commands/context.test.ts | 239 +++++++++++++++ test/unit/core/local_config.test.ts | 22 +- 19 files changed, 1055 insertions(+), 550 deletions(-) create mode 100644 src/commands/context/flags.ts create mode 100644 src/commands/context/handlers.ts create mode 100644 src/commands/context/index.ts create mode 100644 src/commands/context/tasks.ts create mode 100644 test/unit/commands/context.test.ts diff --git a/src/commands/base.ts b/src/commands/base.ts index a4c1c0a48..cbe8c819f 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -159,4 +159,12 @@ export class BaseCommand extends ShellRunner { getUnusedConfigs (configName: string): string[] { return this._configMaps.get(configName).getUnusedConfigs() } + + getK8 () { + return this.k8 + } + + getLocalConfig () { + return this.localConfig + } } diff --git a/src/commands/context/flags.ts b/src/commands/context/flags.ts new file mode 100644 index 000000000..0ec7f6751 --- /dev/null +++ b/src/commands/context/flags.ts @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ + +import * as flags from '../flags.js' + +export const DEFAULT_FLAGS = { + requiredFlags: [], + requiredFlagsWithDisabledPrompt: [flags.namespace, flags.cacheDir, flags.releaseTag], + optionalFlags: [flags.devMode] +} + +export const USE_FLAGS = { + requiredFlags: [], + requiredFlagsWithDisabledPrompt: [], + optionalFlags: [flags.devMode, flags.quiet, flags.clusterName, flags.context, flags.force, flags.namespace] +} \ No newline at end of file diff --git a/src/commands/context/handlers.ts b/src/commands/context/handlers.ts new file mode 100644 index 000000000..cdbd26a0c --- /dev/null +++ b/src/commands/context/handlers.ts @@ -0,0 +1,49 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ +import { type BaseCommand } from '../base.js' +import { type ContextCommandTasks } from './tasks.js' +import * as helpers from '../../core/helpers.js' +import { constants } from '../../core/index.js' +import { type CommandHandlers } from '../../types/index.js' +import * as ContextFlags from './flags.js' + +export class ContextCommandHandlers implements CommandHandlers { + readonly parent: BaseCommand + readonly tasks: ContextCommandTasks + + constructor (parent: BaseCommand, tasks: ContextCommandTasks) { + this.parent = parent + this.tasks = tasks + } + + async use (argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS) + + const action = helpers.commandActionBuilder([ + this.tasks.initialize(argv), + this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8(), argv), + this.tasks.updateLocalConfig(argv), + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }, 'context use', null) + + await action(argv, this) + return true + } + +} diff --git a/src/commands/context/index.ts b/src/commands/context/index.ts new file mode 100644 index 000000000..c97df3bca --- /dev/null +++ b/src/commands/context/index.ts @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ + +import { YargsCommand } from '../../core/index.js' +import { BaseCommand } from './../base.js' +import type { Opts } from '../../types/index.js' +import { ContextCommandTasks } from './tasks.js' +import { ContextCommandHandlers } from './handlers.js' +import * as ContextFlags from './flags.js' +import { getPromptMap } from '../prompts.js' + +/** + * Defines the core functionalities of 'node' command + */ +export class ContextCommand extends BaseCommand { + private handlers: ContextCommandHandlers + + constructor (opts: Opts) { + super(opts) + + this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this, getPromptMap())) + } + + getCommandDefinition () { + return { + command: 'context', + desc: 'Manage local and remote configurations', + builder: (yargs: any) => { + return yargs + .command(new YargsCommand({ + command: 'use', + description: 'updates the local configuration by connecting a deployment to a k8s context', + commandDef: this, + handler: 'use' + }, ContextFlags.USE_FLAGS)) + .demandCommand(1, 'Select a context command') + } + } + } +} diff --git a/src/commands/context/tasks.ts b/src/commands/context/tasks.ts new file mode 100644 index 000000000..b8635de15 --- /dev/null +++ b/src/commands/context/tasks.ts @@ -0,0 +1,88 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ +import { + Task, Templates +} from '../../core/index.js' +import * as flags from '../flags.js' +import type { ListrTaskWrapper } from 'listr2' +import { type BaseCommand } from '../base.js' + +export class ContextCommandTasks { + + private readonly parent: BaseCommand + private readonly promptMap: Map + + constructor (parent, promptMap) { + this.parent = parent + this.promptMap = promptMap + } + + updateLocalConfig (argv) { + return new Task('Update local configuration', async (ctx: any, task: ListrTaskWrapper) => { + this.parent.logger.info('Updating local configuration...') + + const isQuiet = !!argv[flags.quiet.name] + const isForcing = !!argv[flags.force.name] + + let currentDeploymentName = argv[flags.namespace.name] + let clusterAliases = Templates.parseClusterAliases(argv[flags.clusterName.name]) + let contextName = argv[flags.context.name] + + if (isQuiet) { + const currentCluster = (await this.parent.getK8().getKubeConfig().getCurrentCluster()).name + if (!clusterAliases.length) clusterAliases = [currentCluster] + if (!contextName) contextName = await this.parent.getK8().getKubeConfig().getCurrentContext() + + // TODO properly get the active namespace + if (!currentDeploymentName) currentDeploymentName = currentCluster + } + else { + if (!clusterAliases.length) clusterAliases = Templates.parseClusterAliases(await (this.promptMap.get(flags.clusterName.name))(task, clusterAliases)) + if (!contextName) contextName = await (this.promptMap.get(flags.context.name))(task, contextName) + if (!currentDeploymentName) currentDeploymentName = await (this.promptMap.get(flags.namespace.name))(task, currentDeploymentName) + } + + // Select current deployment + this.parent.getLocalConfig().setCurrentDeployment(currentDeploymentName) + + // Set clusters for active deployment + const deployments = this.parent.getLocalConfig().deployments + deployments[currentDeploymentName].clusterAliases = clusterAliases + this.parent.getLocalConfig().setDeployments(deployments) + + this.parent.getK8().getKubeConfig().setCurrentContext(contextName) + + this.parent.logger.info('Save LocalConfig file') + await this.parent.getLocalConfig().write() + }) + } + + initialize (argv: any) { + const { requiredFlags, optionalFlags } = argv + + argv.flags = [ + ...requiredFlags, + ...optionalFlags + ] + + return new Task('Initialize', async (ctx: any, task: ListrTaskWrapper) => { + if (argv[flags.devMode.name]) { + this.parent.logger.setDevMode(true) + } + }) + } +} diff --git a/src/commands/flags.ts b/src/commands/flags.ts index 8e3fcb6df..4736574bf 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -718,6 +718,16 @@ export const userEmailAddress: CommandFlag = { } } +export const context: CommandFlag = { + constName: 'contextName', + name: 'context', + definition: { + describe: 'The kind context name to be used', + defaultValue: '', + type: 'string' + } +} + export const deploymentName: CommandFlag = { constName: 'deploymentName', name: 'deployment-name', @@ -745,9 +755,9 @@ export const grpcTlsCertificatePath: CommandFlag = { name: 'grpc-tls-cert', definition: { describe: - 'TLS Certificate path for the gRPC ' + - '(e.g. "node1=/Users/username/node1-grpc.cert" ' + - 'with multiple nodes comma seperated)', + 'TLS Certificate path for the gRPC ' + + '(e.g. "node1=/Users/username/node1-grpc.cert" ' + + 'with multiple nodes comma seperated)', defaultValue: '', type: 'string' } @@ -758,9 +768,9 @@ export const grpcWebTlsCertificatePath: CommandFlag = { name: 'grpc-web-tls-cert', definition: { describe: - 'TLS Certificate path for gRPC Web ' + - '(e.g. "node1=/Users/username/node1-grpc-web.cert" ' + - 'with multiple nodes comma seperated)', + 'TLS Certificate path for gRPC Web ' + + '(e.g. "node1=/Users/username/node1-grpc-web.cert" ' + + 'with multiple nodes comma seperated)', defaultValue: '', type: 'string' } @@ -771,9 +781,9 @@ export const grpcTlsKeyPath: CommandFlag = { name: 'grpc-tls-key', definition: { describe: - 'TLS Certificate key path for the gRPC ' + - '(e.g. "node1=/Users/username/node1-grpc.key" ' + - 'with multiple nodes comma seperated)', + 'TLS Certificate key path for the gRPC ' + + '(e.g. "node1=/Users/username/node1-grpc.key" ' + + 'with multiple nodes comma seperated)', defaultValue: '', type: 'string' } @@ -784,9 +794,9 @@ export const grpcWebTlsKeyPath: CommandFlag = { name: 'grpc-web-tls-key', definition: { describe: - 'TLC Certificate key path for gRPC Web ' + - '(e.g. "node1=/Users/username/node1-grpc-web.key" ' + - 'with multiple nodes comma seperated)', + 'TLC Certificate key path for gRPC Web ' + + '(e.g. "node1=/Users/username/node1-grpc-web.key" ' + + 'with multiple nodes comma seperated)', defaultValue: '', type: 'string' } diff --git a/src/commands/index.ts b/src/commands/index.ts index 3fd2fcc51..270fa427d 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -16,6 +16,7 @@ */ import * as flags from './flags.js' import { ClusterCommand } from './cluster.js' +import { ContextCommand } from './context/index.js' import { InitCommand } from './init.js' import { MirrorNodeCommand } from './mirror_node.js' import { NetworkCommand } from './network.js' @@ -32,6 +33,7 @@ import { type Opts } from '../types/index.js' function Initialize (opts: Opts) { const initCmd = new InitCommand(opts) const clusterCmd = new ClusterCommand(opts) + const contextCmd = new ContextCommand(opts) const networkCommand = new NetworkCommand(opts) const nodeCmd = new NodeCommand(opts) const relayCmd = new RelayCommand(opts) @@ -41,6 +43,7 @@ function Initialize (opts: Opts) { return [ initCmd.getCommandDefinition(), clusterCmd.getCommandDefinition(), + contextCmd.getCommandDefinition(), networkCommand.getCommandDefinition(), nodeCmd.getCommandDefinition(), relayCmd.getCommandDefinition(), diff --git a/src/commands/node/handlers.ts b/src/commands/node/handlers.ts index dda674156..a4ef69d4b 100644 --- a/src/commands/node/handlers.ts +++ b/src/commands/node/handlers.ts @@ -35,8 +35,9 @@ import type { SoloLogger } from '../../core/logging.js' import type { NodeCommand } from './index.js' import type { NodeCommandTasks } from './tasks.js' import { type Lease } from '../../core/lease/lease.js' +import { type CommandHandlers } from '../../types/index.js' -export class NodeCommandHandlers { +export class NodeCommandHandlers implements CommandHandlers { private readonly accountManager: AccountManager private readonly configManager: ConfigManager private readonly platformInstaller: PlatformInstaller @@ -319,20 +320,20 @@ export class NodeCommandHandlers { const action = helpers.commandActionBuilder([ this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, helpers.updateLoadContextParser), - ...this.updateSubmitTransactionsTasks(argv) + ...this.updateSubmitTransactionsTasks(argv) ], { - concurrent: false, - rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION }, 'Error in submitting transactions for node update', lease) - await action(argv, this) - return true + await action(argv, this) + return true } async updateExecute (argv) { const lease = await this.leaseManager.create() argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_EXECUTE_FLAGS) - const action = helpers.commandActionBuilder([ + const action = helpers.commandActionBuilder([ this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, helpers.updateLoadContextParser), ...this.updateExecuteTasks(argv) diff --git a/src/commands/node/tasks.ts b/src/commands/node/tasks.ts index c002afd81..1788b3911 100644 --- a/src/commands/node/tasks.ts +++ b/src/commands/node/tasks.ts @@ -90,7 +90,7 @@ export class NodeCommandTasks { constructor (opts: { logger: SoloLogger; accountManager: AccountManager; configManager: ConfigManager, k8: K8, platformInstaller: PlatformInstaller, keyManager: KeyManager, profileManager: ProfileManager, - chartManager: ChartManager, certificateManager: CertificateManager, parent: NodeCommand} + chartManager: ChartManager, certificateManager: CertificateManager, parent: NodeCommand} ) { if (!opts || !opts.accountManager) throw new IllegalArgumentError('An instance of core/AccountManager is required', opts.accountManager as any) if (!opts || !opts.configManager) throw new Error('An instance of core/ConfigManager is required') @@ -163,12 +163,12 @@ export class NodeCommandTasks { if (start === 0) { fileTransaction = new FileUpdateTransaction() - .setFileId(constants.UPGRADE_FILE_ID) - .setContents(zipBytesChunk) + .setFileId(constants.UPGRADE_FILE_ID) + .setContents(zipBytesChunk) } else { fileTransaction = new FileAppendTransaction() - .setFileId(constants.UPGRADE_FILE_ID) - .setContents(zipBytesChunk) + .setFileId(constants.UPGRADE_FILE_ID) + .setContents(zipBytesChunk) } const resp = await fileTransaction.execute(nodeClient) const receipt = await resp.getReceipt(nodeClient) @@ -240,7 +240,7 @@ export class NodeCommandTasks { } _fetchPlatformSoftware (nodeAliases: NodeAliases, podNames: Record, releaseTag: string, - task: ListrTaskWrapper, platformInstaller: PlatformInstaller + task: ListrTaskWrapper, platformInstaller: PlatformInstaller ) { const subTasks = [] for (const nodeAlias of nodeAliases) { @@ -248,7 +248,7 @@ export class NodeCommandTasks { subTasks.push({ title: `Update node: ${chalk.yellow(nodeAlias)} [ platformVersion = ${releaseTag} ]`, task: async () => - await platformInstaller.fetchPlatform(podName, releaseTag) + await platformInstaller.fetchPlatform(podName, releaseTag) }) } @@ -284,10 +284,10 @@ export class NodeCommandTasks { } async _checkNetworkNodeActiveness (namespace: string, nodeAlias: NodeAlias, task: ListrTaskWrapper, - title: string, index: number, status = NodeStatusCodes.ACTIVE, - maxAttempts = constants.NETWORK_NODE_ACTIVE_MAX_ATTEMPTS, - delay = constants.NETWORK_NODE_ACTIVE_DELAY, - timeout = constants.NETWORK_NODE_ACTIVE_TIMEOUT + title: string, index: number, status = NodeStatusCodes.ACTIVE, + maxAttempts = constants.NETWORK_NODE_ACTIVE_MAX_ATTEMPTS, + delay = constants.NETWORK_NODE_ACTIVE_DELAY, + timeout = constants.NETWORK_NODE_ACTIVE_TIMEOUT ) { nodeAlias = nodeAlias.trim() as NodeAlias const podName = Templates.renderNetworkPodName(nodeAlias) @@ -319,8 +319,8 @@ export class NodeCommandTasks { const text = await response.text() const statusLine = text - .split('\n') - .find(line => line.startsWith('platform_PlatformStatus')) + .split('\n') + .find(line => line.startsWith('platform_PlatformStatus')) if (!statusLine) { task.title = `${title} - status ${chalk.yellow('STARTING')}, attempt: ${chalk.blueBright(`${attempt}/${maxAttempts}`)}` @@ -368,8 +368,8 @@ export class NodeCommandTasks { subTasks.push({ title: `Check proxy for node: ${chalk.yellow(nodeAlias)}`, task: async () => await this.k8.waitForPodReady( - [`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'], - 1, constants.NETWORK_PROXY_MAX_ATTEMPTS, constants.NETWORK_PROXY_DELAY) + [`app=haproxy-${nodeAlias}`, 'solo.hedera.com/type=haproxy'], + 1, constants.NETWORK_PROXY_MAX_ATTEMPTS, constants.NETWORK_PROXY_DELAY) }) } @@ -424,15 +424,15 @@ export class NodeCommandTasks { copyGrpcTlsCertificates () { return new Task('Copy gRPC TLS Certificates', - (ctx: { config: NodeAddConfigClass }, parentTask: ListrTaskWrapper) => - this.certificateManager.buildCopyTlsCertificatesTasks( - parentTask, - ctx.config.grpcTlsCertificatePath, - ctx.config.grpcWebTlsCertificatePath, - ctx.config.grpcTlsKeyPath, - ctx.config.grpcWebTlsKeyPath - ), - (ctx: any) => !ctx.config.grpcTlsCertificatePath && !ctx.config.grpcWebTlsCertificatePath + (ctx: { config: NodeAddConfigClass }, parentTask: ListrTaskWrapper) => + this.certificateManager.buildCopyTlsCertificatesTasks( + parentTask, + ctx.config.grpcTlsCertificatePath, + ctx.config.grpcWebTlsCertificatePath, + ctx.config.grpcTlsKeyPath, + ctx.config.grpcWebTlsKeyPath + ), + (ctx: any) => !ctx.config.grpcTlsCertificatePath && !ctx.config.grpcWebTlsCertificatePath ) } @@ -464,15 +464,15 @@ export class NodeCommandTasks { // check balance const balance = await new AccountBalanceQuery() - .setAccountId(accountId) - .execute(client) + .setAccountId(accountId) + .execute(client) this.logger.debug(`Account ${accountId} balance: ${balance.hbars}`) // Create the transaction const transaction = new AccountUpdateTransaction() - .setAccountId(accountId) - .setStakedNodeId(Templates.nodeIdFromNodeAlias(nodeAlias) - 1) - .freezeWith(client) + .setAccountId(accountId) + .setStakedNodeId(Templates.nodeIdFromNodeAlias(nodeAlias) - 1) + .freezeWith(client) // Sign the transaction with the account's private key const signTx = await transaction.sign(treasuryPrivateKey) @@ -526,8 +526,8 @@ export class NodeCommandTasks { try { // query the balance const balance = await new AccountBalanceQuery() - .setAccountId(FREEZE_ADMIN_ACCOUNT) - .execute(nodeClient) + .setAccountId(FREEZE_ADMIN_ACCOUNT) + .execute(nodeClient) this.logger.debug(`Freeze admin account balance: ${balance.hbars}`) // transfer some tiny amount to the freeze admin account @@ -537,17 +537,17 @@ export class NodeCommandTasks { nodeClient.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) const prepareUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.PrepareUpgrade) - .setFileId(constants.UPGRADE_FILE_ID) - .setFileHash(upgradeZipHash) - .freezeWith(nodeClient) - .execute(nodeClient) + .setFreezeType(FreezeType.PrepareUpgrade) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(nodeClient) + .execute(nodeClient) const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(nodeClient) this.logger.debug( - `sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, - prepareUpgradeReceipt.status.toString() + `sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, + prepareUpgradeReceipt.status.toString() ) } catch (e: Error | any) { this.logger.error(`Error in prepare upgrade: ${e.message}`, e) @@ -575,16 +575,16 @@ export class NodeCommandTasks { nodeClient.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) const freezeUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.FreezeUpgrade) - .setStartTimestamp(Timestamp.fromDate(futureDate)) - .setFileId(constants.UPGRADE_FILE_ID) - .setFileHash(upgradeZipHash) - .freezeWith(nodeClient) - .execute(nodeClient) + .setFreezeType(FreezeType.FreezeUpgrade) + .setStartTimestamp(Timestamp.fromDate(futureDate)) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(nodeClient) + .execute(nodeClient) const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(nodeClient) this.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, - freezeUpgradeReceipt.status.toString()) + freezeUpgradeReceipt.status.toString()) } catch (e: Error | any) { this.logger.error(`Error in freeze upgrade: ${e.message}`, e) throw new SoloError(`Error in freeze upgrade: ${e.message}`, e) @@ -687,7 +687,7 @@ export class NodeCommandTasks { if (localBuildPath !== '') { return this._uploadPlatformSoftware(ctx.config[aliasesField], podNames, task, localBuildPath) } - return this._fetchPlatformSoftware(ctx.config[aliasesField], podNames, releaseTag, task, this.platformInstaller) + return this._fetchPlatformSoftware(ctx.config[aliasesField], podNames, releaseTag, task, this.platformInstaller) }) } @@ -695,7 +695,7 @@ export class NodeCommandTasks { populateServiceMap () { return new Task('Populate serviceMap', async (ctx: any, task: ListrTaskWrapper) => { ctx.config.serviceMap = await this.accountManager.getNodeServiceMap( - ctx.config.namespace) + ctx.config.namespace) ctx.config.podNames[ctx.config.nodeAlias] = ctx.config.serviceMap.get(ctx.config.nodeAlias).nodePodName }) } @@ -708,7 +708,7 @@ export class NodeCommandTasks { subTasks.push({ title: `Node: ${chalk.yellow(nodeAlias)}`, task: () => - this.platformInstaller.taskSetup(podName) + this.platformInstaller.taskSetup(podName) }) } @@ -908,7 +908,7 @@ export class NodeCommandTasks { subTasks.push({ title: `Node: ${chalk.yellow(nodeAlias)}`, task: async () => - await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/*`]) + await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/*`]) }) } @@ -950,8 +950,8 @@ export class NodeCommandTasks { name: networkNodeServices.nodeAlias }) maxNum = maxNum > AccountId.fromString(networkNodeServices.accountId).num - ? maxNum - : AccountId.fromString(networkNodeServices.accountId).num + ? maxNum + : AccountId.fromString(networkNodeServices.accountId).num lastNodeAlias = networkNodeServices.nodeAlias } @@ -1154,8 +1154,8 @@ export class NodeCommandTasks { valuesArg += ` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}" --set "hedera.nodes[${index}].name=${ctx.newNode.name}"` } const profileValuesFile = await this.profileManager.prepareValuesForNodeAdd( - path.join(config.stagingDir, 'config.txt'), - path.join(config.stagingDir, 'templates', 'application.properties')) + path.join(config.stagingDir, 'config.txt'), + path.join(config.stagingDir, 'templates', 'application.properties')) if (profileValuesFile) { valuesArg += this.prepareValuesFiles(profileValuesFile) } @@ -1163,10 +1163,10 @@ export class NodeCommandTasks { valuesArg = addDebugOptions(valuesArg, config.debugNodeAlias) await this.chartManager.upgrade( - config.namespace, - constants.SOLO_DEPLOYMENT_CHART, constants.SOLO_TESTING_CHART_URL + constants.SOLO_DEPLOYMENT_CHART, - valuesArg, - config.soloChartVersion + config.namespace, + constants.SOLO_DEPLOYMENT_CHART, constants.SOLO_TESTING_CHART_URL + constants.SOLO_DEPLOYMENT_CHART, + valuesArg, + config.soloChartVersion ) }, skip) } @@ -1237,10 +1237,10 @@ export class NodeCommandTasks { subTasks.push({ title: `Check Node: ${chalk.yellow(nodeAlias)}`, task: async () => - await this.k8.waitForPods([constants.POD_PHASE_RUNNING], [ - 'solo.hedera.com/type=network-node', + await this.k8.waitForPods([constants.POD_PHASE_RUNNING], [ + 'solo.hedera.com/type=network-node', `solo.hedera.com/node-name=${nodeAlias}` - ], 1, constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY) // timeout 15 minutes + ], 1, constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY) // timeout 15 minutes }) } @@ -1296,8 +1296,8 @@ export class NodeCommandTasks { this.logger.debug(`Deleting node: ${config.nodeAlias} with account: ${deleteAccountId}`) const nodeId = Templates.nodeIdFromNodeAlias(config.nodeAlias) - 1 const nodeDeleteTx = new NodeDeleteTransaction() - .setNodeId(nodeId) - .freezeWith(config.nodeClient) + .setNodeId(nodeId) + .freezeWith(config.nodeClient) const signedTx = await nodeDeleteTx.sign(config.adminKey) const txResp = await signedTx.execute(config.nodeClient) @@ -1317,13 +1317,13 @@ export class NodeCommandTasks { try { const nodeCreateTx = new NodeCreateTransaction() - .setAccountId(ctx.newNode.accountId) - .setGossipEndpoints(ctx.gossipEndpoints) - .setServiceEndpoints(ctx.grpcServiceEndpoints) - .setGossipCaCertificate(ctx.signingCertDer) - .setCertificateHash(ctx.tlsCertHash) - .setAdminKey(ctx.adminKey.publicKey) - .freezeWith(config.nodeClient) + .setAccountId(ctx.newNode.accountId) + .setGossipEndpoints(ctx.gossipEndpoints) + .setServiceEndpoints(ctx.grpcServiceEndpoints) + .setGossipCaCertificate(ctx.signingCertDer) + .setCertificateHash(ctx.tlsCertHash) + .setAdminKey(ctx.adminKey.publicKey) + .freezeWith(config.nodeClient) const signedTx = await nodeCreateTx.sign(ctx.adminKey) const txResp = await signedTx.execute(config.nodeClient) const nodeCreateReceipt = await txResp.getReceipt(config.nodeClient) @@ -1335,12 +1335,6 @@ export class NodeCommandTasks { }) } - templateTask () { - return new Task('TEMPLATE', async (ctx: any, task: ListrTaskWrapper) => { - - }) - } - initialize (argv: any, configInit: Function, lease: Lease | null) { const { requiredFlags, requiredFlagsWithDisabledPrompt, optionalFlags } = argv const allRequiredFlags = [ diff --git a/src/commands/prompts.ts b/src/commands/prompts.ts index 53037bbbc..e8357b378 100644 --- a/src/commands/prompts.ts +++ b/src/commands/prompts.ts @@ -55,77 +55,77 @@ async function prompt (type: string, task: ListrTaskWrapper, inpu } async function promptText (task: ListrTaskWrapper, input: any, defaultValue: any, promptMessage: string, - emptyCheckMessage: string | null, flagName: string) { + emptyCheckMessage: string | null, flagName: string) { return await prompt('text', task, input, defaultValue, promptMessage, emptyCheckMessage, flagName) } async function promptToggle (task: ListrTaskWrapper, input: any, defaultValue: any, promptMessage: string, - emptyCheckMessage: string| null, flagName: string) { + emptyCheckMessage: string| null, flagName: string) { return await prompt('toggle', task, input, defaultValue, promptMessage, emptyCheckMessage, flagName) } export async function promptNamespace (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - 'solo', - 'Enter namespace name: ', - 'namespace cannot be empty', - flags.namespace.name) + 'solo', + 'Enter namespace name: ', + 'namespace cannot be empty', + flags.namespace.name) } export async function promptClusterSetupNamespace (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - 'solo-cluster', - 'Enter cluster setup namespace name: ', - 'cluster setup namespace cannot be empty', - flags.clusterSetupNamespace.name) + 'solo-cluster', + 'Enter cluster setup namespace name: ', + 'cluster setup namespace cannot be empty', + flags.clusterSetupNamespace.name) } export async function promptNodeAliases (task: ListrTaskWrapper, input: any) { return await prompt('input', task, input, - 'node1,node2,node3', - 'Enter list of node IDs (comma separated list): ', - null, - flags.nodeAliasesUnparsed.name) + 'node1,node2,node3', + 'Enter list of node IDs (comma separated list): ', + null, + flags.nodeAliasesUnparsed.name) } export async function promptReleaseTag (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - 'v0.42.5', - 'Enter release version: ', - 'release tag cannot be empty', - flags.releaseTag.name) + 'v0.42.5', + 'Enter release version: ', + 'release tag cannot be empty', + flags.releaseTag.name) } export async function promptRelayReleaseTag (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.relayReleaseTag.definition.defaultValue, - 'Enter relay release version: ', - 'relay-release-tag cannot be empty', - flags.relayReleaseTag.name) + flags.relayReleaseTag.definition.defaultValue, + 'Enter relay release version: ', + 'relay-release-tag cannot be empty', + flags.relayReleaseTag.name) } export async function promptCacheDir (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - constants.SOLO_CACHE_DIR, - 'Enter local cache directory path: ', - null, - flags.cacheDir.name) + constants.SOLO_CACHE_DIR, + 'Enter local cache directory path: ', + null, + flags.cacheDir.name) } export async function promptForce (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.force.definition.defaultValue, - 'Would you like to force changes? ', - null, - flags.force.name) + flags.force.definition.defaultValue, + 'Would you like to force changes? ', + null, + flags.force.name) } export async function promptChainId (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.chainId.definition.defaultValue, - 'Enter chain ID: ', - null, - flags.chainId.name) + flags.chainId.definition.defaultValue, + 'Enter chain ID: ', + null, + flags.chainId.name) } export async function promptChartDir (task: ListrTaskWrapper, input: any) { @@ -213,50 +213,50 @@ export async function promptProfile (task: ListrTaskWrapper, inpu export async function promptDeployPrometheusStack (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deployPrometheusStack.definition.defaultValue, - 'Would you like to deploy prometheus stack? ', - null, - flags.deployPrometheusStack.name) + flags.deployPrometheusStack.definition.defaultValue, + 'Would you like to deploy prometheus stack? ', + null, + flags.deployPrometheusStack.name) } export async function promptEnablePrometheusSvcMonitor (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.enablePrometheusSvcMonitor.definition.defaultValue, - 'Would you like to enable the Prometheus service monitor for the network nodes? ', - null, - flags.enablePrometheusSvcMonitor.name) + flags.enablePrometheusSvcMonitor.definition.defaultValue, + 'Would you like to enable the Prometheus service monitor for the network nodes? ', + null, + flags.enablePrometheusSvcMonitor.name) } export async function promptDeployMinio (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deployMinio.definition.defaultValue, - 'Would you like to deploy MinIO? ', - null, - flags.deployMinio.name) + flags.deployMinio.definition.defaultValue, + 'Would you like to deploy MinIO? ', + null, + flags.deployMinio.name) } export async function promptDeployCertManager (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deployCertManager.definition.defaultValue, - 'Would you like to deploy Cert Manager? ', - null, - flags.deployCertManager.name) + flags.deployCertManager.definition.defaultValue, + 'Would you like to deploy Cert Manager? ', + null, + flags.deployCertManager.name) } export async function promptDeployCertManagerCrds (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deployCertManagerCrds.definition.defaultValue, - 'Would you like to deploy Cert Manager CRDs? ', - null, - flags.deployCertManagerCrds.name) + flags.deployCertManagerCrds.definition.defaultValue, + 'Would you like to deploy Cert Manager CRDs? ', + null, + flags.deployCertManagerCrds.name) } export async function promptDeployHederaExplorer (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deployHederaExplorer.definition.defaultValue, - 'Would you like to deploy Hedera Explorer? ', - null, - flags.deployHederaExplorer.name) + flags.deployHederaExplorer.definition.defaultValue, + 'Would you like to deploy Hedera Explorer? ', + null, + flags.deployHederaExplorer.name) } export async function promptTlsClusterIssuerType (task: ListrTaskWrapper, input: any) { @@ -281,90 +281,90 @@ export async function promptTlsClusterIssuerType (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.enableHederaExplorerTls.definition.defaultValue, - 'Would you like to enable the Hedera Explorer TLS? ', - null, - flags.enableHederaExplorerTls.name) + flags.enableHederaExplorerTls.definition.defaultValue, + 'Would you like to enable the Hedera Explorer TLS? ', + null, + flags.enableHederaExplorerTls.name) } export async function promptHederaExplorerTlsHostName (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.hederaExplorerTlsHostName.definition.defaultValue, - 'Enter the host name to use for the Hedera Explorer TLS: ', - null, - flags.hederaExplorerTlsHostName.name) + flags.hederaExplorerTlsHostName.definition.defaultValue, + 'Enter the host name to use for the Hedera Explorer TLS: ', + null, + flags.hederaExplorerTlsHostName.name) } export async function promptOperatorId (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.operatorId.definition.defaultValue, - 'Enter operator ID: ', - null, - flags.operatorId.name) + flags.operatorId.definition.defaultValue, + 'Enter operator ID: ', + null, + flags.operatorId.name) } export async function promptOperatorKey (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.operatorKey.definition.defaultValue, - 'Enter operator private key: ', - null, - flags.operatorKey.name) + flags.operatorKey.definition.defaultValue, + 'Enter operator private key: ', + null, + flags.operatorKey.name) } export async function promptReplicaCount (task: ListrTaskWrapper, input: any) { return await prompt('number', task, input, - flags.replicaCount.definition.defaultValue, - 'How many replica do you want? ', - null, - flags.replicaCount.name) + flags.replicaCount.definition.defaultValue, + 'How many replica do you want? ', + null, + flags.replicaCount.name) } export async function promptGenerateGossipKeys (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.generateGossipKeys.definition.defaultValue, - `Would you like to generate Gossip keys? ${typeof input} ${input} `, - null, - flags.generateGossipKeys.name) + flags.generateGossipKeys.definition.defaultValue, + `Would you like to generate Gossip keys? ${typeof input} ${input} `, + null, + flags.generateGossipKeys.name) } export async function promptGenerateTLSKeys (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.generateTlsKeys.definition.defaultValue, - 'Would you like to generate TLS keys? ', - null, - flags.generateTlsKeys.name) + flags.generateTlsKeys.definition.defaultValue, + 'Would you like to generate TLS keys? ', + null, + flags.generateTlsKeys.name) } export async function promptDeletePvcs (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deletePvcs.definition.defaultValue, - 'Would you like to delete persistent volume claims upon uninstall? ', - null, - flags.deletePvcs.name) + flags.deletePvcs.definition.defaultValue, + 'Would you like to delete persistent volume claims upon uninstall? ', + null, + flags.deletePvcs.name) } export async function promptDeleteSecrets (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.deleteSecrets.definition.defaultValue, - 'Would you like to delete secrets upon uninstall? ', - null, - flags.deleteSecrets.name) + flags.deleteSecrets.definition.defaultValue, + 'Would you like to delete secrets upon uninstall? ', + null, + flags.deleteSecrets.name) } export async function promptSoloChartVersion (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.soloChartVersion.definition.defaultValue, - 'Enter solo testing chart version: ', - null, - flags.soloChartVersion.name) + flags.soloChartVersion.definition.defaultValue, + 'Enter solo testing chart version: ', + null, + flags.soloChartVersion.name) } export async function promptUpdateAccountKeys (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.updateAccountKeys.definition.defaultValue, - 'Would you like to updates the special account keys to new keys and stores their keys in a corresponding Kubernetes secret? ', - null, - flags.updateAccountKeys.name) + flags.updateAccountKeys.definition.defaultValue, + 'Would you like to updates the special account keys to new keys and stores their keys in a corresponding Kubernetes secret? ', + null, + flags.updateAccountKeys.name) } export async function promptUserEmailAddress (task: ListrTaskWrapper, input: any) { @@ -383,92 +383,100 @@ export async function promptUserEmailAddress (task: ListrTaskWrapper, input: any) { - return await promptText(task, input, - flags.deploymentName.definition.defaultValue, - 'Enter the Solo deployment name: ', - null, - flags.deploymentName.name) -} - export async function promptDeploymentClusters (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.deploymentClusters.definition.defaultValue, - 'Enter the Solo deployment cluster names (comma separated): ', - null, - flags.deploymentClusters.name) + flags.deploymentClusters.definition.defaultValue, + 'Enter the Solo deployment cluster names (comma separated): ', + null, + flags.deploymentClusters.name) } export async function promptPrivateKey (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.ed25519PrivateKey.definition.defaultValue, - 'Enter the private key: ', - null, - flags.ed25519PrivateKey.name) + flags.ed25519PrivateKey.definition.defaultValue, + 'Enter the private key: ', + null, + flags.ed25519PrivateKey.name) } export async function promptAccountId (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.accountId.definition.defaultValue, - 'Enter the account id: ', - null, - flags.accountId.name) + flags.accountId.definition.defaultValue, + 'Enter the account id: ', + null, + flags.accountId.name) } export async function promptAmount (task: ListrTaskWrapper, input: any) { return await prompt('number', task, input, - flags.amount.definition.defaultValue, - 'How much HBAR do you want to add? ', - null, - flags.amount.name) + flags.amount.definition.defaultValue, + 'How much HBAR do you want to add? ', + null, + flags.amount.name) } export async function promptNewNodeAlias (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.nodeAlias.definition.defaultValue, - 'Enter the new node id: ', - null, - flags.nodeAlias.name) + flags.nodeAlias.definition.defaultValue, + 'Enter the new node id: ', + null, + flags.nodeAlias.name) } export async function promptGossipEndpoints (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.gossipEndpoints.definition.defaultValue, - 'Enter the gossip endpoints(comma separated): ', - null, - flags.gossipEndpoints.name) + flags.gossipEndpoints.definition.defaultValue, + 'Enter the gossip endpoints(comma separated): ', + null, + flags.gossipEndpoints.name) } export async function promptGrpcEndpoints (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.grpcEndpoints.definition.defaultValue, - 'Enter the gRPC endpoints(comma separated): ', - null, - flags.grpcEndpoints.name) + flags.grpcEndpoints.definition.defaultValue, + 'Enter the gRPC endpoints(comma separated): ', + null, + flags.grpcEndpoints.name) } export async function promptEndpointType (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.endpointType.definition.defaultValue, - 'Enter the endpoint type(IP or FQDN): ', - null, - flags.endpointType.name) + flags.endpointType.definition.defaultValue, + 'Enter the endpoint type(IP or FQDN): ', + null, + flags.endpointType.name) +} + +export async function promptContext (task: ListrTaskWrapper, input: any) { + return await promptText(task, input, + flags.context.definition.defaultValue, + 'Enter the context name: ', + null, + flags.context.name) +} + +export async function promptClusterName (task: ListrTaskWrapper, input: any) { + return await promptText(task, input, + flags.clusterName.definition.defaultValue, + 'Enter the cluster name: ', + null, + flags.clusterName.name) } export async function promptPersistentVolumeClaims (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.persistentVolumeClaims.definition.defaultValue, - 'Would you like to enable persistent volume claims to store data outside the pod? ', - null, - flags.persistentVolumeClaims.name) + flags.persistentVolumeClaims.definition.defaultValue, + 'Would you like to enable persistent volume claims to store data outside the pod? ', + null, + flags.persistentVolumeClaims.name) } export async function promptMirrorNodeVersion (task: ListrTaskWrapper, input: any) { return await promptToggle(task, input, - flags.mirrorNodeVersion.definition.defaultValue, - 'Would you like to choose mirror node version? ', - null, - flags.mirrorNodeVersion.name) + flags.mirrorNodeVersion.definition.defaultValue, + 'Would you like to choose mirror node version? ', + null, + flags.mirrorNodeVersion.name) } export async function promptHederaExplorerVersion (task: ListrTaskWrapper, input: any) { @@ -480,106 +488,108 @@ export async function promptHederaExplorerVersion (task: ListrTaskWrapper, input: any) { - return await promptToggle(task, input, - flags.inputDir.definition.defaultValue, - 'Enter path to directory containing the temporary context file', - null, - flags.inputDir.name) + return await promptToggle(task, input, + flags.inputDir.definition.defaultValue, + 'Enter path to directory containing the temporary context file', + null, + flags.inputDir.name) } export async function promptOutputDir (task: ListrTaskWrapper, input: any) { - return await promptToggle(task, input, - flags.outputDir.definition.defaultValue, - 'Enter path to directory to store the temporary context file', - null, - flags.outputDir.name) + return await promptToggle(task, input, + flags.outputDir.definition.defaultValue, + 'Enter path to directory to store the temporary context file', + null, + flags.outputDir.name) } //! ------------- Node Proxy Certificates ------------- !// export async function promptGrpcTlsCertificatePath (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.grpcTlsCertificatePath.definition.defaultValue, - 'Enter node alias and path to TLS certificate for gRPC (ex. nodeAlias=path )', - null, - flags.grpcTlsCertificatePath.name) + flags.grpcTlsCertificatePath.definition.defaultValue, + 'Enter node alias and path to TLS certificate for gRPC (ex. nodeAlias=path )', + null, + flags.grpcTlsCertificatePath.name) } export async function promptGrpcWebTlsCertificatePath (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.grpcWebTlsCertificatePath.definition.defaultValue, - 'Enter node alias and path to TLS certificate for gGRPC web (ex. nodeAlias=path )', - null, - flags.grpcWebTlsCertificatePath.name) + flags.grpcWebTlsCertificatePath.definition.defaultValue, + 'Enter node alias and path to TLS certificate for gGRPC web (ex. nodeAlias=path )', + null, + flags.grpcWebTlsCertificatePath.name) } export async function promptGrpcTlsKeyPath (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.grpcTlsKeyPath.definition.defaultValue, - 'Enter node alias and path to TLS certificate key for gRPC (ex. nodeAlias=path )', - null, - flags.grpcTlsKeyPath.name) + flags.grpcTlsKeyPath.definition.defaultValue, + 'Enter node alias and path to TLS certificate key for gRPC (ex. nodeAlias=path )', + null, + flags.grpcTlsKeyPath.name) } export async function promptGrpcWebTlsKeyPath (task: ListrTaskWrapper, input: any) { return await promptText(task, input, - flags.grpcWebTlsKeyPath.definition.defaultValue, - 'Enter node alias and path to TLS certificate key for gGRPC Web (ex. nodeAlias=path )', - null, - flags.grpcWebTlsKeyPath.name) + flags.grpcWebTlsKeyPath.definition.defaultValue, + 'Enter node alias and path to TLS certificate key for gGRPC Web (ex. nodeAlias=path )', + null, + flags.grpcWebTlsKeyPath.name) } export function getPromptMap (): Map { return new Map() - .set(flags.accountId.name, promptAccountId) - .set(flags.amount.name, promptAmount) - .set(flags.cacheDir.name, promptCacheDir) - .set(flags.chainId.name, promptChainId) - .set(flags.chartDirectory.name, promptChartDir) - .set(flags.clusterSetupNamespace.name, promptClusterSetupNamespace) - .set(flags.deletePvcs.name, promptDeletePvcs) - .set(flags.deleteSecrets.name, promptDeleteSecrets) - .set(flags.deployCertManager.name, promptDeployCertManager) - .set(flags.deployCertManagerCrds.name, promptDeployCertManagerCrds) - .set(flags.deployHederaExplorer.name, promptDeployHederaExplorer) - .set(flags.deployMinio.name, promptDeployMinio) - .set(flags.deployPrometheusStack.name, promptDeployPrometheusStack) - .set(flags.enableHederaExplorerTls.name, promptEnableHederaExplorerTls) - .set(flags.enablePrometheusSvcMonitor.name, promptEnablePrometheusSvcMonitor) - .set(flags.force.name, promptForce) - .set(flags.soloChartVersion.name, promptSoloChartVersion) - .set(flags.generateGossipKeys.name, promptGenerateGossipKeys) - .set(flags.generateTlsKeys.name, promptGenerateTLSKeys) - .set(flags.hederaExplorerTlsHostName.name, promptHederaExplorerTlsHostName) - .set(flags.namespace.name, promptNamespace) - .set(flags.nodeAliasesUnparsed.name, promptNodeAliases) - .set(flags.operatorId.name, promptOperatorId) - .set(flags.operatorKey.name, promptOperatorKey) - .set(flags.persistentVolumeClaims.name, promptPersistentVolumeClaims) - .set(flags.ed25519PrivateKey.name, promptPrivateKey) - .set(flags.profileFile.name, promptProfileFile) - .set(flags.profileName.name, promptProfile) - .set(flags.relayReleaseTag.name, promptRelayReleaseTag) - .set(flags.releaseTag.name, promptReleaseTag) - .set(flags.replicaCount.name, promptReplicaCount) - .set(flags.tlsClusterIssuerType.name, promptTlsClusterIssuerType) - .set(flags.updateAccountKeys.name, promptUpdateAccountKeys) - .set(flags.userEmailAddress.name, promptUserEmailAddress) - .set(flags.valuesFile.name, promptValuesFile) - .set(flags.nodeAlias.name, promptNewNodeAlias) - .set(flags.gossipEndpoints.name, promptGossipEndpoints) - .set(flags.grpcEndpoints.name, promptGrpcEndpoints) - .set(flags.endpointType.name, promptEndpointType) - .set(flags.mirrorNodeVersion.name, promptMirrorNodeVersion) - .set(flags.hederaExplorerVersion, promptHederaExplorerVersion) - .set(flags.inputDir.name, promptInputDir) - .set(flags.outputDir.name, promptOutputDir) - - //! Node Proxy Certificates - .set(flags.grpcTlsCertificatePath.name, promptGrpcTlsCertificatePath) - .set(flags.grpcWebTlsCertificatePath.name, promptGrpcWebTlsCertificatePath) - .set(flags.grpcTlsKeyPath.name, promptGrpcTlsKeyPath) - .set(flags.grpcWebTlsKeyPath.name, promptGrpcWebTlsKeyPath) + .set(flags.accountId.name, promptAccountId) + .set(flags.amount.name, promptAmount) + .set(flags.cacheDir.name, promptCacheDir) + .set(flags.chainId.name, promptChainId) + .set(flags.chartDirectory.name, promptChartDir) + .set(flags.clusterName.name, promptClusterName) + .set(flags.clusterSetupNamespace.name, promptClusterSetupNamespace) + .set(flags.context.name, promptContext) + .set(flags.deletePvcs.name, promptDeletePvcs) + .set(flags.deleteSecrets.name, promptDeleteSecrets) + .set(flags.deployCertManager.name, promptDeployCertManager) + .set(flags.deployCertManagerCrds.name, promptDeployCertManagerCrds) + .set(flags.deployHederaExplorer.name, promptDeployHederaExplorer) + .set(flags.deployMinio.name, promptDeployMinio) + .set(flags.deployPrometheusStack.name, promptDeployPrometheusStack) + .set(flags.enableHederaExplorerTls.name, promptEnableHederaExplorerTls) + .set(flags.enablePrometheusSvcMonitor.name, promptEnablePrometheusSvcMonitor) + .set(flags.force.name, promptForce) + .set(flags.soloChartVersion.name, promptSoloChartVersion) + .set(flags.generateGossipKeys.name, promptGenerateGossipKeys) + .set(flags.generateTlsKeys.name, promptGenerateTLSKeys) + .set(flags.hederaExplorerTlsHostName.name, promptHederaExplorerTlsHostName) + .set(flags.namespace.name, promptNamespace) + .set(flags.nodeAliasesUnparsed.name, promptNodeAliases) + .set(flags.operatorId.name, promptOperatorId) + .set(flags.operatorKey.name, promptOperatorKey) + .set(flags.persistentVolumeClaims.name, promptPersistentVolumeClaims) + .set(flags.ed25519PrivateKey.name, promptPrivateKey) + .set(flags.profileFile.name, promptProfileFile) + .set(flags.profileName.name, promptProfile) + .set(flags.relayReleaseTag.name, promptRelayReleaseTag) + .set(flags.releaseTag.name, promptReleaseTag) + .set(flags.replicaCount.name, promptReplicaCount) + .set(flags.tlsClusterIssuerType.name, promptTlsClusterIssuerType) + .set(flags.updateAccountKeys.name, promptUpdateAccountKeys) + .set(flags.userEmailAddress.name, promptUserEmailAddress) + .set(flags.valuesFile.name, promptValuesFile) + .set(flags.nodeAlias.name, promptNewNodeAlias) + .set(flags.gossipEndpoints.name, promptGossipEndpoints) + .set(flags.grpcEndpoints.name, promptGrpcEndpoints) + .set(flags.endpointType.name, promptEndpointType) + .set(flags.mirrorNodeVersion.name, promptMirrorNodeVersion) + .set(flags.hederaExplorerVersion, promptHederaExplorerVersion) + .set(flags.inputDir.name, promptInputDir) + .set(flags.outputDir.name, promptOutputDir) + + //! Node Proxy Certificates + .set(flags.grpcTlsCertificatePath.name, promptGrpcTlsCertificatePath) + .set(flags.grpcWebTlsCertificatePath.name, promptGrpcWebTlsCertificatePath) + .set(flags.grpcTlsKeyPath.name, promptGrpcTlsKeyPath) + .set(flags.grpcWebTlsKeyPath.name, promptGrpcWebTlsKeyPath) } // build the prompt registry diff --git a/src/core/config/local_config.ts b/src/core/config/local_config.ts index 75231f3e6..be2909489 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -21,10 +21,11 @@ import * as yaml from 'yaml' import { flags } from '../../commands/index.js' import { type Deployment, type Deployments, type LocalConfigData } from './local_config_data.js' import { MissingArgumentError, SoloError } from '../errors.js' -import { promptDeploymentClusters, promptDeploymentName, promptUserEmailAddress } from '../../commands/prompts.js' +import { promptDeploymentClusters, promptNamespace, promptUserEmailAddress } from '../../commands/prompts.js' import { type SoloLogger } from '../logging.js' import { Task } from '../task.js' import { IsDeployments } from '../validator_decorators.js' +import { Templates } from '../templates.js' export class LocalConfig implements LocalConfigData { @IsNotEmpty() @@ -118,20 +119,20 @@ export class LocalConfig implements LocalConfigData { this.logger.info(`Wrote local config to ${this.filePath}`) } - public promptLocalConfigTask (k8, argv): ListrTask[] { + public promptLocalConfigTask (k8, argv): Task { return new Task('Prompt local configuration', async (ctx, task) => { let userEmailAddress = argv[flags.userEmailAddress.name] if (!userEmailAddress) userEmailAddress = await promptUserEmailAddress(task, userEmailAddress) - let deploymentName = argv[flags.deploymentName.name] - if (!deploymentName) deploymentName = await promptDeploymentName(task, deploymentName) + let deploymentName = argv[flags.namespace.name] + if (!deploymentName) deploymentName = await promptNamespace(task, deploymentName) let deploymentClusters = argv[flags.deploymentClusters.name] if (!deploymentClusters) deploymentClusters = await promptDeploymentClusters(task, deploymentClusters) const deployments = {} deployments[deploymentName] = { - clusterAliases: deploymentClusters.split(',') + clusterAliases: Templates.parseClusterAliases(deploymentClusters) } this.userEmailAddress = userEmailAddress @@ -141,6 +142,6 @@ export class LocalConfig implements LocalConfigData { await this.write() return this - }, this.skipPromptTask) as ListrTask[] + }, this.skipPromptTask) as Task } } diff --git a/src/core/helpers.ts b/src/core/helpers.ts index c50e4e02d..23484498e 100644 --- a/src/core/helpers.ts +++ b/src/core/helpers.ts @@ -28,7 +28,7 @@ import { Listr } from 'listr2' import { type AccountManager } from './account_manager.js' import { type NodeAlias, type NodeAliases, type PodName } from '../types/aliases.js' import { type NodeDeleteConfigClass, type NodeUpdateConfigClass } from '../commands/node/configs.js' -import { type CommandFlag } from '../types/index.js' +import { type CommandFlag, type CommandHandlers } from '../types/index.js' import { type V1Pod } from '@kubernetes/client-node' import { type SoloLogger } from './logging.js' import { type NodeCommandHandlers } from '../commands/node/handlers.js' @@ -50,9 +50,9 @@ export function splitFlagInput (input: string, separator = ',') { } return input - .split(separator) - .map(s => s.trim()) - .filter(Boolean) + .split(separator) + .map(s => s.trim()) + .filter(Boolean) } /** @@ -98,12 +98,12 @@ export function getTmpDir () { export function createBackupDir (destDir: string, prefix = 'backup', curDate = new Date()) { const dateDir = util.format('%s%s%s_%s%s%s', - curDate.getFullYear(), - curDate.getMonth().toString().padStart(2, '0'), - curDate.getDate().toString().padStart(2, '0'), - curDate.getHours().toString().padStart(2, '0'), - curDate.getMinutes().toString().padStart(2, '0'), - curDate.getSeconds().toString().padStart(2, '0') + curDate.getFullYear(), + curDate.getMonth().toString().padStart(2, '0'), + curDate.getDate().toString().padStart(2, '0'), + curDate.getHours().toString().padStart(2, '0'), + curDate.getMinutes().toString().padStart(2, '0'), + curDate.getSeconds().toString().padStart(2, '0') ) const backupDir = path.join(destDir, prefix, dateDir) @@ -160,7 +160,7 @@ export function backupOldPemKeys (nodeAliases: NodeAliases, keysDir: string, cur export function isNumeric (str: string) { if (typeof str !== 'string') return false // we only process strings! return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... - !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail + !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail } /** @@ -466,7 +466,7 @@ export function prepareEndpoints (endpointType: string, endpoints: string[], def } export function commandActionBuilder (actionTasks: any, options: any, errorString: string, lease: Lease | null) { - return async function (argv: any, commandDef: NodeCommandHandlers) { + return async function (argv: any, commandDef: CommandHandlers) { const tasks = new Listr([ ...actionTasks ], options) @@ -477,7 +477,14 @@ export function commandActionBuilder (actionTasks: any, options: any, errorStrin commandDef.parent.logger.error(`${errorString}: ${e.message}`, e) throw new SoloError(`${errorString}: ${e.message}`, e) } finally { - const promises = [commandDef.close()] + const promises = [] + + // @ts-ignore + if (commandDef.close) { + // @ts-ignore + promises.push(commandDef.close()) + } + if (lease) promises.push(lease.release()) await Promise.all(promises) } diff --git a/src/core/k8.ts b/src/core/k8.ts index df3ec38ce..89c440bea 100644 --- a/src/core/k8.ts +++ b/src/core/k8.ts @@ -23,7 +23,7 @@ import { flags } from '../commands/index.js' import { SoloError, IllegalArgumentError, MissingArgumentError } from './errors.js' import * as tar from 'tar' import { v4 as uuid4 } from 'uuid' -import { type V1Lease, V1ObjectMeta, V1Secret } from '@kubernetes/client-node' +import { type V1Lease, V1ObjectMeta, V1Secret, Config } from '@kubernetes/client-node' import { sleep } from './helpers.js' import { type ConfigManager, constants } from './index.js' import * as stream from 'node:stream' @@ -44,7 +44,7 @@ interface TDirectoryData {directory: boolean; owner: string; group: string; size */ export class K8 { static PodReadyCondition = new Map() - .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) + .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) private kubeConfig!: k8s.KubeConfig kubeClient!: k8s.CoreV1Api private coordinationApiClient: k8s.CoordinationV1Api @@ -75,7 +75,7 @@ export class K8 { if (!this.kubeConfig.getCurrentContext()) { throw new SoloError('No active kubernetes context found. ' + - 'Please set current kubernetes context.') + 'Please set current kubernetes context.') } if (!this.kubeConfig.getCurrentCluster()) { @@ -189,11 +189,11 @@ export class K8 { const ns = this._getNamespace() const fieldSelector = `metadata.name=${name}` const resp = await this.kubeClient.listNamespacedPod( - ns, - undefined, - undefined, - undefined, - fieldSelector + ns, + undefined, + undefined, + undefined, + fieldSelector ) return this.filterItem(resp.body.items, { name }) @@ -207,12 +207,12 @@ export class K8 { const ns = this._getNamespace() const labelSelector = labels.join(',') const result = await this.kubeClient.listNamespacedPod( - ns, - undefined, - undefined, - undefined, - undefined, - labelSelector + ns, + undefined, + undefined, + undefined, + undefined, + labelSelector ) return result.body.items @@ -226,12 +226,12 @@ export class K8 { const ns = this._getNamespace() const labelSelector = labels.join(',') const result = await this.kubeClient.listNamespacedSecret( - ns, - undefined, - undefined, - undefined, - undefined, - labelSelector + ns, + undefined, + undefined, + undefined, + undefined, + labelSelector ) return result.body.items @@ -261,11 +261,11 @@ export class K8 { const ns = this._getNamespace() const fieldSelector = `metadata.name=${name}` const resp = await this.kubeClient.listNamespacedService( - ns, - undefined, - undefined, - undefined, - fieldSelector + ns, + undefined, + undefined, + undefined, + fieldSelector ) return this.filterItem(resp.body.items, { name }) @@ -424,17 +424,17 @@ export class K8 { */ async hasDir (podName: string, containerName: string, destPath: string) { return await this.execContainer( - podName, - containerName, - ['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] + podName, + containerName, + ['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] ) === 'true' } mkdir (podName: PodName, containerName: string, destPath: string) { return this.execContainer( - podName, - containerName, - ['bash', '-c', 'mkdir -p "' + destPath + '"'] + podName, + containerName, + ['bash', '-c', 'mkdir -p "' + destPath + '"'] ) } @@ -448,7 +448,7 @@ export class K8 { if (status === 'Failure') { return this.exitWithError(localContext, `${messagePrefix} Failure occurred`) } - this.logger.debug(`${messagePrefix} callback(status)=${status}`) + this.logger.debug(`${messagePrefix} callback(status)=${status}`) } @@ -475,7 +475,7 @@ export class K8 { } registerOutputPassthroughStreamOnData (localContext: LocalContextObject, messagePrefix: string, - outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { + outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { outputPassthroughStream.on('data', (chunk) => { this.logger.debug(`${messagePrefix} received chunk size=${chunk.length}`) const canWrite = outputFileStream.write(chunk) // Write chunk to file and check if buffer is full @@ -487,7 +487,7 @@ export class K8 { } registerOutputFileStreamOnDrain (localContext: LocalContextObject, messagePrefix: string, - outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { + outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { outputFileStream.on('drain', () => { outputPassthroughStream.resume() this.logger.debug(`${messagePrefix} stream drained, resume write`) @@ -547,28 +547,28 @@ export class K8 { inputStream.pipe(inputPassthroughStream) execInstance.exec(namespace, podName, containerName, command, null, errPassthroughStream, inputPassthroughStream, false, - ({ status }) => self.handleCallback(status, localContext, messagePrefix)) - .then(conn => { - self.logger.info(`${messagePrefix} connection established`) - localContext.connection = conn + ({ status }) => self.handleCallback(status, localContext, messagePrefix)) + .then(conn => { + self.logger.info(`${messagePrefix} connection established`) + localContext.connection = conn - self.registerConnectionOnError(localContext, messagePrefix, conn) + self.registerConnectionOnError(localContext, messagePrefix, conn) - self.registerConnectionOnMessage(messagePrefix) + self.registerConnectionOnMessage(messagePrefix) - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) + } - // Cleanup temp file after successful copy - inputPassthroughStream.end() // End the passthrough stream - self._deleteTempFile(tmpFile) // Cleanup temp file - self.logger.info(`${messagePrefix} Successfully copied!`) - return resolve(true) + // Cleanup temp file after successful copy + inputPassthroughStream.end() // End the passthrough stream + self._deleteTempFile(tmpFile) // Cleanup temp file + self.logger.info(`${messagePrefix} Successfully copied!`) + return resolve(true) + }) }) - }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -648,59 +648,59 @@ export class K8 { self.logger.debug(`${messagePrefix} running...`) execInstance.exec( - namespace, - podName, - containerName, - command, - outputFileStream, - errPassthroughStream, - null, - false, - ({ status }) => { - if (status === 'Failure') { - self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} Failure occurred`) - } + namespace, + podName, + containerName, + command, + outputFileStream, + errPassthroughStream, + null, + false, + ({ status }) => { + if (status === 'Failure') { + self._deleteTempFile(tmpFile) + return self.exitWithError(localContext, `${messagePrefix} Failure occurred`) + } self.logger.debug(`${messagePrefix} callback(status)=${status}`) - }) - .then(conn => { - self.logger.debug(`${messagePrefix} connection established`) - localContext.connection = conn - - conn.on('error', (e) => { - self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} failed, connection error: ${e.message}`) }) + .then(conn => { + self.logger.debug(`${messagePrefix} connection established`) + localContext.connection = conn - self.registerConnectionOnMessage(messagePrefix) + conn.on('error', (e) => { + self._deleteTempFile(tmpFile) + return self.exitWithError(localContext, `${messagePrefix} failed, connection error: ${e.message}`) + }) - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } + self.registerConnectionOnMessage(messagePrefix) - outputFileStream.end() - outputFileStream.close(() => { - try { - fs.copyFileSync(tmpFile, destPath) + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) + } - self._deleteTempFile(tmpFile) + outputFileStream.end() + outputFileStream.close(() => { + try { + fs.copyFileSync(tmpFile, destPath) - const stat = fs.statSync(destPath) - if (stat && stat.size === srcFileSize) { - self.logger.debug(`${messagePrefix} finished`) - return resolve(true) - } + self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} files did not match, srcFileSize=${srcFileSize}, stat.size=${stat?.size}`) - } catch (e: Error | any) { - return self.exitWithError(localContext, `${messagePrefix} failed to complete download`) - } + const stat = fs.statSync(destPath) + if (stat && stat.size === srcFileSize) { + self.logger.debug(`${messagePrefix} finished`) + return resolve(true) + } + + return self.exitWithError(localContext, `${messagePrefix} files did not match, srcFileSize=${srcFileSize}, stat.size=${stat?.size}`) + } catch (e: Error | any) { + return self.exitWithError(localContext, `${messagePrefix} failed to complete download`) + } + }) }) }) - }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -753,39 +753,39 @@ export class K8 { self.logger.debug(`${messagePrefix} running...`) execInstance.exec( - namespace, - podName, - containerName, - command, - outputFileStream, - errPassthroughStream, - null, - false, - ({ status }) => self.handleCallback(status, localContext, messagePrefix)) - .then(conn => { - self.logger.debug(`${messagePrefix} connection established`) - localContext.connection = conn - - self.registerConnectionOnError(localContext, messagePrefix, conn) - - self.registerConnectionOnMessage(messagePrefix) - - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (!localContext.errorMessage) { - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } + namespace, + podName, + containerName, + command, + outputFileStream, + errPassthroughStream, + null, + false, + ({ status }) => self.handleCallback(status, localContext, messagePrefix)) + .then(conn => { + self.logger.debug(`${messagePrefix} connection established`) + localContext.connection = conn - outputFileStream.end() - outputFileStream.close(() => { - self.logger.debug(`${messagePrefix} finished`) - const outData = fs.readFileSync(tmpFile) - return resolve(outData.toString()) - }) - } + self.registerConnectionOnError(localContext, messagePrefix, conn) + + self.registerConnectionOnMessage(messagePrefix) + + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (!localContext.errorMessage) { + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) + } + + outputFileStream.end() + outputFileStream.close(() => { + self.logger.debug(`${messagePrefix} finished`) + const outData = fs.readFileSync(tmpFile) + return resolve(outData.toString()) + }) + } + }) }) - }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -878,24 +878,24 @@ export class K8 { try { const isPortOpen = await new Promise((resolve) => { const testServer = net.createServer() - .once('error', err => { - if (err) { - resolve(false) - } - }) - .once('listening', () => { - testServer - .once('close', () => { - hasError++ - if (hasError > 1) { - resolve(false) - } else { - resolve(true) - } - }) - .close() - }) - .listen(server.localPort, '0.0.0.0') + .once('error', err => { + if (err) { + resolve(false) + } + }) + .once('listening', () => { + testServer + .once('close', () => { + hasError++ + if (hasError > 1) { + resolve(false) + } else { + resolve(true) + } + }) + .close() + }) + .listen(server.localPort, '0.0.0.0') }) if (isPortOpen) { return @@ -911,7 +911,7 @@ export class K8 { } async waitForPods (phases = [constants.POD_PHASE_RUNNING], labels: string[] = [], podCount = 1, maxAttempts = constants.PODS_RUNNING_MAX_ATTEMPTS, - delay = constants.PODS_RUNNING_DELAY, podItemPredicate?: (items: k8s.V1Pod) => any): Promise { + delay = constants.PODS_RUNNING_DELAY, podItemPredicate?: (items: k8s.V1Pod) => any): Promise { const ns = this._getNamespace() const labelSelector = labels.join(',') @@ -925,14 +925,14 @@ export class K8 { // wait for the pod to be available with the given status and labels const resp = await this.kubeClient.listNamespacedPod( - ns, - // @ts-ignore - false, - false, - undefined, - undefined, - labelSelector, - podCount + ns, + // @ts-ignore + false, + false, + undefined, + undefined, + labelSelector, + podCount ) this.logger.debug(`${resp.body?.items?.length}/${podCount} pod found [namespace:${ns}, labelSelector: ${labelSelector}] [attempt: ${attempts}/${maxAttempts}]`) @@ -1021,12 +1021,12 @@ export class K8 { const pvcs: string[] = [] const labelSelector = labels.join(',') const resp = await this.kubeClient.listNamespacedPersistentVolumeClaim( - namespace, - undefined, - undefined, - undefined, - undefined, - labelSelector + namespace, + undefined, + undefined, + undefined, + undefined, + labelSelector ) for (const item of resp.body.items) { @@ -1046,12 +1046,12 @@ export class K8 { const secrets: string[] = [] const labelSelector = labels.join(',') const resp = await this.kubeClient.listNamespacedSecret( - namespace, - undefined, - undefined, - undefined, - undefined, - labelSelector + namespace, + undefined, + undefined, + undefined, + undefined, + labelSelector ) for (const item of resp.body.items) { @@ -1069,8 +1069,8 @@ export class K8 { */ async deletePvc (name: string, namespace: string) { const resp = await this.kubeClient.deleteNamespacedPersistentVolumeClaim( - name, - namespace + name, + namespace ) return resp.response.statusCode === 200.0 @@ -1085,7 +1085,7 @@ export class K8 { */ async getSecret (namespace: string, labelSelector: string) { const result = await this.kubeClient.listNamespacedSecret(namespace, - undefined, undefined, undefined, undefined, labelSelector) + undefined, undefined, undefined, undefined, labelSelector) if (result.response.statusCode === 200 && result.body.items && result.body.items.length > 0) { const secretObject = result.body.items[0] @@ -1097,7 +1097,7 @@ export class K8 { data: secretObject.data as Record } } - return null + return null } @@ -1165,7 +1165,7 @@ export class K8 { lease.spec = spec const { response, body } = await this.coordinationApiClient.createNamespacedLease(namespace, lease) - .catch(e => e) + .catch(e => e) this._handleKubernetesClientError(response, body, 'Failed to create namespaced lease') @@ -1174,7 +1174,7 @@ export class K8 { async readNamespacedLease (leaseName: string, namespace: string) { const { response, body } = await this.coordinationApiClient.readNamespacedLease(leaseName, namespace) - .catch(e => e) + .catch(e => e) this._handleKubernetesClientError(response, body, 'Failed to read namespaced lease') @@ -1185,7 +1185,7 @@ export class K8 { lease.spec.renewTime = new k8s.V1MicroTime() const { response, body } = await this.coordinationApiClient.replaceNamespacedLease(leaseName, namespace, lease) - .catch(e => e) + .catch(e => e) this._handleKubernetesClientError(response, body, 'Failed to renew namespaced lease') @@ -1208,7 +1208,7 @@ export class K8 { async deleteNamespacedLease (name: string, namespace: string) { const { response, body } = await this.coordinationApiClient.deleteNamespacedLease(name, namespace) - .catch(e => e) + .catch(e => e) this._handleKubernetesClientError(response, body, 'Failed to delete namespaced lease') diff --git a/src/core/task.ts b/src/core/task.ts index 9f45a78af..e96410399 100644 --- a/src/core/task.ts +++ b/src/core/task.ts @@ -15,11 +15,9 @@ * */ export class Task { - constructor (title: string, taskFunc: Function, skip: Function | boolean = false) { - return { - title, - task: taskFunc, - skip - } + constructor (public title: string, public task: Function, public skip: Function | boolean = false) { + this.title = title + this.task = task + this.skip = skip } } diff --git a/src/core/templates.ts b/src/core/templates.ts index daf1b9c0a..61f66701f 100644 --- a/src/core/templates.ts +++ b/src/core/templates.ts @@ -105,12 +105,12 @@ export class Templates { } static renderDistinguishedName ( - nodeAlias: NodeAlias, - state = 'TX', - locality = 'Richardson', - org = 'Hedera', - orgUnit = 'Hedera', - country = 'US' + nodeAlias: NodeAlias, + state = 'TX', + locality = 'Richardson', + org = 'Hedera', + orgUnit = 'Hedera', + country = 'US' ) { return new x509.Name(`CN=${nodeAlias},ST=${state},L=${locality},O=${org},OU=${orgUnit},C=${country}`) } @@ -133,9 +133,9 @@ export class Templates { } public static installationPath ( - dep: string, - osPlatform: NodeJS.Platform | string = os.platform(), - installationDir: string = path.join(constants.SOLO_HOME_DIR, 'bin') + dep: string, + osPlatform: NodeJS.Platform | string = os.platform(), + installationDir: string = path.join(constants.SOLO_HOME_DIR, 'bin') ) { switch (dep) { case constants.HELM: @@ -191,11 +191,11 @@ export class Templates { */ static renderGrpcTlsCertificatesSecretName (nodeAlias: NodeAlias, type: GrpcProxyTlsEnums) { switch (type) { - //? HAProxy Proxy + //? HAProxy Proxy case GrpcProxyTlsEnums.GRPC: return `haproxy-proxy-secret-${nodeAlias}` - //? Envoy Proxy + //? Envoy Proxy case GrpcProxyTlsEnums.GRPC_WEB: return `envoy-proxy-secret-${nodeAlias}` } @@ -211,13 +211,17 @@ export class Templates { */ static renderGrpcTlsCertificatesSecretLabelObject (nodeAlias: NodeAlias, type: GrpcProxyTlsEnums) { switch (type) { - //? HAProxy Proxy + //? HAProxy Proxy case GrpcProxyTlsEnums.GRPC: return { 'haproxy-proxy-secret': nodeAlias } - //? Envoy Proxy + //? Envoy Proxy case GrpcProxyTlsEnums.GRPC_WEB: return { 'envoy-proxy-secret': nodeAlias } } } + + static parseClusterAliases (clusterAliases: string) { + return clusterAliases ? clusterAliases.split(',') : [] + } } diff --git a/src/types/index.ts b/src/types/index.ts index f3aa92581..408aa4d8b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,6 +24,7 @@ import type { ProfileManager, DependencyManager, AccountManager, LeaseManager, CertificateManager, LocalConfig } from '../core/index.js' +import { type BaseCommand } from '../commands/base.js' export interface NodeKeyObject { privateKey: crypto.webcrypto.CryptoKey @@ -83,3 +84,7 @@ export interface Opts { certificateManager: CertificateManager localConfig: LocalConfig } + +export interface CommandHandlers { + parent: BaseCommand +} \ No newline at end of file diff --git a/test/test_util.ts b/test/test_util.ts index d30e0f399..3c88f8161 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -322,8 +322,8 @@ export function balanceQueryShouldSucceed (accountManager: AccountManager, cmd: expect(accountManager._nodeClient).not.to.be.null const balance = await new AccountBalanceQuery() - .setAccountId(accountManager._nodeClient.getOperator().accountId) - .execute(accountManager._nodeClient) + .setAccountId(accountManager._nodeClient.getOperator().accountId) + .execute(accountManager._nodeClient) expect(balance.hbars).not.be.null } catch (e) { @@ -343,9 +343,9 @@ export function accountCreationShouldSucceed (accountManager: AccountManager, no const amount = 100 const newAccount = await new AccountCreateTransaction() - .setKey(privateKey) - .setInitialBalance(Hbar.from(amount, HbarUnit.Hbar)) - .execute(accountManager._nodeClient) + .setKey(privateKey) + .setInitialBalance(Hbar.from(amount, HbarUnit.Hbar)) + .execute(accountManager._nodeClient) // Get the new account ID const getReceipt = await newAccount.getReceipt(accountManager._nodeClient) @@ -413,3 +413,19 @@ export function getK8Instance (configManager: ConfigManager) { return new K8(configManager, testLogger) } } + +export const testLocalConfigData = { + userEmailAddress: 'john.doe@example.com', + deployments: { + 'deployment': { + clusterAliases: ['cluster-1'], + }, + 'deployment-2': { + clusterAliases: ['cluster-2'], + }, + 'deployment-3': { + clusterAliases: ['cluster-3'], + } + }, + currentDeploymentName: 'deployment', +} diff --git a/test/unit/commands/context.test.ts b/test/unit/commands/context.test.ts new file mode 100644 index 000000000..f599befd5 --- /dev/null +++ b/test/unit/commands/context.test.ts @@ -0,0 +1,239 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ +import sinon from 'sinon' +import { describe, it, beforeEach } from 'mocha' +import { expect } from 'chai' + +import { ContextCommandTasks } from '../../../src/commands/context/tasks.js' +import { + AccountManager, CertificateManager, + ChartManager, + ConfigManager, + DependencyManager, + Helm, K8, KeyManager, LeaseManager, + LocalConfig, + PackageDownloader, PlatformInstaller, ProfileManager +} from '../../../src/core/index.js' +import { getTestCacheDir, testLocalConfigData } from '../../test_util.js' +import { BaseCommand } from '../../../src/commands/base.js' +import { flags } from '../../../src/commands/index.js' +import { SoloLogger } from '../../../src/core/logging.js' +import type Sinon from 'sinon' +import { type Opts } from '../../../src/types/index.js' +import fs from 'fs' +import { stringify } from 'yaml' +import { type Cluster, KubeConfig } from '@kubernetes/client-node' + + +describe('ContextCommandTasks unit tests', () => { + const filePath = `${getTestCacheDir('ContextCommandTasks')}/localConfig.yaml` + + const getBaseCommandOpts = () => { + const loggerStub = sinon.createStubInstance(SoloLogger) + const k8Stub = sinon.createStubInstance(K8) + const kubeConfigStub = sinon.createStubInstance(KubeConfig) + kubeConfigStub.getCurrentContext.returns('context-3') + kubeConfigStub.getCurrentCluster.returns({ + name: 'cluster-3', + caData: 'caData', + caFile: 'caFile', + server: 'server-3', + skipTLSVerify: true, + tlsServerName: 'tls-3', + } as Cluster) + + k8Stub.getKubeConfig.returns(kubeConfigStub) + + return { + logger: loggerStub, + helm: sinon.createStubInstance(Helm), + k8: k8Stub, + chartManager: sinon.createStubInstance(ChartManager), + configManager: sinon.createStubInstance(ConfigManager), + depManager: sinon.createStubInstance(DependencyManager), + localConfig: new LocalConfig(filePath, loggerStub), + downloader: sinon.createStubInstance(PackageDownloader), + keyManager: sinon.createStubInstance(KeyManager), + accountManager: sinon.createStubInstance(AccountManager), + platformInstaller: sinon.createStubInstance(PlatformInstaller), + profileManager: sinon.createStubInstance(ProfileManager), + leaseManager: sinon.createStubInstance(LeaseManager), + certificateManager: sinon.createStubInstance(CertificateManager), + } as Opts + } + + describe('updateLocalConfig', () => { + let tasks: ContextCommandTasks + let command: BaseCommand + let loggerStub: Sinon.SinonStubbedInstance + let localConfig: LocalConfig + let promptMap: Map + + + async function runUpdateLocalConfigTask (argv) { + const taskObj = tasks.updateLocalConfig(argv) + return taskObj.task({}, sinon.stub()) + } + + function getPromptMap (): Map { + return new Map() + .set(flags.namespace.name, sinon.stub().callsFake(() => { + return new Promise((resolve) => { + resolve('deployment-3') + }) + })) + .set(flags.clusterName.name, sinon.stub().callsFake(() => { + return new Promise((resolve) => { + resolve('cluster-3') + }) + })) + .set(flags.context.name, sinon.stub().callsFake(() => { + return new Promise((resolve) => { + resolve('context-3') + }) + })) + } + + afterEach(async () => { + await fs.promises.unlink(filePath) + }) + + beforeEach( async () => { + promptMap = getPromptMap() + loggerStub = sinon.createStubInstance(SoloLogger) + await fs.promises.writeFile(filePath, stringify(testLocalConfigData)) + command = new BaseCommand(getBaseCommandOpts()) + tasks = new ContextCommandTasks(command, promptMap) + }) + + it('should update local configuration with provided values', async () => { + const argv = { + [flags.namespace.name]: 'deployment-2', + [flags.clusterName.name]: 'cluster-2', + [flags.context.name]: 'context-2', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-2') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-2']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2') + }) + + it('should prompt for all flags if none are provided', async () => { + const argv = {} + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-3') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-3']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3') + expect(promptMap.get(flags.namespace.name)).to.have.been.calledOnce + expect(promptMap.get(flags.clusterName.name)).to.have.been.calledOnce + expect(promptMap.get(flags.context.name)).to.have.been.calledOnce + }) + + it('should prompt for namespace if no value is provided', async () => { + const argv = { + [flags.clusterName.name]: 'cluster-2', + [flags.context.name]: 'context-2', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-3') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-2']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2') + expect(promptMap.get(flags.namespace.name)).to.have.been.calledOnce + expect(promptMap.get(flags.clusterName.name)).to.not.have.been.called + expect(promptMap.get(flags.context.name)).to.not.have.been.called + }) + + it('should prompt for cluster if no value is provided', async () => { + const argv = { + [flags.namespace.name]: 'deployment-2', + [flags.context.name]: 'context-2', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-2') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-3']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2') + expect(promptMap.get(flags.namespace.name)).to.not.have.been.called + expect(promptMap.get(flags.clusterName.name)).to.have.been.calledOnce + expect(promptMap.get(flags.context.name)).to.not.have.been.called + }) + + it('should prompt for context if no value is provided', async () => { + const argv = { + [flags.namespace.name]: 'deployment-2', + [flags.clusterName.name]: 'cluster-2', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-2') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-2']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3') + expect(promptMap.get(flags.namespace.name)).to.not.have.been.called + expect(promptMap.get(flags.clusterName.name)).to.not.have.been.called + expect(promptMap.get(flags.context.name)).to.have.been.calledOnce + }) + + it('should use cluster from kubectl if no value is provided and quiet=true', async () => { + const argv = { + [flags.namespace.name]: 'deployment-2', + [flags.context.name]: 'context-2', + [flags.quiet.name]: 'true', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-2') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-3']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2') + expect(promptMap.get(flags.namespace.name)).to.not.have.been.called + expect(promptMap.get(flags.clusterName.name)).to.not.have.been.called + expect(promptMap.get(flags.context.name)).to.not.have.been.called + }) + + // TODO enable when namespace is retrieved from kubectl + xit('should use namespace from kubectl if no value is provided and quiet=true', async () => { + const argv = { + [flags.clusterName.name]: 'cluster-2', + [flags.context.name]: 'context-2', + [flags.quiet.name]: 'true', + } + + await runUpdateLocalConfigTask(argv) + localConfig = new LocalConfig(filePath, loggerStub) + + expect(localConfig.currentDeploymentName).to.equal('deployment-2') + expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-2']) + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3') + expect(promptMap.get(flags.namespace.name)).to.not.have.been.called + expect(promptMap.get(flags.clusterName.name)).to.not.have.been.called + expect(promptMap.get(flags.context.name)).to.not.have.been.called + }) + }) +}) diff --git a/test/unit/core/local_config.test.ts b/test/unit/core/local_config.test.ts index 27128a616..c1feddaa6 100644 --- a/test/unit/core/local_config.test.ts +++ b/test/unit/core/local_config.test.ts @@ -19,24 +19,12 @@ import fs from 'fs' import { stringify } from 'yaml' import { expect } from 'chai' import { MissingArgumentError, SoloError } from '../../../src/core/errors.js' -import { getTestCacheDir, testLogger } from '../../test_util.js' +import { getTestCacheDir, testLogger, testLocalConfigData } from '../../test_util.js' describe('LocalConfig', () => { let localConfig const filePath = `${getTestCacheDir('LocalConfig')}/localConfig.yaml` - const config = { - userEmailAddress: 'john.doe@example.com', - deployments: { - 'my-deployment': { - clusterAliases: ['cluster-1', 'context-1'], - }, - 'my-other-deployment': { - clusterAliases: ['cluster-2', 'context-2'], - } - }, - currentDeploymentName: 'my-deployment' - } - + const config = testLocalConfigData const expectFailedValidation = () => { try { @@ -87,10 +75,10 @@ describe('LocalConfig', () => { it('should set deployments', async () => { const newDeployments = { - 'my-deployment': { + 'deployment': { clusterAliases: ['cluster-1', 'context-1'], }, - 'my-new-deployment': { + 'deployment-2': { clusterAliases: ['cluster-3', 'context-3'], } } @@ -147,7 +135,7 @@ describe('LocalConfig', () => { }) it('should set current deployment', async () => { - const newCurrentDeployment = 'my-other-deployment' + const newCurrentDeployment = 'deployment-2' localConfig.setCurrentDeployment(newCurrentDeployment) expect(localConfig.currentDeploymentName).to.eq(newCurrentDeployment) From e5db03f0e9e10813b0bd13deb85c78f58910e0cb Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Mon, 25 Nov 2024 17:45:51 +0200 Subject: [PATCH 2/6] chore: update context connect command Signed-off-by: Ivo Yankov --- src/commands/context/handlers.ts | 2 +- src/commands/context/index.ts | 4 ++-- src/commands/context/tasks.ts | 32 ++++++++++++++++++++++-------- src/commands/prompts.ts | 13 ++++++------ src/core/config/local_config.ts | 15 +++++++------- test/unit/commands/context.test.ts | 13 ++++++++---- 6 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/commands/context/handlers.ts b/src/commands/context/handlers.ts index cdbd26a0c..134f9bc6b 100644 --- a/src/commands/context/handlers.ts +++ b/src/commands/context/handlers.ts @@ -30,7 +30,7 @@ export class ContextCommandHandlers implements CommandHandlers { this.tasks = tasks } - async use (argv: any) { + async connect (argv: any) { argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS) const action = helpers.commandActionBuilder([ diff --git a/src/commands/context/index.ts b/src/commands/context/index.ts index c97df3bca..3b20ee677 100644 --- a/src/commands/context/index.ts +++ b/src/commands/context/index.ts @@ -42,10 +42,10 @@ export class ContextCommand extends BaseCommand { builder: (yargs: any) => { return yargs .command(new YargsCommand({ - command: 'use', + command: 'connect', description: 'updates the local configuration by connecting a deployment to a k8s context', commandDef: this, - handler: 'use' + handler: 'connect' }, ContextFlags.USE_FLAGS)) .demandCommand(1, 'Select a context command') } diff --git a/src/commands/context/tasks.ts b/src/commands/context/tasks.ts index b8635de15..d0e74b2f8 100644 --- a/src/commands/context/tasks.ts +++ b/src/commands/context/tasks.ts @@ -36,24 +36,37 @@ export class ContextCommandTasks { this.parent.logger.info('Updating local configuration...') const isQuiet = !!argv[flags.quiet.name] - const isForcing = !!argv[flags.force.name] let currentDeploymentName = argv[flags.namespace.name] let clusterAliases = Templates.parseClusterAliases(argv[flags.clusterName.name]) let contextName = argv[flags.context.name] + const kubeContexts = await this.parent.getK8().getKubeConfig().getContexts() + if (isQuiet) { - const currentCluster = (await this.parent.getK8().getKubeConfig().getCurrentCluster()).name - if (!clusterAliases.length) clusterAliases = [currentCluster] + const currentCluster = (await this.parent.getK8().getKubeConfig().getCurrentCluster()) + if (!clusterAliases.length) clusterAliases = [currentCluster.name] if (!contextName) contextName = await this.parent.getK8().getKubeConfig().getCurrentContext() - // TODO properly get the active namespace - if (!currentDeploymentName) currentDeploymentName = currentCluster + if (!currentDeploymentName) { + const selectedContext = kubeContexts.find(e => e.name === contextName) + currentDeploymentName = selectedContext && selectedContext.namespace ? selectedContext.namespace : 'default' + } } else { - if (!clusterAliases.length) clusterAliases = Templates.parseClusterAliases(await (this.promptMap.get(flags.clusterName.name))(task, clusterAliases)) - if (!contextName) contextName = await (this.promptMap.get(flags.context.name))(task, contextName) - if (!currentDeploymentName) currentDeploymentName = await (this.promptMap.get(flags.namespace.name))(task, currentDeploymentName) + if (!clusterAliases.length) { + const prompt = this.promptMap.get(flags.clusterName.name) + const unparsedClusterAliases = await prompt(task, clusterAliases) + clusterAliases = Templates.parseClusterAliases(unparsedClusterAliases) + } + if (!contextName) { + const prompt = this.promptMap.get(flags.context.name) + contextName = await prompt(task, kubeContexts.map(c => c.name), contextName) + } + if (!currentDeploymentName) { + const prompt = this.promptMap.get(flags.namespace.name) + currentDeploymentName = await prompt(task, currentDeploymentName) + } } // Select current deployment @@ -66,6 +79,9 @@ export class ContextCommandTasks { this.parent.getK8().getKubeConfig().setCurrentContext(contextName) + this.parent.logger.info(`currentDeploymentName: ${currentDeploymentName}`) + this.parent.logger.info(`contextName: ${contextName}`) + this.parent.logger.info(`clusterAliases: ${clusterAliases.join(' ')}`) this.parent.logger.info('Save LocalConfig file') await this.parent.getLocalConfig().write() }) diff --git a/src/commands/prompts.ts b/src/commands/prompts.ts index e8357b378..a8e977674 100644 --- a/src/commands/prompts.ts +++ b/src/commands/prompts.ts @@ -447,12 +447,13 @@ export async function promptEndpointType (task: ListrTaskWrapper, flags.endpointType.name) } -export async function promptContext (task: ListrTaskWrapper, input: any) { - return await promptText(task, input, - flags.context.definition.defaultValue, - 'Enter the context name: ', - null, - flags.context.name) +export async function promptContext (task: ListrTaskWrapper, contexts: string[], input: any) { + return await task.prompt(ListrEnquirerPromptAdapter).run({ + type: 'select', + name: 'context', + message: 'Select kubectl context', + choices: contexts + }) } export async function promptClusterName (task: ListrTaskWrapper, input: any) { diff --git a/src/core/config/local_config.ts b/src/core/config/local_config.ts index be2909489..da73800a9 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -120,6 +120,7 @@ export class LocalConfig implements LocalConfigData { } public promptLocalConfigTask (k8, argv): Task { + const self = this return new Task('Prompt local configuration', async (ctx, task) => { let userEmailAddress = argv[flags.userEmailAddress.name] if (!userEmailAddress) userEmailAddress = await promptUserEmailAddress(task, userEmailAddress) @@ -135,13 +136,13 @@ export class LocalConfig implements LocalConfigData { clusterAliases: Templates.parseClusterAliases(deploymentClusters) } - this.userEmailAddress = userEmailAddress - this.deployments = deployments - this.currentDeploymentName = deploymentName - this.validate() - await this.write() + self.userEmailAddress = userEmailAddress + self.deployments = deployments + self.currentDeploymentName = deploymentName + self.validate() + await self.write() - return this - }, this.skipPromptTask) as Task + return self + }, self.skipPromptTask) as Task } } diff --git a/test/unit/commands/context.test.ts b/test/unit/commands/context.test.ts index f599befd5..31b392ef1 100644 --- a/test/unit/commands/context.test.ts +++ b/test/unit/commands/context.test.ts @@ -39,13 +39,19 @@ import { stringify } from 'yaml' import { type Cluster, KubeConfig } from '@kubernetes/client-node' -describe('ContextCommandTasks unit tests', () => { +describe.only('ContextCommandTasks unit tests', () => { const filePath = `${getTestCacheDir('ContextCommandTasks')}/localConfig.yaml` const getBaseCommandOpts = () => { const loggerStub = sinon.createStubInstance(SoloLogger) const k8Stub = sinon.createStubInstance(K8) const kubeConfigStub = sinon.createStubInstance(KubeConfig) + kubeConfigStub.getContexts.returns([ + { cluster: 'cluster-1', user: 'user-1', name: 'context-1', namespace: 'deployment-1' }, + { cluster: 'cluster-2', user: 'user-2', name: 'context-2', namespace: 'deployment-2' }, + { cluster: 'cluster-3', user: 'user-3', name: 'context-3', namespace: 'deployment-3' }, + ]) + kubeConfigStub.getCurrentContext.returns('context-3') kubeConfigStub.getCurrentContext.returns('context-3') kubeConfigStub.getCurrentCluster.returns({ name: 'cluster-3', @@ -217,8 +223,7 @@ describe('ContextCommandTasks unit tests', () => { expect(promptMap.get(flags.context.name)).to.not.have.been.called }) - // TODO enable when namespace is retrieved from kubectl - xit('should use namespace from kubectl if no value is provided and quiet=true', async () => { + it('should use namespace from kubectl if no value is provided and quiet=true', async () => { const argv = { [flags.clusterName.name]: 'cluster-2', [flags.context.name]: 'context-2', @@ -230,7 +235,7 @@ describe('ContextCommandTasks unit tests', () => { expect(localConfig.currentDeploymentName).to.equal('deployment-2') expect(localConfig.getCurrentDeployment().clusterAliases).to.deep.equal(['cluster-2']) - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3') + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2') expect(promptMap.get(flags.namespace.name)).to.not.have.been.called expect(promptMap.get(flags.clusterName.name)).to.not.have.been.called expect(promptMap.get(flags.context.name)).to.not.have.been.called From 74bd85bfc920c5c0705bd4b27194bb1bed4495bd Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Tue, 26 Nov 2024 11:52:33 +0200 Subject: [PATCH 3/6] feat: add descriptive error messages for LocalConfig Signed-off-by: Ivo Yankov --- src/core/config/local_config.ts | 40 ++++++++++++++++++----------- src/core/error_messages.ts | 23 +++++++++++++++++ src/core/validator_decorators.ts | 2 +- test/unit/commands/context.test.ts | 2 +- test/unit/core/local_config.test.ts | 23 +++++++++-------- 5 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 src/core/error_messages.ts diff --git a/src/core/config/local_config.ts b/src/core/config/local_config.ts index da73800a9..8d1b516d9 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -15,7 +15,6 @@ * */ import { IsEmail, IsNotEmpty, IsObject, IsString, validateSync } from 'class-validator' -import { type ListrTask } from 'listr2' import fs from 'fs' import * as yaml from 'yaml' import { flags } from '../../commands/index.js' @@ -26,21 +25,29 @@ import { type SoloLogger } from '../logging.js' import { Task } from '../task.js' import { IsDeployments } from '../validator_decorators.js' import { Templates } from '../templates.js' +import { ErrorMessages } from '../error_messages.js' export class LocalConfig implements LocalConfigData { - @IsNotEmpty() - @IsEmail() + @IsEmail({}, { + message: ErrorMessages.LOCAL_CONFIG_INVALID_EMAIL + }) userEmailAddress: string // The string is the name of the deployment, will be used as the namespace, // so it needs to be available in all targeted clusters + @IsDeployments({ + message: ErrorMessages.LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT + }) @IsNotEmpty() - @IsObject() - @IsDeployments() + @IsObject({ + message: ErrorMessages.LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT + }) deployments: Deployments + @IsString({ + message: ErrorMessages.LOCAL_CONFIG_CURRENT_DEPLOYMENT_DOES_NOT_EXIST + }) @IsNotEmpty() - @IsString() currentDeploymentName : string private readonly skipPromptTask: boolean = false @@ -56,7 +63,7 @@ export class LocalConfig implements LocalConfigData { for(const key in parsedConfig) { if (!allowedKeys.includes(key)) { - throw new SoloError('Validation of local config failed') + throw new SoloError(ErrorMessages.LOCAL_CONFIG_GENERIC) } this[key] = parsedConfig[key] } @@ -67,20 +74,23 @@ export class LocalConfig implements LocalConfigData { } private validate () { - const genericMessage = 'Validation of local config failed' const errors = validateSync(this, {}) if (errors.length) { - throw new SoloError(genericMessage) + // throw the first error: + const prop = Object.keys(errors[0]?.constraints) + if (prop[0]) { + throw new SoloError(errors[0].constraints[prop[0]]) + } + else { + throw new SoloError(ErrorMessages.LOCAL_CONFIG_GENERIC) + } } - try { - // Custom validations: - if (!this.deployments[this.currentDeploymentName]) { - throw new SoloError(genericMessage) - } + // Custom validations: + if (!this.deployments[this.currentDeploymentName]) { + throw new SoloError(ErrorMessages.LOCAL_CONFIG_CURRENT_DEPLOYMENT_DOES_NOT_EXIST) } - catch(e: any) { throw new SoloError(genericMessage) } } public setUserEmailAddress (emailAddress: string): this { diff --git a/src/core/error_messages.ts b/src/core/error_messages.ts new file mode 100644 index 000000000..71a83ce5b --- /dev/null +++ b/src/core/error_messages.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + * + */ + +export const ErrorMessages = { + LOCAL_CONFIG_CURRENT_DEPLOYMENT_DOES_NOT_EXIST: 'The selected namespace does not correspond to a deployment in the local configuration', + LOCAL_CONFIG_GENERIC: 'Validation of local config failed', + LOCAL_CONFIG_INVALID_EMAIL: 'Invalid email address provided', + LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT: 'Wrong deployments format' +} \ No newline at end of file diff --git a/src/core/validator_decorators.ts b/src/core/validator_decorators.ts index ec8187e38..535955557 100644 --- a/src/core/validator_decorators.ts +++ b/src/core/validator_decorators.ts @@ -15,6 +15,7 @@ * */ import { registerDecorator, type ValidationOptions, type ValidationArguments } from 'class-validator' +import { ErrorMessages } from "./error_messages.js"; const isObject = (obj) => obj === Object(obj) @@ -26,7 +27,6 @@ export const IsDeployments = (validationOptions?: ValidationOptions) => { propertyName: propertyName, constraints: [], options: { - message: 'Wrong deployments format', ...validationOptions, }, validator: { diff --git a/test/unit/commands/context.test.ts b/test/unit/commands/context.test.ts index 31b392ef1..14e54b128 100644 --- a/test/unit/commands/context.test.ts +++ b/test/unit/commands/context.test.ts @@ -39,7 +39,7 @@ import { stringify } from 'yaml' import { type Cluster, KubeConfig } from '@kubernetes/client-node' -describe.only('ContextCommandTasks unit tests', () => { +describe('ContextCommandTasks unit tests', () => { const filePath = `${getTestCacheDir('ContextCommandTasks')}/localConfig.yaml` const getBaseCommandOpts = () => { diff --git a/test/unit/core/local_config.test.ts b/test/unit/core/local_config.test.ts index c1feddaa6..de00761f7 100644 --- a/test/unit/core/local_config.test.ts +++ b/test/unit/core/local_config.test.ts @@ -20,20 +20,21 @@ import { stringify } from 'yaml' import { expect } from 'chai' import { MissingArgumentError, SoloError } from '../../../src/core/errors.js' import { getTestCacheDir, testLogger, testLocalConfigData } from '../../test_util.js' +import {ErrorMessages} from "../../../src/core/error_messages.js"; describe('LocalConfig', () => { let localConfig const filePath = `${getTestCacheDir('LocalConfig')}/localConfig.yaml` const config = testLocalConfigData - const expectFailedValidation = () => { + const expectFailedValidation = (expectedMessage) => { try { new LocalConfig(filePath, testLogger) expect.fail('Expected an error to be thrown') } catch(error) { expect(error).to.be.instanceOf(SoloError) - expect(error.message).to.equal('Validation of local config failed') + expect(error.message).to.equal(expectedMessage) } } @@ -176,41 +177,41 @@ describe('LocalConfig', () => { it('should throw a validation error if the config file is not a valid LocalConfig', async () => { // without any known properties await fs.promises.writeFile(filePath, 'foo: bar') - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_GENERIC) // with extra property await fs.promises.writeFile(filePath, stringify({ ...config, foo: 'bar' })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_GENERIC) }) it('should throw a validation error if userEmailAddress is not a valid email', async () => { await fs.promises.writeFile(filePath, stringify({ ...config, userEmailAddress: 'foo' })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_INVALID_EMAIL) await fs.promises.writeFile(filePath, stringify({ ...config, userEmailAddress: 5 })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_INVALID_EMAIL) }) it('should throw a validation error if deployments format is not correct', async () => { await fs.promises.writeFile(filePath, stringify({ ...config, deployments: 'foo' })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT) await fs.promises.writeFile(filePath, stringify({ ...config, deployments: { 'foo': 'bar' } })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT) await fs.promises.writeFile(filePath, stringify({ ...config, deployments: [{ 'foo': 'bar' }] }) ) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_INVALID_DEPLOYMENTS_FORMAT) }) it('should throw a validation error if currentDeploymentName format is not correct', async () => { await fs.promises.writeFile(filePath, stringify({ ...config, currentDeploymentName: 5 })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_CURRENT_DEPLOYMENT_DOES_NOT_EXIST) await fs.promises.writeFile(filePath, stringify({ ...config, currentDeploymentName: ['foo', 'bar'] })) - expectFailedValidation() + expectFailedValidation(ErrorMessages.LOCAL_CONFIG_CURRENT_DEPLOYMENT_DOES_NOT_EXIST) }) }) \ No newline at end of file From bd41b5dabbfcc8e926ba918554368d5bf3e968c5 Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Tue, 26 Nov 2024 11:53:33 +0200 Subject: [PATCH 4/6] chore: format Signed-off-by: Ivo Yankov --- docs/content/User/SDK.md | 13 +++++++++++-- examples/sdk-network-connection/README.md | 2 ++ src/core/validator_decorators.ts | 1 - test/unit/core/local_config.test.ts | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/content/User/SDK.md b/docs/content/User/SDK.md index ec3d07785..29611dba3 100644 --- a/docs/content/User/SDK.md +++ b/docs/content/User/SDK.md @@ -1,4 +1,5 @@ # Instructions for using Solo with Hedera JavaScript SDK + First, please follow solo repository README to install solo and Docker Desktop. You also need to install the Taskfile tool following the instructions here: https://taskfile.dev/installation/ @@ -9,10 +10,13 @@ Then we start with launching a local Solo network with the following commands: # launch a local Solo network with mirror node and hedera explorer task default-with-mirror-node ``` + Then create a new test account with the following command: + ``` npm run solo-test -- account create -n solo-e2e --hbar-amount 100 ``` + The output would be similar to the following: ```bash @@ -26,10 +30,13 @@ The output would be similar to the following: ``` Then use the following commmand to get private key of the account `0.0.1007`: + ```bash npm run solo-test -- account get --account-id 0.0.1007 -n solo-e2e --private-key ``` + The output would be similar to the following: + ```bash { "accountId": "0.0.1007", @@ -52,7 +59,8 @@ OPERATOR_KEY="302a300506032b65700321001d8978e647aca1195c54a4d3d5dc469b95666de14e # Hedera Network HEDERA_NETWORK="local-node" ``` -Make sure to assign the value of accountId to OPERATOR_ID and the value of privateKey to OPERATOR_KEY. + +Make sure to assign the value of accountId to OPERATOR\_ID and the value of privateKey to OPERATOR\_KEY. Then try the following command to run the test @@ -69,9 +77,11 @@ account id = 0.0.1009 ``` Or try the topic creation example: + ```bash node examples/create-topic.js ``` + The output should be similar to the following: ```bash @@ -89,4 +99,3 @@ Finally, after done with using solo, using the following command to tear down th ```bash task clean ``` - diff --git a/examples/sdk-network-connection/README.md b/examples/sdk-network-connection/README.md index eafd68dbb..f1bc698b8 100644 --- a/examples/sdk-network-connection/README.md +++ b/examples/sdk-network-connection/README.md @@ -1,11 +1,13 @@ # Solo Network Connection Example ## pre-requirements: + 1. fork or download the solo repository: https://github.com/hashgraph/solo 2. have NodeJS 20+ and NPM installed: https://nodejs.org/en/download/package-manager 3. have Taskfile installed: https://taskfile.dev/installation/ ## running the Solo connection example: + 1. open a terminal and cd into the root of the solo repo directory 2. run: `task default-with-mirror` 3. run: `cd examples/sdk-network-connection` diff --git a/src/core/validator_decorators.ts b/src/core/validator_decorators.ts index 535955557..89cb13562 100644 --- a/src/core/validator_decorators.ts +++ b/src/core/validator_decorators.ts @@ -15,7 +15,6 @@ * */ import { registerDecorator, type ValidationOptions, type ValidationArguments } from 'class-validator' -import { ErrorMessages } from "./error_messages.js"; const isObject = (obj) => obj === Object(obj) diff --git a/test/unit/core/local_config.test.ts b/test/unit/core/local_config.test.ts index de00761f7..74316c862 100644 --- a/test/unit/core/local_config.test.ts +++ b/test/unit/core/local_config.test.ts @@ -20,7 +20,7 @@ import { stringify } from 'yaml' import { expect } from 'chai' import { MissingArgumentError, SoloError } from '../../../src/core/errors.js' import { getTestCacheDir, testLogger, testLocalConfigData } from '../../test_util.js' -import {ErrorMessages} from "../../../src/core/error_messages.js"; +import { ErrorMessages } from '../../../src/core/error_messages.js' describe('LocalConfig', () => { let localConfig From 1083862842bfd8b7abb650e437c16e9eee6fd9ea Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Thu, 28 Nov 2024 16:22:07 +0200 Subject: [PATCH 5/6] chore: addressing comments Signed-off-by: Ivo Yankov --- src/commands/context/flags.ts | 6 - src/commands/context/tasks.ts | 5 +- src/commands/flags.ts | 2 +- src/core/k8.ts | 236 +++++++++++++++++----------------- src/core/task.ts | 6 +- 5 files changed, 121 insertions(+), 134 deletions(-) diff --git a/src/commands/context/flags.ts b/src/commands/context/flags.ts index 0ec7f6751..af8c12483 100644 --- a/src/commands/context/flags.ts +++ b/src/commands/context/flags.ts @@ -17,12 +17,6 @@ import * as flags from '../flags.js' -export const DEFAULT_FLAGS = { - requiredFlags: [], - requiredFlagsWithDisabledPrompt: [flags.namespace, flags.cacheDir, flags.releaseTag], - optionalFlags: [flags.devMode] -} - export const USE_FLAGS = { requiredFlags: [], requiredFlagsWithDisabledPrompt: [], diff --git a/src/commands/context/tasks.ts b/src/commands/context/tasks.ts index d0e74b2f8..f4150ebd6 100644 --- a/src/commands/context/tasks.ts +++ b/src/commands/context/tasks.ts @@ -79,10 +79,7 @@ export class ContextCommandTasks { this.parent.getK8().getKubeConfig().setCurrentContext(contextName) - this.parent.logger.info(`currentDeploymentName: ${currentDeploymentName}`) - this.parent.logger.info(`contextName: ${contextName}`) - this.parent.logger.info(`clusterAliases: ${clusterAliases.join(' ')}`) - this.parent.logger.info('Save LocalConfig file') + this.parent.logger.info(`Save LocalConfig file: [currentDeploymentName: ${currentDeploymentName}, contextName: ${contextName}, clusterAliases: ${clusterAliases.join(' ')}]`) await this.parent.getLocalConfig().write() }) } diff --git a/src/commands/flags.ts b/src/commands/flags.ts index 13344955d..5aa408bdb 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -752,7 +752,7 @@ export const context: CommandFlag = { constName: 'contextName', name: 'context', definition: { - describe: 'The kind context name to be used', + describe: 'The Kubernetes context name to be used', defaultValue: '', type: 'string' } diff --git a/src/core/k8.ts b/src/core/k8.ts index 3e1f2ef38..eac085c7c 100644 --- a/src/core/k8.ts +++ b/src/core/k8.ts @@ -45,7 +45,7 @@ interface TDirectoryData {directory: boolean; owner: string; group: string; size */ export class K8 { static PodReadyCondition = new Map() - .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) + .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) private kubeConfig!: k8s.KubeConfig kubeClient!: k8s.CoreV1Api private coordinationApiClient: k8s.CoordinationV1Api @@ -76,7 +76,7 @@ export class K8 { if (!this.kubeConfig.getCurrentContext()) { throw new SoloError('No active kubernetes context found. ' + - 'Please set current kubernetes context.') + 'Please set current kubernetes context.') } if (!this.kubeConfig.getCurrentCluster()) { @@ -238,12 +238,12 @@ export class K8 { const ns = this._getNamespace() const labelSelector = labels.join(',') const result = await this.kubeClient.listNamespacedSecret( - ns, - undefined, - undefined, - undefined, - undefined, - labelSelector + ns, + undefined, + undefined, + undefined, + undefined, + labelSelector ) return result.body.items @@ -442,17 +442,17 @@ export class K8 { */ async hasDir (podName: string, containerName: string, destPath: string) { return await this.execContainer( - podName, - containerName, - ['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] + podName, + containerName, + ['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] ) === 'true' } mkdir (podName: PodName, containerName: string, destPath: string) { return this.execContainer( - podName, - containerName, - ['bash', '-c', 'mkdir -p "' + destPath + '"'] + podName, + containerName, + ['bash', '-c', 'mkdir -p "' + destPath + '"'] ) } @@ -493,7 +493,7 @@ export class K8 { } registerOutputPassthroughStreamOnData (localContext: LocalContextObject, messagePrefix: string, - outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { + outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { outputPassthroughStream.on('data', (chunk) => { this.logger.debug(`${messagePrefix} received chunk size=${chunk.length}`) const canWrite = outputFileStream.write(chunk) // Write chunk to file and check if buffer is full @@ -505,7 +505,7 @@ export class K8 { } registerOutputFileStreamOnDrain (localContext: LocalContextObject, messagePrefix: string, - outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { + outputPassthroughStream: stream.PassThrough, outputFileStream: fs.WriteStream) { outputFileStream.on('drain', () => { outputPassthroughStream.resume() this.logger.debug(`${messagePrefix} stream drained, resume write`) @@ -565,28 +565,28 @@ export class K8 { inputStream.pipe(inputPassthroughStream) execInstance.exec(namespace, podName, containerName, command, null, errPassthroughStream, inputPassthroughStream, false, - ({ status }) => self.handleCallback(status, localContext, messagePrefix)) - .then(conn => { - self.logger.info(`${messagePrefix} connection established`) - localContext.connection = conn + ({ status }) => self.handleCallback(status, localContext, messagePrefix)) + .then(conn => { + self.logger.info(`${messagePrefix} connection established`) + localContext.connection = conn - self.registerConnectionOnError(localContext, messagePrefix, conn) + self.registerConnectionOnError(localContext, messagePrefix, conn) - self.registerConnectionOnMessage(messagePrefix) + self.registerConnectionOnMessage(messagePrefix) - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) + } - // Cleanup temp file after successful copy - inputPassthroughStream.end() // End the passthrough stream - self._deleteTempFile(tmpFile) // Cleanup temp file - self.logger.info(`${messagePrefix} Successfully copied!`) - return resolve(true) - }) + // Cleanup temp file after successful copy + inputPassthroughStream.end() // End the passthrough stream + self._deleteTempFile(tmpFile) // Cleanup temp file + self.logger.info(`${messagePrefix} Successfully copied!`) + return resolve(true) }) + }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -666,59 +666,59 @@ export class K8 { self.logger.debug(`${messagePrefix} running...`) execInstance.exec( - namespace, - podName, - containerName, - command, - outputFileStream, - errPassthroughStream, - null, - false, - ({ status }) => { - if (status === 'Failure') { - self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} Failure occurred`) - } - self.logger.debug(`${messagePrefix} callback(status)=${status}`) - - }) - .then(conn => { - self.logger.debug(`${messagePrefix} connection established`) - localContext.connection = conn + namespace, + podName, + containerName, + command, + outputFileStream, + errPassthroughStream, + null, + false, + ({ status }) => { + if (status === 'Failure') { + self._deleteTempFile(tmpFile) + return self.exitWithError(localContext, `${messagePrefix} Failure occurred`) + } + self.logger.debug(`${messagePrefix} callback(status)=${status}`) - conn.on('error', (e) => { - self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} failed, connection error: ${e.message}`) - }) + }) + .then(conn => { + self.logger.debug(`${messagePrefix} connection established`) + localContext.connection = conn - self.registerConnectionOnMessage(messagePrefix) + conn.on('error', (e) => { + self._deleteTempFile(tmpFile) + return self.exitWithError(localContext, `${messagePrefix} failed, connection error: ${e.message}`) + }) - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } + self.registerConnectionOnMessage(messagePrefix) - outputFileStream.end() - outputFileStream.close(() => { - try { - fs.copyFileSync(tmpFile, destPath) + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) + } - self._deleteTempFile(tmpFile) + outputFileStream.end() + outputFileStream.close(() => { + try { + fs.copyFileSync(tmpFile, destPath) - const stat = fs.statSync(destPath) - if (stat && stat.size === srcFileSize) { - self.logger.debug(`${messagePrefix} finished`) - return resolve(true) - } + self._deleteTempFile(tmpFile) - return self.exitWithError(localContext, `${messagePrefix} files did not match, srcFileSize=${srcFileSize}, stat.size=${stat?.size}`) - } catch (e: Error | any) { - return self.exitWithError(localContext, `${messagePrefix} failed to complete download`) + const stat = fs.statSync(destPath) + if (stat && stat.size === srcFileSize) { + self.logger.debug(`${messagePrefix} finished`) + return resolve(true) } - }) + + return self.exitWithError(localContext, `${messagePrefix} files did not match, srcFileSize=${srcFileSize}, stat.size=${stat?.size}`) + } catch (e: Error | any) { + return self.exitWithError(localContext, `${messagePrefix} failed to complete download`) + } }) }) + }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -771,39 +771,39 @@ export class K8 { self.logger.debug(`${messagePrefix} running...`) execInstance.exec( - namespace, - podName, - containerName, - command, - outputFileStream, - errPassthroughStream, - null, - false, - ({ status }) => self.handleCallback(status, localContext, messagePrefix)) - .then(conn => { - self.logger.debug(`${messagePrefix} connection established`) - localContext.connection = conn - - self.registerConnectionOnError(localContext, messagePrefix, conn) - - self.registerConnectionOnMessage(messagePrefix) - - conn.on('close', (code, reason) => { - self.logger.debug(`${messagePrefix} connection closed`) - if (!localContext.errorMessage) { - if (code !== 1000) { // code 1000 is the success code - return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) - } - - outputFileStream.end() - outputFileStream.close(() => { - self.logger.debug(`${messagePrefix} finished`) - const outData = fs.readFileSync(tmpFile) - return resolve(outData.toString()) - }) + namespace, + podName, + containerName, + command, + outputFileStream, + errPassthroughStream, + null, + false, + ({ status }) => self.handleCallback(status, localContext, messagePrefix)) + .then(conn => { + self.logger.debug(`${messagePrefix} connection established`) + localContext.connection = conn + + self.registerConnectionOnError(localContext, messagePrefix, conn) + + self.registerConnectionOnMessage(messagePrefix) + + conn.on('close', (code, reason) => { + self.logger.debug(`${messagePrefix} connection closed`) + if (!localContext.errorMessage) { + if (code !== 1000) { // code 1000 is the success code + return self.exitWithError(localContext, `${messagePrefix} failed with code=${code}, reason=${reason}`) } - }) + + outputFileStream.end() + outputFileStream.close(() => { + self.logger.debug(`${messagePrefix} finished`) + const outData = fs.readFileSync(tmpFile) + return resolve(outData.toString()) + }) + } }) + }) self.registerErrorStreamOnData(localContext, errPassthroughStream) @@ -1115,17 +1115,17 @@ export class K8 { */ async getSecret (namespace: string, labelSelector: string) { const result = await this.kubeClient.listNamespacedSecret( - namespace, - undefined, - undefined, - undefined, - undefined, - labelSelector, - undefined, - undefined, - undefined, - undefined, - 5 * MINUTES + namespace, + undefined, + undefined, + undefined, + undefined, + labelSelector, + undefined, + undefined, + undefined, + undefined, + 5 * MINUTES ) if (result.response.statusCode === 200 && result.body.items && result.body.items.length > 0) { diff --git a/src/core/task.ts b/src/core/task.ts index e96410399..3b6333424 100644 --- a/src/core/task.ts +++ b/src/core/task.ts @@ -15,9 +15,5 @@ * */ export class Task { - constructor (public title: string, public task: Function, public skip: Function | boolean = false) { - this.title = title - this.task = task - this.skip = skip - } + constructor (public title: string, public task: Function, public skip: Function | boolean = false) { } } From d48164ca69af9f3dfbd87b301368bc718fcf8c34 Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Thu, 28 Nov 2024 17:45:58 +0200 Subject: [PATCH 6/6] feat: cache context results in k8 Signed-off-by: Ivo Yankov --- src/commands/context/tasks.ts | 2 +- src/core/config/local_config_data.ts | 8 ++++---- src/core/k8.ts | 17 ++++++++++++++--- test/e2e/integration/core/k8_e2e.test.ts | 5 +++++ test/unit/commands/context.test.ts | 4 ++-- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/commands/context/tasks.ts b/src/commands/context/tasks.ts index f4150ebd6..e39af341d 100644 --- a/src/commands/context/tasks.ts +++ b/src/commands/context/tasks.ts @@ -41,7 +41,7 @@ export class ContextCommandTasks { let clusterAliases = Templates.parseClusterAliases(argv[flags.clusterName.name]) let contextName = argv[flags.context.name] - const kubeContexts = await this.parent.getK8().getKubeConfig().getContexts() + const kubeContexts = await this.parent.getK8().getContexts() if (isQuiet) { const currentCluster = (await this.parent.getK8().getKubeConfig().getCurrentCluster()) diff --git a/src/core/config/local_config_data.ts b/src/core/config/local_config_data.ts index 2fe4c8403..064c8e859 100644 --- a/src/core/config/local_config_data.ts +++ b/src/core/config/local_config_data.ts @@ -21,10 +21,10 @@ export interface Deployment { // an alias for the cluster, provided during the configuration // of the deployment, must be unique -export type Deployments = Record; +export type Deployments = Record export interface LocalConfigData { - userEmailAddress: string; - deployments: Deployments; - currentDeploymentName: string; + userEmailAddress: string + deployments: Deployments + currentDeploymentName: string } \ No newline at end of file diff --git a/src/core/k8.ts b/src/core/k8.ts index eac085c7c..f08102a33 100644 --- a/src/core/k8.ts +++ b/src/core/k8.ts @@ -23,7 +23,7 @@ import { flags } from '../commands/index.js' import { SoloError, IllegalArgumentError, MissingArgumentError } from './errors.js' import * as tar from 'tar' import { v4 as uuid4 } from 'uuid' -import { type V1Lease, V1ObjectMeta, V1Secret, Config } from '@kubernetes/client-node' +import { type V1Lease, V1ObjectMeta, V1Secret, type Context } from '@kubernetes/client-node' import { sleep } from './helpers.js' import { type ConfigManager, constants } from './index.js' import * as stream from 'node:stream' @@ -44,6 +44,8 @@ interface TDirectoryData {directory: boolean; owner: string; group: string; size * For parallel execution, create separate instances by invoking clone() */ export class K8 { + private _cachedContexts: Context[] + static PodReadyCondition = new Map() .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) private kubeConfig!: k8s.KubeConfig @@ -320,15 +322,24 @@ export class K8 { * Get a list of contexts * @returns a list of context names */ - getContexts () { + getContextNames () : string[] { const contexts: string[] = [] - for (const context of this.kubeConfig.getContexts()) { + + for (const context of this.getContexts()) { contexts.push(context.name) } return contexts } + getContexts () :Context[] { + if (!this._cachedContexts) { + this._cachedContexts = this.kubeConfig.getContexts() + } + + return this._cachedContexts + } + /** * List files and directories in a container * diff --git a/test/e2e/integration/core/k8_e2e.test.ts b/test/e2e/integration/core/k8_e2e.test.ts index 7aa91b623..9e5d76444 100644 --- a/test/e2e/integration/core/k8_e2e.test.ts +++ b/test/e2e/integration/core/k8_e2e.test.ts @@ -131,6 +131,11 @@ describe('K8', () => { expect(namespaces).to.contain(constants.DEFAULT_NAMESPACE) }).timeout(defaultTimeout) + it('should be able to list context names', () => { + const contexts = k8.getContextNames() + expect(contexts).not.to.have.lengthOf(0) + }).timeout(defaultTimeout) + it('should be able to list contexts', () => { const contexts = k8.getContexts() expect(contexts).not.to.have.lengthOf(0) diff --git a/test/unit/commands/context.test.ts b/test/unit/commands/context.test.ts index 14e54b128..adfbff478 100644 --- a/test/unit/commands/context.test.ts +++ b/test/unit/commands/context.test.ts @@ -45,12 +45,12 @@ describe('ContextCommandTasks unit tests', () => { const getBaseCommandOpts = () => { const loggerStub = sinon.createStubInstance(SoloLogger) const k8Stub = sinon.createStubInstance(K8) - const kubeConfigStub = sinon.createStubInstance(KubeConfig) - kubeConfigStub.getContexts.returns([ + k8Stub.getContexts.returns([ { cluster: 'cluster-1', user: 'user-1', name: 'context-1', namespace: 'deployment-1' }, { cluster: 'cluster-2', user: 'user-2', name: 'context-2', namespace: 'deployment-2' }, { cluster: 'cluster-3', user: 'user-3', name: 'context-3', namespace: 'deployment-3' }, ]) + const kubeConfigStub = sinon.createStubInstance(KubeConfig) kubeConfigStub.getCurrentContext.returns('context-3') kubeConfigStub.getCurrentContext.returns('context-3') kubeConfigStub.getCurrentCluster.returns({