UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

303 lines (261 loc) 10.4 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import {Listr} from 'listr2'; import {SoloError} from '../core/errors.js'; import {BaseCommand, type Opts} from './base.js'; import {Flags as flags} from './flags.js'; import * as constants from '../core/constants.js'; import chalk from 'chalk'; import {ListrRemoteConfig} from '../core/config/remote/listr_config_tasks.js'; import {ClusterCommandTasks} from './cluster/tasks.js'; import {type ClusterRef, type DeploymentName, type NamespaceNameAsString} from '../core/config/remote/types.js'; import {type CommandFlag} from '../types/flag_types.js'; import {type CommandBuilder} from '../types/aliases.js'; import {type SoloListrTask} from '../types/index.js'; import {ErrorMessages} from '../core/error_messages.js'; import {splitFlagInput} from '../core/helpers.js'; import {type NamespaceName} from '../core/kube/resources/namespace/namespace_name.js'; import {type ClusterChecks} from '../core/cluster_checks.js'; import {container} from 'tsyringe-neo'; import {InjectTokens} from '../core/dependency_injection/inject_tokens.js'; export class DeploymentCommand extends BaseCommand { readonly tasks: ClusterCommandTasks; constructor(opts: Opts) { super(opts); this.tasks = new ClusterCommandTasks(this, this.k8Factory); } private static get DEPLOY_FLAGS_LIST(): CommandFlag[] { return [ flags.quiet, flags.context, flags.namespace, flags.clusterRef, flags.userEmailAddress, flags.deployment, flags.deploymentClusters, flags.nodeAliasesUnparsed, ]; } private static get LIST_DEPLOYMENTS_FLAGS_LIST(): CommandFlag[] { return [flags.quiet, flags.clusterRef]; } public async create(argv: any): Promise<boolean> { const self = this; interface Config { quiet: boolean; context: string; clusters: string[]; namespace: NamespaceName; deployment: DeploymentName; deploymentClusters: string[]; nodeAliases: string[]; clusterRef: ClusterRef; email: string; } interface Context { config: Config; } const tasks = new Listr<Context>( [ { title: 'Initialize', task: async (ctx, task) => { self.configManager.update(argv); self.logger.debug('Updated config with argv', {config: self.configManager.config}); await self.configManager.executePrompt(task, [ flags.namespace, flags.deployment, flags.deploymentClusters, flags.nodeAliasesUnparsed, ]); const deploymentName = self.configManager.getFlag<DeploymentName>(flags.deployment); if (self.localConfig.deployments && self.localConfig.deployments[deploymentName]) { throw new SoloError(ErrorMessages.DEPLOYMENT_NAME_ALREADY_EXISTS(deploymentName)); } ctx.config = { namespace: self.configManager.getFlag<NamespaceName>(flags.namespace), deployment: self.configManager.getFlag<DeploymentName>(flags.deployment), deploymentClusters: splitFlagInput(self.configManager.getFlag<string>(flags.deploymentClusters)), nodeAliases: splitFlagInput(self.configManager.getFlag<string>(flags.nodeAliasesUnparsed)), clusterRef: self.configManager.getFlag<ClusterRef>(flags.clusterRef), context: self.configManager.getFlag<string>(flags.context), email: self.configManager.getFlag<string>(flags.userEmailAddress), } as Config; self.logger.debug('Prepared config', {config: ctx.config, cachedConfig: self.configManager.config}); }, }, this.setupHomeDirectoryTask(), this.localConfig.promptLocalConfigTask(self.k8Factory), { title: 'Add new deployment to local config', task: async (ctx, task) => { const {deployments} = this.localConfig; const {deployment, namespace: configNamespace, deploymentClusters} = ctx.config; deployments[deployment] = { namespace: configNamespace.name, clusters: deploymentClusters, }; this.localConfig.setDeployments(deployments); await this.localConfig.write(); }, }, this.tasks.selectContext(), { title: 'Validate context', task: async (ctx, task) => { ctx.config.context = ctx.config.context ?? self.configManager.getFlag<string>(flags.context); const availableContexts = self.k8Factory.default().contexts().list(); if (availableContexts.includes(ctx.config.context)) { task.title += chalk.green(`- validated context ${ctx.config.context}`); return; } throw new SoloError( `Context with name ${ctx.config.context} not found, available contexts include ${availableContexts.join(', ')}`, ); }, }, this.tasks.updateLocalConfig(), { title: 'Validate cluster connections', task: async (ctx, task) => { const subTasks: SoloListrTask<Context>[] = []; for (const cluster of self.localConfig.deployments[ctx.config.deployment].clusters) { const context = self.localConfig.clusterRefs?.[cluster]; if (!context) continue; subTasks.push({ title: `Testing connection to cluster: ${chalk.cyan(cluster)}`, task: async (_, task) => { if (!(await self.k8Factory.default().contexts().testContextConnection(context))) { task.title = `${task.title} - ${chalk.red('Cluster connection failed')}`; throw new SoloError(`Cluster connection failed for: ${cluster}`); } }, }); } return task.newListr(subTasks, { concurrent: true, rendererOptions: {collapseSubtasks: false}, }); }, }, ListrRemoteConfig.createRemoteConfigInMultipleClusters(this, argv), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }, ); try { await tasks.run(); } catch (e: Error | unknown) { throw new SoloError('Error creating deployment', e); } return true; } private async list(argv: any): Promise<boolean> { const self = this; interface Config { clusterName: ClusterRef; } interface Context { config: Config; } const tasks = new Listr<Context>( [ { title: 'Initialize', task: async (ctx, task) => { self.configManager.update(argv); self.logger.debug('Updated config with argv', {config: self.configManager.config}); await self.configManager.executePrompt(task, [flags.clusterRef]); ctx.config = { clusterName: self.configManager.getFlag<ClusterRef>(flags.clusterRef), } as Config; self.logger.debug('Prepared config', {config: ctx.config, cachedConfig: self.configManager.config}); }, }, { title: 'Validate context', task: async ctx => { const clusterName = ctx.config.clusterName; const context = self.localConfig.clusterRefs[clusterName]; self.k8Factory.default().contexts().updateCurrent(context); const namespaces = await self.k8Factory.default().namespaces().list(); const namespacesWithRemoteConfigs: NamespaceNameAsString[] = []; for (const namespace of namespaces) { const isFound: boolean = await container .resolve<ClusterChecks>(InjectTokens.ClusterChecks) .isRemoteConfigPresentInNamespace(namespace); if (isFound) namespacesWithRemoteConfigs.push(namespace.name); } self.logger.showList(`Deployments inside cluster: ${chalk.cyan(clusterName)}`, namespacesWithRemoteConfigs); }, }, ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }, ); try { await tasks.run(); } catch (e: Error | unknown) { throw new SoloError(`Error installing chart ${constants.SOLO_DEPLOYMENT_CHART}`, e); } return true; } public getCommandDefinition(): {command: string; desc: string; builder: CommandBuilder} { const self = this; return { command: 'deployment', desc: 'Manage solo network deployment', builder: yargs => { return yargs .command({ command: 'create', desc: 'Creates solo deployment', builder: y => flags.setCommandFlags(y, ...DeploymentCommand.DEPLOY_FLAGS_LIST), handler: argv => { self.logger.info("==== Running 'deployment create' ==="); self.logger.info(argv); self .create(argv) .then(r => { self.logger.info('==== Finished running `deployment create`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .command({ command: 'list', desc: 'List solo deployments inside a cluster', builder: y => flags.setCommandFlags(y, ...DeploymentCommand.LIST_DEPLOYMENTS_FLAGS_LIST), handler: argv => { self.logger.info("==== Running 'deployment list' ==="); self.logger.info(argv); self .list(argv) .then(r => { self.logger.info('==== Finished running `deployment list`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .demandCommand(1, 'Select a chart command'); }, }; } close(): Promise<void> { // no-op return Promise.resolve(); } }