@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
540 lines (477 loc) • 21.4 kB
text/typescript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {Task} from '../../core/task.js';
import {Flags as flags} from '../flags.js';
import {type ListrTaskWrapper} from 'listr2';
import {type ConfigBuilder} from '../../types/aliases.js';
import {type BaseCommand} from '../base.js';
import {splitFlagInput} from '../../core/helpers.js';
import * as constants from '../../core/constants.js';
import path from 'path';
import chalk from 'chalk';
import {ListrLease} from '../../core/lease/listr_lease.js';
import {ErrorMessages} from '../../core/error_messages.js';
import {SoloError} from '../../core/errors.js';
import {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js';
import {type RemoteConfigDataWrapper} from '../../core/config/remote/remote_config_data_wrapper.js';
import {type K8Factory} from '../../core/kube/k8_factory.js';
import {type SoloListrTask, type SoloListrTaskWrapper} from '../../types/index.js';
import {type SelectClusterContextContext} from './configs.js';
import {type DeploymentName} from '../../core/config/remote/types.js';
import {type LocalConfig} from '../../core/config/local_config.js';
import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer';
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 ClusterCommandTasks {
private readonly parent: BaseCommand;
private readonly clusterChecks: ClusterChecks = container.resolve(InjectTokens.ClusterChecks);
constructor(
parent,
private readonly k8Factory: K8Factory,
) {
this.parent = parent;
}
testConnectionToCluster(cluster: string, localConfig: LocalConfig, parentTask: ListrTaskWrapper<any, any, any>) {
const self = this;
return {
title: `Test connection to cluster: ${chalk.cyan(cluster)}`,
task: async (_, subTask: ListrTaskWrapper<any, any, any>) => {
let context = localConfig.clusterRefs[cluster];
if (!context) {
const isQuiet = self.parent.getConfigManager().getFlag(flags.quiet);
if (isQuiet) {
context = self.parent.getK8Factory().default().contexts().readCurrent();
} else {
context = await self.promptForContext(parentTask, cluster);
}
localConfig.clusterRefs[cluster] = context;
}
if (!(await self.parent.getK8Factory().default().contexts().testContextConnection(context))) {
subTask.title = `${subTask.title} - ${chalk.red('Cluster connection failed')}`;
throw new SoloError(`${ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER_DETAILED(context, cluster)}`);
}
},
};
}
validateRemoteConfigForCluster(
cluster: string,
currentClusterName: string,
localConfig: LocalConfig,
currentRemoteConfig: RemoteConfigDataWrapper,
) {
const self = this;
return {
title: `Pull and validate remote configuration for cluster: ${chalk.cyan(cluster)}`,
task: async (_, subTask: ListrTaskWrapper<any, any, any>) => {
const context = localConfig.clusterRefs[cluster];
self.parent.getK8Factory().default().contexts().updateCurrent(context);
const remoteConfigFromOtherCluster = await self.parent.getRemoteConfigManager().get();
if (!RemoteConfigManager.compare(currentRemoteConfig, remoteConfigFromOtherCluster)) {
throw new SoloError(ErrorMessages.REMOTE_CONFIGS_DO_NOT_MATCH(currentClusterName, cluster));
}
},
};
}
readClustersFromRemoteConfig(argv) {
const self = this;
return {
title: 'Read clusters from remote config',
task: async (ctx, task) => {
const localConfig = this.parent.getLocalConfig();
const currentClusterName = this.parent.getK8Factory().default().clusters().readCurrent();
const currentRemoteConfig: RemoteConfigDataWrapper = await this.parent.getRemoteConfigManager().get();
const subTasks = [];
const remoteConfigClusters = Object.keys(currentRemoteConfig.clusters);
const otherRemoteConfigClusters: string[] = remoteConfigClusters.filter(c => c !== currentClusterName);
// Validate connections for the other clusters
for (const cluster of otherRemoteConfigClusters) {
subTasks.push(self.testConnectionToCluster(cluster, localConfig, task));
}
// Pull and validate RemoteConfigs from the other clusters
for (const cluster of otherRemoteConfigClusters) {
subTasks.push(
self.validateRemoteConfigForCluster(cluster, currentClusterName, localConfig, currentRemoteConfig),
);
}
return task.newListr(subTasks, {
concurrent: false,
rendererOptions: {collapseSubtasks: false},
});
},
};
}
updateLocalConfig(): SoloListrTask<SelectClusterContextContext> {
return new Task('Update local configuration', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
this.parent.logger.info('Compare local and remote configuration...');
const configManager = this.parent.getConfigManager();
const isQuiet = configManager.getFlag(flags.quiet);
await this.parent.getRemoteConfigManager().modify(async remoteConfig => {
// Update current deployment with cluster list from remoteConfig
const localConfig = this.parent.getLocalConfig();
const localDeployments = localConfig.deployments;
const remoteClusterList: string[] = [];
let deploymentName;
const remoteNamespace = remoteConfig.metadata.namespace;
for (const deployment in localConfig.deployments) {
if (localConfig.deployments[deployment].namespace === remoteNamespace) {
deploymentName = deployment;
break;
}
}
if (localConfig.deployments[deploymentName]) {
for (const cluster of Object.keys(remoteConfig.clusters)) {
if (localConfig.deployments[deploymentName].namespace === remoteConfig.clusters[cluster].valueOf()) {
remoteClusterList.push(cluster);
}
}
ctx.config.clusters = remoteClusterList;
localDeployments[deploymentName].clusters = ctx.config.clusters;
} else {
const clusters = Object.keys(remoteConfig.clusters);
localDeployments[deploymentName] = {clusters, namespace: remoteNamespace};
ctx.config.clusters = clusters;
}
localConfig.setDeployments(localDeployments);
const contexts = splitFlagInput(configManager.getFlag(flags.context));
for (let i = 0; i < ctx.config.clusters.length; i++) {
const cluster = ctx.config.clusters[i];
const context = contexts[i];
// If a context is provided, use it to update the mapping
if (context) {
localConfig.clusterRefs[cluster] = context;
} else if (!localConfig.clusterRefs[cluster]) {
// In quiet mode, use the currently selected context to update the mapping
if (isQuiet) {
localConfig.clusterRefs[cluster] = this.parent.getK8Factory().default().contexts().readCurrent();
}
// Prompt the user to select a context if mapping value is missing
else {
localConfig.clusterRefs[cluster] = await this.promptForContext(task, cluster);
}
}
}
this.parent.logger.info('Update local configuration...');
await localConfig.write();
});
});
}
private async getSelectedContext(
task: SoloListrTaskWrapper<SelectClusterContextContext>,
selectedCluster: string,
localConfig: LocalConfig,
isQuiet: boolean,
) {
let selectedContext;
if (isQuiet) {
selectedContext = this.parent.getK8Factory().default().contexts().readCurrent();
} else {
selectedContext = await this.promptForContext(task, selectedCluster);
localConfig.clusterRefs[selectedCluster] = selectedContext;
}
return selectedContext;
}
private async promptForContext(task: SoloListrTaskWrapper<SelectClusterContextContext>, cluster: string) {
const kubeContexts = this.parent.getK8Factory().default().contexts().list();
return flags.context.prompt(task, kubeContexts, cluster);
}
private async selectContextForFirstCluster(
task: SoloListrTaskWrapper<SelectClusterContextContext>,
clusters: string[],
localConfig: LocalConfig,
isQuiet: boolean,
) {
const selectedCluster = clusters[0];
if (localConfig.clusterRefs[selectedCluster]) {
return localConfig.clusterRefs[selectedCluster];
}
// If a cluster does not exist in LocalConfig mapping prompt the user to select a context or use the current one
else {
return this.getSelectedContext(task, selectedCluster, localConfig, isQuiet);
}
}
/**
* Prepare values arg for cluster setup command
*
* @param [chartDir] - local charts directory (default is empty)
* @param [prometheusStackEnabled] - a bool to denote whether to install prometheus stack
* @param [minioEnabled] - a bool to denote whether to install minio
* @param [certManagerEnabled] - a bool to denote whether to install cert manager
* @param [certManagerCrdsEnabled] - a bool to denote whether to install cert manager CRDs
*/
private prepareValuesArg(
chartDir = flags.chartDirectory.definition.defaultValue as string,
prometheusStackEnabled = flags.deployPrometheusStack.definition.defaultValue as boolean,
minioEnabled = flags.deployMinio.definition.defaultValue as boolean,
certManagerEnabled = flags.deployCertManager.definition.defaultValue as boolean,
certManagerCrdsEnabled = flags.deployCertManagerCrds.definition.defaultValue as boolean,
) {
let valuesArg = chartDir ? `-f ${path.join(chartDir, 'solo-cluster-setup', 'values.yaml')}` : '';
valuesArg += ` --set cloud.prometheusStack.enabled=${prometheusStackEnabled}`;
valuesArg += ` --set cloud.certManager.enabled=${certManagerEnabled}`;
valuesArg += ` --set cert-manager.installCRDs=${certManagerCrdsEnabled}`;
valuesArg += ` --set cloud.minio.enabled=${minioEnabled}`;
if (certManagerEnabled && !certManagerCrdsEnabled) {
this.parent.logger.showUser(
chalk.yellowBright('> WARNING:'),
chalk.yellow(
'cert-manager CRDs are required for cert-manager, please enable it if you have not installed it independently.',
),
);
}
return valuesArg;
}
/** Show list of installed chart */
private async showInstalledChartList(clusterSetupNamespace: NamespaceName) {
this.parent.logger.showList(
'Installed Charts',
await this.parent.getChartManager().getInstalledCharts(clusterSetupNamespace),
);
}
selectContext(): SoloListrTask<SelectClusterContextContext> {
return {
title: 'Resolve context for remote cluster',
task: async (_, task) => {
this.parent.logger.info('Resolve context for remote cluster...');
const configManager = this.parent.getConfigManager();
const isQuiet = configManager.getFlag<boolean>(flags.quiet);
const deploymentName: string = configManager.getFlag<DeploymentName>(flags.deployment);
let clusters = splitFlagInput(configManager.getFlag<string>(flags.clusterRef));
const contexts = splitFlagInput(configManager.getFlag<string>(flags.context));
const namespace = configManager.getFlag<NamespaceName>(flags.namespace);
const localConfig = this.parent.getLocalConfig();
let selectedContext: string;
let selectedCluster: string;
// TODO - BEGIN... added this because it was confusing why we have both clusterRef and deploymentClusters
if (clusters?.length === 0) {
clusters = splitFlagInput(configManager.getFlag<string>(flags.deploymentClusters));
}
// If one or more contexts are provided, use the first one
if (contexts.length) {
selectedContext = contexts[0];
if (clusters.length) {
selectedCluster = clusters[0];
} else if (localConfig.deployments[deploymentName]) {
selectedCluster = localConfig.deployments[deploymentName].clusters[0];
}
}
// If one or more clusters are provided, use the first one to determine the context
// from the mapping in the LocalConfig
else if (clusters.length) {
selectedCluster = clusters[0];
selectedContext = await this.selectContextForFirstCluster(task, clusters, localConfig, isQuiet);
}
// If a deployment name is provided, get the clusters associated with the deployment from the LocalConfig
// and select the context from the mapping, corresponding to the first deployment cluster
else if (deploymentName) {
const deployment = localConfig.deployments[deploymentName];
if (deployment && deployment.clusters.length) {
selectedCluster = deployment.clusters[0];
selectedContext = await this.selectContextForFirstCluster(task, deployment.clusters, localConfig, isQuiet);
}
// The provided deployment does not exist in the LocalConfig
else {
// Add the deployment to the LocalConfig with the currently selected cluster and context in KubeConfig
if (isQuiet) {
selectedContext = this.parent.getK8Factory().default().contexts().readCurrent();
selectedCluster = this.parent.getK8Factory().default().clusters().readCurrent();
localConfig.deployments[deploymentName] = {
clusters: [selectedCluster],
namespace: namespace ? namespace.name : '',
};
if (!localConfig.clusterRefs[selectedCluster]) {
localConfig.clusterRefs[selectedCluster] = selectedContext;
}
}
// Prompt user for clusters and contexts
else {
const promptedClusters = await flags.clusterRef.prompt(task, '');
clusters = splitFlagInput(promptedClusters);
for (const cluster of clusters) {
if (!localConfig.clusterRefs[cluster]) {
localConfig.clusterRefs[cluster] = await this.promptForContext(task, cluster);
}
}
selectedCluster = clusters[0];
selectedContext = localConfig.clusterRefs[clusters[0]];
}
}
}
const connectionValid = await this.parent
.getK8Factory()
.default()
.contexts()
.testContextConnection(selectedContext);
if (!connectionValid) {
throw new SoloError(ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER(selectedContext, selectedCluster));
}
this.parent.getK8Factory().default().contexts().updateCurrent(selectedContext);
this.parent.getConfigManager().setFlag(flags.context, selectedContext);
},
};
}
initialize(argv: any, configInit: ConfigBuilder) {
const {requiredFlags, optionalFlags} = argv;
argv.flags = [...requiredFlags, ...optionalFlags];
return new Task('Initialize', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
if (argv[flags.devMode.name]) {
this.parent.logger.setDevMode(true);
}
ctx.config = await configInit(argv, ctx, task);
});
}
showClusterList() {
return new Task('List all available clusters', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
this.parent.logger.showList('Clusters', this.parent.getK8Factory().default().clusters().list());
});
}
getClusterInfo() {
return new Task('Get cluster info', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
try {
const clusterName = this.parent.getK8Factory().default().clusters().readCurrent();
this.parent.logger.showUser(`Cluster Name (${clusterName})`);
this.parent.logger.showUser('\n');
} catch (e: Error | unknown) {
this.parent.logger.showUserError(e);
}
});
}
prepareChartValues(argv) {
const self = this;
return new Task(
'Prepare chart values',
async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
ctx.chartPath = await this.parent.prepareChartPath(
ctx.config.chartDir,
constants.SOLO_TESTING_CHART_URL,
constants.SOLO_CLUSTER_SETUP_CHART,
);
// if minio is already present, don't deploy it
if (ctx.config.deployMinio && (await self.clusterChecks.isMinioInstalled(ctx.config.clusterSetupNamespace))) {
ctx.config.deployMinio = false;
}
// if prometheus is found, don't deploy it
if (
ctx.config.deployPrometheusStack &&
!(await self.clusterChecks.isPrometheusInstalled(ctx.config.clusterSetupNamespace))
) {
ctx.config.deployPrometheusStack = false;
}
// if cert manager is installed, don't deploy it
if (
(ctx.config.deployCertManager || ctx.config.deployCertManagerCrds) &&
(await self.clusterChecks.isCertManagerInstalled())
) {
ctx.config.deployCertManager = false;
ctx.config.deployCertManagerCrds = false;
}
// If all are already present or not wanted, skip installation
if (
!ctx.config.deployPrometheusStack &&
!ctx.config.deployMinio &&
!ctx.config.deployCertManager &&
!ctx.config.deployCertManagerCrds
) {
ctx.isChartInstalled = true;
return;
}
ctx.valuesArg = this.prepareValuesArg(
ctx.config.chartDir,
ctx.config.deployPrometheusStack,
ctx.config.deployMinio,
ctx.config.deployCertManager,
ctx.config.deployCertManagerCrds,
);
},
ctx => ctx.isChartInstalled,
);
}
installClusterChart(argv) {
const parent = this.parent;
return new Task(
`Install '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`,
async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
const clusterSetupNamespace = ctx.config.clusterSetupNamespace;
const version = ctx.config.soloChartVersion;
const valuesArg = ctx.valuesArg;
try {
parent.logger.debug(`Installing chart chartPath = ${ctx.chartPath}, version = ${version}`);
await parent
.getChartManager()
.install(
clusterSetupNamespace,
constants.SOLO_CLUSTER_SETUP_CHART,
ctx.chartPath,
version,
valuesArg,
this.k8Factory.default().contexts().readCurrent(),
);
} catch (e: Error | unknown) {
// if error, uninstall the chart and rethrow the error
parent.logger.debug(
`Error on installing ${constants.SOLO_CLUSTER_SETUP_CHART}. attempting to rollback by uninstalling the chart`,
e,
);
try {
await parent
.getChartManager()
.uninstall(
clusterSetupNamespace,
constants.SOLO_CLUSTER_SETUP_CHART,
this.k8Factory.default().contexts().readCurrent(),
);
} catch {
// ignore error during uninstall since we are doing the best-effort uninstall here
}
throw e;
}
if (argv.dev) {
await this.showInstalledChartList(clusterSetupNamespace);
}
},
ctx => ctx.isChartInstalled,
);
}
acquireNewLease(argv) {
return new Task('Acquire new lease', async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
const lease = await this.parent.getLeaseManager().create();
return ListrLease.newAcquireLeaseTask(lease, task);
});
}
uninstallClusterChart(argv) {
const parent = this.parent;
const self = this;
return new Task(
`Uninstall '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`,
async (ctx: any, task: ListrTaskWrapper<any, any, any>) => {
const clusterSetupNamespace = ctx.config.clusterSetupNamespace;
if (!argv.force && (await self.clusterChecks.isRemoteConfigPresentInAnyNamespace())) {
const confirm = await task.prompt(ListrEnquirerPromptAdapter).run({
type: 'toggle',
default: false,
message:
'There is remote config for one of the deployments' +
'Are you sure you would like to uninstall the cluster?',
});
if (!confirm) {
// eslint-disable-next-line n/no-process-exit
process.exit(0);
}
}
await parent
.getChartManager()
.uninstall(
clusterSetupNamespace,
constants.SOLO_CLUSTER_SETUP_CHART,
this.k8Factory.default().contexts().readCurrent(),
);
if (argv.dev) {
await this.showInstalledChartList(clusterSetupNamespace);
}
},
ctx => !ctx.isChartInstalled,
);
}
}