UNPKG

@hashgraph/solo

Version:

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

396 lines 21.5 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import { Task } from '../../core/task.js'; import { Flags as flags } from '../flags.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 { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'; import { container } from 'tsyringe-neo'; import { InjectTokens } from '../../core/dependency_injection/inject_tokens.js'; export class ClusterCommandTasks { k8Factory; parent; clusterChecks = container.resolve(InjectTokens.ClusterChecks); constructor(parent, k8Factory) { this.k8Factory = k8Factory; this.parent = parent; } testConnectionToCluster(cluster, localConfig, parentTask) { const self = this; return { title: `Test connection to cluster: ${chalk.cyan(cluster)}`, task: async (_, subTask) => { 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, currentClusterName, localConfig, currentRemoteConfig) { const self = this; return { title: `Pull and validate remote configuration for cluster: ${chalk.cyan(cluster)}`, task: async (_, subTask) => { 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 = await this.parent.getRemoteConfigManager().get(); const subTasks = []; const remoteConfigClusters = Object.keys(currentRemoteConfig.clusters); const otherRemoteConfigClusters = 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() { return new Task('Update local configuration', async (ctx, task) => { 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 = []; 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(); }); }); } async getSelectedContext(task, selectedCluster, localConfig, isQuiet) { let selectedContext; if (isQuiet) { selectedContext = this.parent.getK8Factory().default().contexts().readCurrent(); } else { selectedContext = await this.promptForContext(task, selectedCluster); localConfig.clusterRefs[selectedCluster] = selectedContext; } return selectedContext; } async promptForContext(task, cluster) { const kubeContexts = this.parent.getK8Factory().default().contexts().list(); return flags.context.prompt(task, kubeContexts, cluster); } async selectContextForFirstCluster(task, clusters, localConfig, isQuiet) { 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 */ prepareValuesArg(chartDir = flags.chartDirectory.definition.defaultValue, prometheusStackEnabled = flags.deployPrometheusStack.definition.defaultValue, minioEnabled = flags.deployMinio.definition.defaultValue, certManagerEnabled = flags.deployCertManager.definition.defaultValue, certManagerCrdsEnabled = flags.deployCertManagerCrds.definition.defaultValue) { 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 */ async showInstalledChartList(clusterSetupNamespace) { this.parent.logger.showList('Installed Charts', await this.parent.getChartManager().getInstalledCharts(clusterSetupNamespace)); } selectContext() { 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(flags.quiet); const deploymentName = configManager.getFlag(flags.deployment); let clusters = splitFlagInput(configManager.getFlag(flags.clusterRef)); const contexts = splitFlagInput(configManager.getFlag(flags.context)); const namespace = configManager.getFlag(flags.namespace); const localConfig = this.parent.getLocalConfig(); let selectedContext; let selectedCluster; // TODO - BEGIN... added this because it was confusing why we have both clusterRef and deploymentClusters if (clusters?.length === 0) { clusters = splitFlagInput(configManager.getFlag(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, configInit) { const { requiredFlags, optionalFlags } = argv; argv.flags = [...requiredFlags, ...optionalFlags]; return new Task('Initialize', async (ctx, task) => { 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, task) => { this.parent.logger.showList('Clusters', this.parent.getK8Factory().default().clusters().list()); }); } getClusterInfo() { return new Task('Get cluster info', async (ctx, task) => { try { const clusterName = this.parent.getK8Factory().default().clusters().readCurrent(); this.parent.logger.showUser(`Cluster Name (${clusterName})`); this.parent.logger.showUser('\n'); } catch (e) { this.parent.logger.showUserError(e); } }); } prepareChartValues(argv) { const self = this; return new Task('Prepare chart values', async (ctx, task) => { 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, task) => { 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) { // 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, task) => { 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, task) => { 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); } } //# sourceMappingURL=tasks.js.map