@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
303 lines (261 loc) • 10.4 kB
text/typescript
/**
* 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();
}
}