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/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..af8c12483 --- /dev/null +++ b/src/commands/context/flags.ts @@ -0,0 +1,24 @@ +/** + * 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 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..134f9bc6b --- /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 connect (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..3b20ee677 --- /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: 'connect', + description: 'updates the local configuration by connecting a deployment to a k8s context', + commandDef: this, + 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 new file mode 100644 index 000000000..e39af341d --- /dev/null +++ b/src/commands/context/tasks.ts @@ -0,0 +1,101 @@ +/** + * 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] + + 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().getContexts() + + if (isQuiet) { + const currentCluster = (await this.parent.getK8().getKubeConfig().getCurrentCluster()) + if (!clusterAliases.length) clusterAliases = [currentCluster.name] + if (!contextName) contextName = await this.parent.getK8().getKubeConfig().getCurrentContext() + + if (!currentDeploymentName) { + const selectedContext = kubeContexts.find(e => e.name === contextName) + currentDeploymentName = selectedContext && selectedContext.namespace ? selectedContext.namespace : 'default' + } + } + else { + 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 + 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: [currentDeploymentName: ${currentDeploymentName}, contextName: ${contextName}, clusterAliases: ${clusterAliases.join(' ')}]`) + 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 ac84e0d09..5aa408bdb 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -748,6 +748,16 @@ export const userEmailAddress: CommandFlag = { } } +export const context: CommandFlag = { + constName: 'contextName', + name: 'context', + definition: { + describe: 'The Kubernetes context name to be used', + defaultValue: '', + type: 'string' + } +} + export const deploymentName: CommandFlag = { constName: 'deploymentName', name: 'deployment-name', @@ -775,9 +785,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' } @@ -788,9 +798,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' } @@ -801,9 +811,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' } @@ -814,9 +824,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 576b0179f..4319bdf28 100644 --- a/src/commands/node/handlers.ts +++ b/src/commands/node/handlers.ts @@ -44,9 +44,10 @@ 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' import { NodeSubcommandType } from '../../core/enumerations.js' -export class NodeCommandHandlers { +export class NodeCommandHandlers implements CommandHandlers { private readonly accountManager: AccountManager private readonly configManager: ConfigManager private readonly platformInstaller: PlatformInstaller @@ -328,20 +329,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 cd5e1bc49..aab66c82d 100644 --- a/src/commands/node/tasks.ts +++ b/src/commands/node/tasks.ts @@ -89,10 +89,10 @@ export class NodeCommandTasks { private readonly prepareValuesFiles: any constructor (opts: { - logger: SoloLogger; accountManager: AccountManager; configManager: ConfigManager, - k8: K8, platformInstaller: PlatformInstaller, keyManager: KeyManager, profileManager: ProfileManager, - chartManager: ChartManager, certificateManager: CertificateManager, parent: NodeCommand - } + logger: SoloLogger; accountManager: AccountManager; configManager: ConfigManager, + k8: K8, platformInstaller: PlatformInstaller, keyManager: KeyManager, profileManager: ProfileManager, + 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') @@ -965,7 +965,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/*`]) }) } @@ -1016,8 +1016,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 } @@ -1365,8 +1365,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) @@ -1386,13 +1386,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) @@ -1404,12 +1404,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..a8e977674 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,101 @@ 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, 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) { + 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 +489,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..8d1b516d9 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -15,31 +15,39 @@ * */ 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' 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' +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 @@ -55,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] } @@ -66,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 { @@ -118,29 +129,30 @@ export class LocalConfig implements LocalConfigData { this.logger.info(`Wrote local config to ${this.filePath}`) } - public promptLocalConfigTask (k8, argv): ListrTask[] { + 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) - 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 - 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 ListrTask[] + return self + }, self.skipPromptTask) as Task } } 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/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/helpers.ts b/src/core/helpers.ts index 935f02563..1b189b1a7 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 } /** @@ -506,7 +506,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) @@ -517,7 +517,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 c25486594..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 } 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 * @@ -466,7 +477,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}`) } @@ -679,7 +690,7 @@ export class K8 { self._deleteTempFile(tmpFile) return self.exitWithError(localContext, `${messagePrefix} Failure occurred`) } - self.logger.debug(`${messagePrefix} callback(status)=${status}`) + self.logger.debug(`${messagePrefix} callback(status)=${status}`) }) .then(conn => { @@ -896,24 +907,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 @@ -929,7 +940,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(',') @@ -1099,8 +1110,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 @@ -1115,17 +1126,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) { @@ -1138,7 +1149,7 @@ export class K8 { data: secretObject.data as Record } } - return null + return null } @@ -1206,7 +1217,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') @@ -1215,7 +1226,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') @@ -1226,7 +1237,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') @@ -1249,7 +1260,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..3b6333424 100644 --- a/src/core/task.ts +++ b/src/core/task.ts @@ -15,11 +15,5 @@ * */ 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) { } } 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/core/validator_decorators.ts b/src/core/validator_decorators.ts index ec8187e38..89cb13562 100644 --- a/src/core/validator_decorators.ts +++ b/src/core/validator_decorators.ts @@ -26,7 +26,6 @@ export const IsDeployments = (validationOptions?: ValidationOptions) => { propertyName: propertyName, constraints: [], options: { - message: 'Wrong deployments format', ...validationOptions, }, validator: { 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/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/test_util.ts b/test/test_util.ts index a1c57ab29..c183c7b20 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -328,8 +328,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) { @@ -349,9 +349,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) @@ -419,3 +419,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..adfbff478 --- /dev/null +++ b/test/unit/commands/context.test.ts @@ -0,0 +1,244 @@ +/** + * 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) + 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({ + 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 + }) + + 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', + [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-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 + }) + }) +}) diff --git a/test/unit/core/local_config.test.ts b/test/unit/core/local_config.test.ts index 27128a616..74316c862 100644 --- a/test/unit/core/local_config.test.ts +++ b/test/unit/core/local_config.test.ts @@ -19,33 +19,22 @@ 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' +import { ErrorMessages } from '../../../src/core/error_messages.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 = () => { + 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) } } @@ -87,10 +76,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 +136,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) @@ -188,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