UNPKG

@hashgraph/solo

Version:

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

920 lines 46.5 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'; import chalk from 'chalk'; import { Listr } from 'listr2'; import { IllegalArgumentError, MissingArgumentError, SoloError } from '../core/errors.js'; import { BaseCommand } from './base.js'; import { Flags as flags } from './flags.js'; import * as constants from '../core/constants.js'; import { Templates } from '../core/templates.js'; import * as helpers from '../core/helpers.js'; import { addDebugOptions, resolveValidJsonFilePath, validatePath } from '../core/helpers.js'; import { resolveNamespaceFromDeployment } from '../core/resolvers.js'; import path from 'path'; import fs from 'fs'; import { ListrLease } from '../core/lease/listr_lease.js'; import { ConsensusNodeComponent } from '../core/config/remote/components/consensus_node_component.js'; import { ConsensusNodeStates } from '../core/config/remote/enumerations.js'; import { EnvoyProxyComponent } from '../core/config/remote/components/envoy_proxy_component.js'; import { HaProxyComponent } from '../core/config/remote/components/ha_proxy_component.js'; import { v4 as uuidv4 } from 'uuid'; import { NamespaceName } from '../core/kube/resources/namespace/namespace_name.js'; import { PvcRef } from '../core/kube/resources/pvc/pvc_ref.js'; import { PvcName } from '../core/kube/resources/pvc/pvc_name.js'; import { Base64 } from 'js-base64'; import { SecretType } from '../core/kube/resources/secret/secret_type.js'; import { Duration } from '../core/time/duration.js'; export class NetworkCommand extends BaseCommand { keyManager; platformInstaller; profileManager; certificateManager; profileValuesFile; constructor(opts) { super(opts); if (!opts || !opts.k8Factory) throw new Error('An instance of core/K8Factory is required'); if (!opts || !opts.keyManager) throw new IllegalArgumentError('An instance of core/KeyManager is required', opts.keyManager); if (!opts || !opts.platformInstaller) throw new IllegalArgumentError('An instance of core/PlatformInstaller is required', opts.platformInstaller); if (!opts || !opts.profileManager) throw new MissingArgumentError('An instance of core/ProfileManager is required', opts.downloader); if (!opts || !opts.certificateManager) throw new MissingArgumentError('An instance of core/CertificateManager is required', opts.certificateManager); this.certificateManager = opts.certificateManager; this.keyManager = opts.keyManager; this.platformInstaller = opts.platformInstaller; this.profileManager = opts.profileManager; } static get DEPLOY_CONFIGS_NAME() { return 'deployConfigs'; } static get DEPLOY_FLAGS_LIST() { return [ flags.apiPermissionProperties, flags.app, flags.applicationEnv, flags.applicationProperties, flags.bootstrapProperties, flags.genesisThrottlesFile, flags.cacheDir, flags.chainId, flags.chartDirectory, flags.enablePrometheusSvcMonitor, flags.soloChartVersion, flags.debugNodeAlias, flags.loadBalancerEnabled, flags.log4j2Xml, flags.deployment, flags.nodeAliasesUnparsed, flags.persistentVolumeClaims, flags.profileFile, flags.profileName, flags.quiet, flags.releaseTag, flags.settingTxt, flags.networkDeploymentValuesFile, flags.grpcTlsCertificatePath, flags.grpcWebTlsCertificatePath, flags.grpcTlsKeyPath, flags.grpcWebTlsKeyPath, flags.haproxyIps, flags.envoyIps, flags.storageType, flags.gcsAccessKey, flags.gcsSecrets, flags.gcsEndpoint, flags.gcsBucket, flags.gcsBucketPrefix, flags.awsAccessKey, flags.awsSecrets, flags.awsEndpoint, flags.awsBucket, flags.awsBucketPrefix, flags.backupBucket, flags.googleCredential, ]; } async prepareMinioSecrets(config, minioAccessKey, minioSecretKey) { // Generating new minio credentials const minioData = {}; const namespace = config.namespace; const envString = `MINIO_ROOT_USER=${minioAccessKey}\nMINIO_ROOT_PASSWORD=${minioSecretKey}`; minioData['config.env'] = Base64.encode(envString); // create minio secret in each cluster for (const context of config.contexts) { this.logger.debug(`creating minio secret using context: ${context}`); const isMinioSecretCreated = await this.k8Factory .getK8(context) .secrets() .createOrReplace(namespace, constants.MINIO_SECRET_NAME, SecretType.OPAQUE, minioData, undefined); if (!isMinioSecretCreated) { throw new SoloError(`failed to create new minio secret using context: ${context}`); } this.logger.debug(`created minio secret using context: ${context}`); } } async prepareStreamUploaderSecrets(config) { const namespace = config.namespace; // Generating cloud storage secrets const { gcsAccessKey, gcsSecrets, gcsEndpoint, awsAccessKey, awsSecrets, awsEndpoint } = config; const cloudData = {}; if (config.storageType === constants.StorageType.AWS_ONLY || config.storageType === constants.StorageType.AWS_AND_GCS) { cloudData['S3_ACCESS_KEY'] = Base64.encode(awsAccessKey); cloudData['S3_SECRET_KEY'] = Base64.encode(awsSecrets); cloudData['S3_ENDPOINT'] = Base64.encode(awsEndpoint); } if (config.storageType === constants.StorageType.GCS_ONLY || config.storageType === constants.StorageType.AWS_AND_GCS) { cloudData['GCS_ACCESS_KEY'] = Base64.encode(gcsAccessKey); cloudData['GCS_SECRET_KEY'] = Base64.encode(gcsSecrets); cloudData['GCS_ENDPOINT'] = Base64.encode(gcsEndpoint); } // create secret in each cluster for (const context of config.contexts) { this.logger.debug(`creating secret for storage credential of type '${config.storageType}' using context: ${context}`); const isCloudSecretCreated = await this.k8Factory .getK8(context) .secrets() .createOrReplace(namespace, constants.UPLOADER_SECRET_NAME, SecretType.OPAQUE, cloudData, undefined); if (!isCloudSecretCreated) { throw new SoloError(`failed to create secret for storage credentials of type '${config.storageType}' using context: ${context}`); } this.logger.debug(`created secret for storage credential of type '${config.storageType}' using context: ${context}`); } } async prepareBackupUploaderSecrets(config) { if (config.googleCredential) { const backupData = {}; const namespace = config.namespace; const googleCredential = fs.readFileSync(config.googleCredential, 'utf8'); backupData['saJson'] = Base64.encode(googleCredential); // create secret in each cluster for (const context of config.contexts) { this.logger.debug(`creating secret for backup uploader using context: ${context}`); const k8client = this.k8Factory.getK8(context); const isBackupSecretCreated = await k8client .secrets() .createOrReplace(namespace, constants.BACKUP_SECRET_NAME, SecretType.OPAQUE, backupData, undefined); if (!isBackupSecretCreated) { throw new SoloError(`failed to create secret for backup uploader using context: ${context}`); } this.logger.debug(`created secret for backup uploader using context: ${context}`); } } } async prepareStorageSecrets(config) { try { if (config.storageType !== constants.StorageType.MINIO_ONLY) { const minioAccessKey = uuidv4(); const minioSecretKey = uuidv4(); await this.prepareMinioSecrets(config, minioAccessKey, minioSecretKey); await this.prepareStreamUploaderSecrets(config); } await this.prepareBackupUploaderSecrets(config); } catch (e) { const errorMessage = 'failed to create Kubernetes storage secret '; this.logger.error(errorMessage, e); throw new SoloError(errorMessage, e); } } /** * Prepare values args string for each cluster-ref * @param config */ async prepareValuesArgMap(config) { const valuesArgs = this.prepareValuesArg(config); // prepare values files for each cluster const valuesArgMap = {}; const profileName = this.configManager.getFlag(flags.profileName); this.profileValuesFile = await this.profileManager.prepareValuesForSoloChart(profileName, config.consensusNodes); const valuesFiles = BaseCommand.prepareValuesFilesMap(config.clusterRefs, config.chartDirectory, this.profileValuesFile, config.valuesFile); for (const clusterRef of Object.keys(valuesFiles)) { valuesArgMap[clusterRef] = valuesArgs[clusterRef] + valuesFiles[clusterRef]; this.logger.debug(`Prepared helm chart values for cluster-ref: ${clusterRef}`, { valuesArg: valuesArgMap }); } return valuesArgMap; } /** * Prepare the values argument for the helm chart for a given config * @param config */ prepareValuesArg(config) { const valuesArgs = {}; const clusterRefs = []; let extraEnvIndex = 0; // initialize the valueArgs for (const consensusNode of config.consensusNodes) { // add the cluster to the list of clusters if (!clusterRefs[consensusNode.cluster]) clusterRefs.push(consensusNode.cluster); // set the extraEnv settings on the nodes for running with a local build or tool if (config.app !== constants.HEDERA_APP_NAME) { extraEnvIndex = 1; // used to add the debug options when using a tool or local build of hedera let valuesArg = valuesArgs[consensusNode.cluster] ?? ''; valuesArg += ` --set "hedera.nodes[${consensusNode.nodeId}].root.extraEnv[0].name=JAVA_MAIN_CLASS"`; valuesArg += ` --set "hedera.nodes[${consensusNode.nodeId}].root.extraEnv[0].value=com.swirlds.platform.Browser"`; valuesArgs[consensusNode.cluster] = valuesArg; } else { // make sure each cluster has an empty string for the valuesArg valuesArgs[consensusNode.cluster] = ''; } } // add debug options to the debug node config.consensusNodes.filter(consensusNode => { if (consensusNode.name === config.debugNodeAlias) { valuesArgs[consensusNode.cluster] = addDebugOptions(valuesArgs[consensusNode.cluster], config.debugNodeAlias, extraEnvIndex); } }); if (config.storageType === constants.StorageType.AWS_AND_GCS || config.storageType === constants.StorageType.GCS_ONLY) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set cloud.gcs.enabled=true')); } if (config.storageType === constants.StorageType.AWS_AND_GCS || config.storageType === constants.StorageType.AWS_ONLY) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set cloud.s3.enabled=true')); } if (config.storageType === constants.StorageType.GCS_ONLY || config.storageType === constants.StorageType.AWS_ONLY || config.storageType === constants.StorageType.AWS_AND_GCS) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set cloud.minio.enabled=false')); } if (config.storageType !== constants.StorageType.MINIO_ONLY) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set cloud.generateNewSecrets=false')); } if (config.gcsBucket) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set cloud.buckets.streamBucket=${config.gcsBucket}` + ` --set minio-server.tenant.buckets[0].name=${config.gcsBucket}`)); } if (config.gcsBucketPrefix) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set cloud.buckets.streamBucketPrefix=${config.gcsBucketPrefix}`)); } if (config.awsBucket) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set cloud.buckets.streamBucket=${config.awsBucket}` + ` --set minio-server.tenant.buckets[0].name=${config.awsBucket}`)); } if (config.awsBucketPrefix) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set cloud.buckets.streamBucketPrefix=${config.awsBucketPrefix}`)); } if (config.backupBucket) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set defaults.sidecars.backupUploader.enabled=true' + ` --set defaults.sidecars.backupUploader.config.backupBucket=${config.backupBucket}`)); } clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set "telemetry.prometheus.svcMonitor.enabled=${config.enablePrometheusSvcMonitor}"` + ` --set "defaults.volumeClaims.enabled=${config.persistentVolumeClaims}"`)); // Iterate over each node and set static IPs for HAProxy this.addArgForEachRecord(config.haproxyIpsParsed, config.consensusNodes, valuesArgs, ' --set "hedera.nodes[${nodeId}].haproxyStaticIP=${recordValue}"'); // Iterate over each node and set static IPs for Envoy Proxy this.addArgForEachRecord(config.envoyIpsParsed, config.consensusNodes, valuesArgs, ' --set "hedera.nodes[${nodeId}].envoyProxyStaticIP=${recordValue}"'); if (config.resolvedThrottlesFile) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ` --set-file "hedera.configMaps.genesisThrottlesJson=${config.resolvedThrottlesFile}"`)); } if (config.loadBalancerEnabled) { clusterRefs.forEach(clusterRef => (valuesArgs[clusterRef] += ' --set "defaults.haproxy.service.type=LoadBalancer"' + ' --set "defaults.envoyProxy.service.type=LoadBalancer"' + ' --set "defaults.consensus.service.type=LoadBalancer"')); } return valuesArgs; } /** * Adds the template string to the argument for each record * @param records - the records to iterate over * @param consensusNodes - the consensus nodes to iterate over * @param valuesArgs - the values arguments to add to * @param templateString - the template string to add * @private */ addArgForEachRecord(records, consensusNodes, valuesArgs, templateString) { if (records) { consensusNodes.forEach(consensusNode => { if (records[consensusNode.name]) { const newTemplateString = templateString.replace('${nodeId}', consensusNode.nodeId.toString()); valuesArgs[consensusNode.cluster] += newTemplateString.replace('${recordValue}', records[consensusNode.name]); } }); } } async prepareNamespaces(config) { const namespace = config.namespace; // check and create namespace in each cluster for (const context of config.contexts) { const k8client = this.k8Factory.getK8(context); if (!(await k8client.namespaces().has(namespace))) { this.logger.debug(`creating namespace '${namespace}' using context: ${context}`); await k8client.namespaces().create(namespace); this.logger.debug(`created namespace '${namespace}' using context: ${context}`); } else { this.logger.debug(`namespace '${namespace}' found using context: ${context}`); } } } async prepareConfig(task, argv, promptForNodeAliases = false) { this.configManager.update(argv); this.logger.debug('Updated config with argv', { config: this.configManager.config }); const flagsWithDisabledPrompts = [ flags.apiPermissionProperties, flags.app, flags.applicationEnv, flags.applicationProperties, flags.bootstrapProperties, flags.genesisThrottlesFile, flags.cacheDir, flags.chainId, flags.chartDirectory, flags.debugNodeAlias, flags.loadBalancerEnabled, flags.log4j2Xml, flags.persistentVolumeClaims, flags.profileName, flags.profileFile, flags.settingTxt, flags.grpcTlsCertificatePath, flags.grpcWebTlsCertificatePath, flags.grpcTlsKeyPath, flags.grpcWebTlsKeyPath, flags.haproxyIps, flags.envoyIps, flags.storageType, flags.gcsAccessKey, flags.gcsSecrets, flags.gcsEndpoint, flags.gcsBucket, flags.gcsBucketPrefix, ]; if (promptForNodeAliases) flagsWithDisabledPrompts.push(flags.nodeAliasesUnparsed); // disable the prompts that we don't want to prompt the user for flags.disablePrompts(flagsWithDisabledPrompts); await this.configManager.executePrompt(task, NetworkCommand.DEPLOY_FLAGS_LIST); let namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); if (!namespace) { namespace = NamespaceName.of(this.configManager.getFlag(flags.deployment)); } this.configManager.setFlag(flags.namespace, namespace); // create a config object for subsequent steps const config = this.getConfig(NetworkCommand.DEPLOY_CONFIGS_NAME, NetworkCommand.DEPLOY_FLAGS_LIST, [ 'chartPath', 'keysDir', 'nodeAliases', 'stagingDir', 'stagingKeysDir', 'valuesArgMap', 'resolvedThrottlesFile', 'namespace', 'consensusNodes', 'contexts', 'clusterRefs', ]); config.nodeAliases = helpers.parseNodeAliases(config.nodeAliasesUnparsed); if (config.haproxyIps) { config.haproxyIpsParsed = Templates.parseNodeAliasToIpMapping(config.haproxyIps); } if (config.envoyIps) { config.envoyIpsParsed = Templates.parseNodeAliasToIpMapping(config.envoyIps); } // compute values config.chartPath = await this.prepareChartPath(config.chartDirectory, constants.SOLO_TESTING_CHART_URL, constants.SOLO_DEPLOYMENT_CHART); // compute other config parameters config.keysDir = path.join(validatePath(config.cacheDir), 'keys'); config.stagingDir = Templates.renderStagingDir(config.cacheDir, config.releaseTag); config.stagingKeysDir = path.join(validatePath(config.stagingDir), 'keys'); config.resolvedThrottlesFile = resolveValidJsonFilePath(config.genesisThrottlesFile, flags.genesisThrottlesFile.definition.defaultValue); config.consensusNodes = this.getConsensusNodes(); config.contexts = this.getContexts(); config.clusterRefs = this.getClusterRefs(); if (config.nodeAliases.length === 0) { config.nodeAliases = config.consensusNodes.map(node => node.name); if (config.nodeAliases.length === 0) { throw new SoloError('no node aliases provided via flags or RemoteConfig'); } this.configManager.setFlag(flags.nodeAliasesUnparsed, config.nodeAliases.join(',')); } config.valuesArgMap = await this.prepareValuesArgMap(config); // need to prepare the namespaces before we can proceed config.namespace = namespace; await this.prepareNamespaces(config); // prepare staging keys directory if (!fs.existsSync(config.stagingKeysDir)) { fs.mkdirSync(config.stagingKeysDir, { recursive: true }); } // create cached keys dir if it does not exist yet if (!fs.existsSync(config.keysDir)) { fs.mkdirSync(config.keysDir); } this.logger.debug('Preparing storage secrets'); await this.prepareStorageSecrets(config); this.logger.debug('Prepared config', { config, cachedConfig: this.configManager.config, }); return config; } async destroyTask(ctx, task) { const self = this; task.title = `Uninstalling chart ${constants.SOLO_DEPLOYMENT_CHART}`; await self.chartManager.uninstall(ctx.config.namespace, constants.SOLO_DEPLOYMENT_CHART, this.k8Factory.default().contexts().readCurrent()); if (ctx.config.deletePvcs) { const pvcs = await self.k8Factory.default().pvcs().list(ctx.config.namespace, []); task.title = `Deleting PVCs in namespace ${ctx.config.namespace}`; if (pvcs) { for (const pvc of pvcs) { await self.k8Factory .default() .pvcs() .delete(PvcRef.of(ctx.config.namespace, PvcName.of(pvc))); } } } if (ctx.config.deleteSecrets) { task.title = `Deleting secrets in namespace ${ctx.config.namespace}`; const secrets = await self.k8Factory.default().secrets().list(ctx.config.namespace); if (secrets) { for (const secret of secrets) { await self.k8Factory.default().secrets().delete(ctx.config.namespace, secret.name); } } } } /** Run helm install and deploy network components */ async deploy(argv) { const self = this; const lease = await self.leaseManager.create(); const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { ctx.config = await self.prepareConfig(task, argv, true); return ListrLease.newAcquireLeaseTask(lease, task); }, }, { title: 'Copy gRPC TLS Certificates', task: (ctx, parentTask) => self.certificateManager.buildCopyTlsCertificatesTasks(parentTask, ctx.config.grpcTlsCertificatePath, ctx.config.grpcWebTlsCertificatePath, ctx.config.grpcTlsKeyPath, ctx.config.grpcWebTlsKeyPath), skip: ctx => !ctx.config.grpcTlsCertificatePath && !ctx.config.grpcWebTlsCertificatePath, }, { title: 'Check if cluster setup chart is installed', task: async (ctx) => { for (const context of ctx.config.contexts) { const isChartInstalled = await this.chartManager.isChartInstalled(null, constants.SOLO_CLUSTER_SETUP_CHART, context); if (!isChartInstalled) { throw new SoloError(`Chart ${constants.SOLO_CLUSTER_SETUP_CHART} is not installed for cluster: ${context}. Run 'solo cluster setup'`); } } }, }, { title: 'Prepare staging directory', task: (_, parentTask) => { return parentTask.newListr([ { title: 'Copy Gossip keys to staging', task: ctx => { const config = ctx.config; this.keyManager.copyGossipKeysToStaging(config.keysDir, config.stagingKeysDir, config.nodeAliases); }, }, { title: 'Copy gRPC TLS keys to staging', task: ctx => { const config = ctx.config; for (const nodeAlias of config.nodeAliases) { const tlsKeyFiles = self.keyManager.prepareTLSKeyFilePaths(nodeAlias, config.keysDir); self.keyManager.copyNodeKeysToStaging(tlsKeyFiles, config.stagingKeysDir); } }, }, ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); }, }, { title: 'Copy node keys to secrets', task: (ctx, parentTask) => { const config = ctx.config; // set up the subtasks return parentTask.newListr(self.platformInstaller.copyNodeKeys(config.stagingDir, config.consensusNodes, config.contexts), { concurrent: true, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); }, }, { title: `Install chart '${constants.SOLO_DEPLOYMENT_CHART}'`, task: async (ctx) => { const config = ctx.config; for (const clusterRef of Object.keys(config.clusterRefs)) { if (await self.chartManager.isChartInstalled(config.namespace, constants.SOLO_DEPLOYMENT_CHART, config.clusterRefs[clusterRef])) { await self.chartManager.uninstall(config.namespace, constants.SOLO_DEPLOYMENT_CHART, this.k8Factory.getK8(config.clusterRefs[clusterRef]).contexts().readCurrent()); } await this.chartManager.install(config.namespace, constants.SOLO_DEPLOYMENT_CHART, ctx.config.chartPath, config.soloChartVersion, config.valuesArgMap[clusterRef], config.clusterRefs[clusterRef]); } }, }, { title: 'Check for load balancer', skip: ctx => ctx.config.loadBalancerEnabled === false, task: (ctx, task) => { const subTasks = []; const config = ctx.config; //Add check for network node service to be created and load balancer to be assigned (if load balancer is enabled) for (const consensusNode of config.consensusNodes) { subTasks.push({ title: `Load balancer is assigned for: ${chalk.yellow(consensusNode.name)}, cluster: ${chalk.yellow(consensusNode.cluster)}`, task: async () => { let attempts = 0; let svc = null; while (attempts < constants.LOAD_BALANCER_CHECK_MAX_ATTEMPTS) { svc = await self.k8Factory .getK8(consensusNode.context) .services() .list(config.namespace, [ `solo.hedera.com/node-id=${consensusNode.nodeId},solo.hedera.com/type=network-node-svc`, ]); if (svc && svc.length > 0 && svc[0].status?.loadBalancer?.ingress?.length > 0) { let shouldContinue = false; for (let i = 0; i < svc[0].status.loadBalancer.ingress.length; i++) { const ingress = svc[0].status.loadBalancer.ingress[i]; if (!ingress.hostname && !ingress.ip) { shouldContinue = true; // try again if there is neither a hostname nor an ip break; } } if (shouldContinue) { continue; } return; } attempts++; await helpers.sleep(Duration.ofSeconds(constants.LOAD_BALANCER_CHECK_DELAY_SECS)); } throw new SoloError('Load balancer not found'); }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, }, }); }, }, { title: 'Redeploy chart with external IP address config', skip: ctx => ctx.config.loadBalancerEnabled === false, task: async (ctx, task) => { // Update the valuesArgMap with the external IP addresses // This regenerates the config.txt and genesis-network.json files with the external IP addresses ctx.config.valuesArgMap = await this.prepareValuesArgMap(ctx.config); // Perform a helm upgrade for each cluster const subTasks = []; const config = ctx.config; for (const clusterRef of Object.keys(config.clusterRefs)) { subTasks.push({ title: `Upgrade chart for cluster: ${chalk.yellow(clusterRef)}`, task: async () => { await this.chartManager.upgrade(config.namespace, constants.SOLO_DEPLOYMENT_CHART, ctx.config.chartPath, config.soloChartVersion, config.valuesArgMap[clusterRef], config.clusterRefs[clusterRef]); }, }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, }, }); }, }, { title: 'Check node pods are running', task: (ctx, task) => { const subTasks = []; const config = ctx.config; // nodes for (const consensusNode of config.consensusNodes) { subTasks.push({ title: `Check Node: ${chalk.yellow(consensusNode.name)}, Cluster: ${chalk.yellow(consensusNode.cluster)}`, task: async () => await self.k8Factory .getK8(consensusNode.context) .pods() .waitForRunningPhase(config.namespace, [`solo.hedera.com/node-name=${consensusNode.name}`, 'solo.hedera.com/type=network-node'], constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY), }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: false, // no need to run concurrently since if one node is up, the rest should be up by then rendererOptions: { collapseSubtasks: false, }, }); }, }, { title: 'Check proxy pods are running', task: (ctx, task) => { const subTasks = []; const config = ctx.config; // HAProxy for (const consensusNode of config.consensusNodes) { subTasks.push({ title: `Check HAProxy for: ${chalk.yellow(consensusNode.name)}, cluster: ${chalk.yellow(consensusNode.cluster)}`, task: async () => await self.k8Factory .getK8(consensusNode.context) .pods() .waitForRunningPhase(config.namespace, ['solo.hedera.com/type=haproxy'], constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY), }); } // Envoy Proxy for (const consensusNode of config.consensusNodes) { subTasks.push({ title: `Check Envoy Proxy for: ${chalk.yellow(consensusNode.name)}, cluster: ${chalk.yellow(consensusNode.cluster)}`, task: async () => await self.k8Factory .getK8(consensusNode.context) .pods() .waitForRunningPhase(ctx.config.namespace, ['solo.hedera.com/type=envoy-proxy'], constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY), }); } // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, rendererOptions: { collapseSubtasks: false, }, }); }, }, { title: 'Check auxiliary pods are ready', task: (_, task) => { const subTasks = []; // minio subTasks.push({ title: 'Check MinIO', task: async (ctx) => { for (const context of ctx.config.contexts) { await self.k8Factory .getK8(context) .pods() .waitForReadyStatus(ctx.config.namespace, ['v1.min.io/tenant=minio'], constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY); } }, // skip if only cloud storage is/are used skip: ctx => ctx.config.storageType === constants.StorageType.GCS_ONLY || ctx.config.storageType === constants.StorageType.AWS_ONLY || ctx.config.storageType === constants.StorageType.AWS_AND_GCS, }); // set up the subtasks return task.newListr(subTasks, { concurrent: false, // no need to run concurrently since if one node is up, the rest should be up by then rendererOptions: { collapseSubtasks: false, }, }); }, }, this.addNodesAndProxies(), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); } catch (e) { throw new SoloError(`Error installing chart ${constants.SOLO_DEPLOYMENT_CHART}`, e); } finally { await lease.release(); } return true; } async destroy(argv) { const self = this; const lease = await self.leaseManager.create(); let networkDestroySuccess = true; const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { if (!argv.force) { const confirm = await task.prompt(ListrEnquirerPromptAdapter).run({ type: 'toggle', default: false, message: 'Are you sure you would like to destroy the network components?', }); if (!confirm) { process.exit(0); } } self.configManager.update(argv); await self.configManager.executePrompt(task, [flags.deletePvcs, flags.deleteSecrets]); const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); ctx.config = { deletePvcs: self.configManager.getFlag(flags.deletePvcs), deleteSecrets: self.configManager.getFlag(flags.deleteSecrets), namespace, enableTimeout: self.configManager.getFlag(flags.enableTimeout), force: self.configManager.getFlag(flags.force), }; return ListrLease.newAcquireLeaseTask(lease, task); }, }, { title: 'Running sub-tasks to destroy network', task: async (ctx, task) => { if (ctx.config.enableTimeout) { const timeoutId = setTimeout(() => { const message = `\n\nUnable to finish network destroy in ${constants.NETWORK_DESTROY_WAIT_TIMEOUT} seconds\n\n`; self.logger.error(message); self.logger.showUser(chalk.red(message)); networkDestroySuccess = false; if (ctx.config.deletePvcs && ctx.config.deleteSecrets && ctx.config.force) { self.k8Factory.default().namespaces().delete(ctx.config.namespace); } else { // If the namespace is not being deleted, // remove all components data from the remote configuration self.remoteConfigManager.deleteComponents(); } }, constants.NETWORK_DESTROY_WAIT_TIMEOUT * 1_000); await self.destroyTask(ctx, task); clearTimeout(timeoutId); } else { await self.destroyTask(ctx, task); } }, }, ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); } catch (e) { throw new SoloError('Error destroying network', e); } finally { await lease.release(); } return networkDestroySuccess; } /** Run helm upgrade to refresh network components with new settings */ async refresh(argv) { const self = this; const lease = await self.leaseManager.create(); const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { ctx.config = await self.prepareConfig(task, argv); return ListrLease.newAcquireLeaseTask(lease, task); }, }, { title: `Upgrade chart '${constants.SOLO_DEPLOYMENT_CHART}'`, task: async (ctx) => { const config = ctx.config; for (const clusterRef of Object.keys(config.valuesArgMap)) { await this.chartManager.upgrade(config.namespace, constants.SOLO_DEPLOYMENT_CHART, ctx.config.chartPath, config.soloChartVersion, config.valuesArgMap[clusterRef], this.k8Factory.default().contexts().readCurrent()); } }, }, { title: 'Waiting for network pods to be running', task: async (ctx) => { const config = ctx.config; await this.k8Factory .default() .pods() .waitForRunningPhase(config.namespace, ['solo.hedera.com/type=network-node', 'solo.hedera.com/type=network-node'], constants.PODS_RUNNING_MAX_ATTEMPTS, constants.PODS_RUNNING_DELAY); }, }, ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); } catch (e) { throw new SoloError(`Error upgrading chart ${constants.SOLO_DEPLOYMENT_CHART}`, e); } finally { await lease.release(); } return true; } getCommandDefinition() { const self = this; return { command: 'network', desc: 'Manage solo network deployment', builder: (yargs) => { return yargs .command({ command: 'deploy', desc: "Deploy solo network. Requires the chart `solo-cluster-setup` to have been installed in the cluster. If it hasn't the following command can be ran: `solo cluster setup`", builder: (y) => flags.setCommandFlags(y, ...NetworkCommand.DEPLOY_FLAGS_LIST), handler: (argv) => { self.logger.info("==== Running 'network deploy' ==="); self.logger.info(argv); self .deploy(argv) .then(r => { self.logger.info('==== Finished running `network deploy`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .command({ command: 'destroy', desc: 'Destroy solo network', builder: (y) => flags.setCommandFlags(y, flags.deletePvcs, flags.deleteSecrets, flags.enableTimeout, flags.force, flags.deployment, flags.quiet), handler: (argv) => { self.logger.info("==== Running 'network destroy' ==="); self.logger.info(argv); self .destroy(argv) .then(r => { self.logger.info('==== Finished running `network destroy`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .command({ command: 'refresh', desc: 'Refresh solo network deployment', builder: (y) => flags.setCommandFlags(y, ...NetworkCommand.DEPLOY_FLAGS_LIST), handler: (argv) => { self.logger.info("==== Running 'chart upgrade' ==="); self.logger.info(argv); self .refresh(argv) .then(r => { self.logger.info('==== Finished running `chart upgrade`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .demandCommand(1, 'Select a chart command'); }, }; } /** Adds the consensus node, envoy and haproxy components to remote config. */ addNodesAndProxies() { return { title: 'Add node and proxies to remote config', skip: () => !this.remoteConfigManager.isLoaded(), task: async (ctx) => { const { config: { namespace }, } = ctx; await this.remoteConfigManager.modify(async (remoteConfig) => { for (const consensusNode of ctx.config.consensusNodes) { remoteConfig.components.edit(consensusNode.name, new ConsensusNodeComponent(consensusNode.name, consensusNode.cluster, namespace.name, ConsensusNodeStates.REQUESTED, consensusNode.nodeId)); remoteConfig.components.add(`envoy-proxy-${consensusNode.name}`, new EnvoyProxyComponent(`envoy-proxy-${consensusNode.name}`, consensusNode.cluster, namespace.name)); remoteConfig.components.add(`haproxy-${consensusNode.name}`, new HaProxyComponent(`haproxy-${consensusNode.name}`, consensusNode.cluster, namespace.name)); } }); }, }; } close() { // no-op return Promise.resolve(); } } //# sourceMappingURL=network.js.map