UNPKG

@hashgraph/solo

Version:

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

397 lines 19.4 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer'; import { Listr } from 'listr2'; import { SoloError, MissingArgumentError } from '../core/errors.js'; import * as constants from '../core/constants.js'; import { BaseCommand } from './base.js'; import { Flags as flags } from './flags.js'; import { ListrRemoteConfig } from '../core/config/remote/listr_config_tasks.js'; import { ListrLease } from '../core/lease/listr_lease.js'; import { ComponentType } from '../core/config/remote/enumerations.js'; import { MirrorNodeExplorerComponent } from '../core/config/remote/components/mirror_node_explorer_component.js'; import { resolveNamespaceFromDeployment } from '../core/resolvers.js'; import { container } from 'tsyringe-neo'; import { InjectTokens } from '../core/dependency_injection/inject_tokens.js'; export class ExplorerCommand extends BaseCommand { profileManager; constructor(opts) { super(opts); if (!opts || !opts.profileManager) throw new MissingArgumentError('An instance of core/ProfileManager is required', opts.downloader); this.profileManager = opts.profileManager; } static get DEPLOY_CONFIGS_NAME() { return 'deployConfigs'; } static get DEPLOY_FLAGS_LIST() { return [ flags.chartDirectory, flags.clusterRef, flags.enableIngress, flags.enableHederaExplorerTls, flags.hederaExplorerTlsHostName, flags.hederaExplorerStaticIp, flags.hederaExplorerVersion, flags.mirrorStaticIp, flags.namespace, flags.deployment, flags.profileFile, flags.profileName, flags.quiet, flags.clusterSetupNamespace, flags.soloChartVersion, flags.tlsClusterIssuerType, flags.valuesFile, ]; } /** * @param config - the configuration object */ async prepareHederaExplorerValuesArg(config) { let valuesArg = ''; const profileName = this.configManager.getFlag(flags.profileName); const profileValuesFile = await this.profileManager.prepareValuesHederaExplorerChart(profileName); if (profileValuesFile) { valuesArg += this.prepareValuesFiles(profileValuesFile); } if (config.valuesFile) { valuesArg += this.prepareValuesFiles(config.valuesFile); } if (config.enableIngress) { valuesArg += ' --set ingress.enabled=true'; valuesArg += ` --set ingressClassName=${config.namespace}-hedera-explorer-ingress-class`; } valuesArg += ` --set fullnameOverride=${constants.HEDERA_EXPLORER_RELEASE_NAME}`; valuesArg += ` --set proxyPass./api="http://${constants.MIRROR_NODE_RELEASE_NAME}-rest" `; return valuesArg; } /** * @param config - the configuration object */ async prepareSoloChartSetupValuesArg(config) { const { tlsClusterIssuerType, namespace, mirrorStaticIp, hederaExplorerStaticIp } = config; let valuesArg = ''; if (!['acme-staging', 'acme-prod', 'self-signed'].includes(tlsClusterIssuerType)) { throw new Error(`Invalid TLS cluster issuer type: ${tlsClusterIssuerType}, must be one of: "acme-staging", "acme-prod", or "self-signed"`); } const clusterChecks = container.resolve(InjectTokens.ClusterChecks); // Install ingress controller only if haproxy ingress not already present if (!(await clusterChecks.isIngressControllerInstalled()) && config.enableIngress) { valuesArg += ' --set ingress.enabled=true'; valuesArg += ' --set haproxyIngressController.enabled=true'; valuesArg += ` --set ingressClassName=${namespace}-hedera-explorer-ingress-class`; } if (!(await clusterChecks.isCertManagerInstalled())) { valuesArg += ' --set cloud.certManager.enabled=true'; valuesArg += ' --set cert-manager.installCRDs=true'; } if (hederaExplorerStaticIp !== '') { valuesArg += ` --set haproxy-ingress.controller.service.loadBalancerIP=${hederaExplorerStaticIp}`; } else if (mirrorStaticIp !== '') { valuesArg += ` --set haproxy-ingress.controller.service.loadBalancerIP=${mirrorStaticIp}`; } if (tlsClusterIssuerType === 'self-signed') { valuesArg += ' --set selfSignedClusterIssuer.enabled=true'; } else { valuesArg += ` --set global.explorerNamespace=${namespace}`; valuesArg += ' --set acmeClusterIssuer.enabled=true'; valuesArg += ` --set certClusterIssuerType=${tlsClusterIssuerType}`; } if (config.valuesFile) { valuesArg += this.prepareValuesFiles(config.valuesFile); } return valuesArg; } async prepareValuesArg(config) { let valuesArg = ''; if (config.valuesFile) { valuesArg += this.prepareValuesFiles(config.valuesFile); } return valuesArg; } async deploy(argv) { const self = this; const lease = await self.leaseManager.create(); const tasks = new Listr([ { title: 'Initialize', task: async (ctx, task) => { self.configManager.update(argv); // disable the prompts that we don't want to prompt the user for flags.disablePrompts([ flags.enableHederaExplorerTls, flags.hederaExplorerTlsHostName, flags.hederaExplorerStaticIp, flags.hederaExplorerVersion, flags.tlsClusterIssuerType, flags.valuesFile, ]); await self.configManager.executePrompt(task, ExplorerCommand.DEPLOY_FLAGS_LIST); ctx.config = this.getConfig(ExplorerCommand.DEPLOY_CONFIGS_NAME, ExplorerCommand.DEPLOY_FLAGS_LIST, [ 'valuesArg', ]); ctx.config.valuesArg += await self.prepareValuesArg(ctx.config); ctx.config.clusterContext = ctx.config.clusterRef ? this.getLocalConfig().clusterRefs[ctx.config.clusterRef] : this.k8Factory.default().contexts().readCurrent(); if (!(await self.k8Factory.getK8(ctx.config.clusterContext).namespaces().has(ctx.config.namespace))) { throw new SoloError(`namespace ${ctx.config.namespace} does not exist`); } return ListrLease.newAcquireLeaseTask(lease, task); }, }, ListrRemoteConfig.loadRemoteConfig(this, argv), { title: 'Upgrade solo-setup chart', task: async (ctx) => { const config = ctx.config; const { chartDirectory, clusterSetupNamespace, soloChartVersion } = config; const chartPath = await this.prepareChartPath(chartDirectory, constants.SOLO_TESTING_CHART_URL, constants.SOLO_CLUSTER_SETUP_CHART); const soloChartSetupValuesArg = await self.prepareSoloChartSetupValuesArg(config); // if cert-manager isn't already installed we want to install it separate from the certificate issuers // as they will fail to be created due to the order of the installation being dependent on the cert-manager // being installed first if (soloChartSetupValuesArg.includes('cloud.certManager.enabled=true')) { await self.chartManager.upgrade(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART, chartPath, soloChartVersion, ' --set cloud.certManager.enabled=true --set cert-manager.installCRDs=true', ctx.config.clusterContext); } // wait cert-manager to be ready to proceed, otherwise may get error of "failed calling webhook" await self.k8Factory .getK8(ctx.config.clusterContext) .pods() .waitForReadyStatus(constants.DEFAULT_CERT_MANAGER_NAMESPACE, [ 'app.kubernetes.io/component=webhook', `app.kubernetes.io/instance=${constants.SOLO_CLUSTER_SETUP_CHART}`, ], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY); // sleep for a few seconds to allow cert-manager to be ready await new Promise(resolve => setTimeout(resolve, 10000)); await self.chartManager.upgrade(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART, chartPath, soloChartVersion, soloChartSetupValuesArg, ctx.config.clusterContext); if (config.enableIngress) { // patch ingressClassName of mirror ingress so it can be recognized by haproxy ingress controller await this.k8Factory .getK8(ctx.config.clusterContext) .ingresses() .update(config.namespace, constants.MIRROR_NODE_RELEASE_NAME, { spec: { ingressClassName: `${config.namespace}-hedera-explorer-ingress-class`, }, }); // to support GRPC over HTTP/2 await this.k8Factory .getK8(ctx.config.clusterContext) .configMaps() .update(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART + '-haproxy-ingress', { 'backend-protocol': 'h2', }); } }, skip: ctx => !ctx.config.enableHederaExplorerTls && !ctx.config.enableIngress, }, { title: 'Install explorer', task: async (ctx) => { const config = ctx.config; let exploreValuesArg = self.prepareValuesFiles(constants.EXPLORER_VALUES_FILE); exploreValuesArg += await self.prepareHederaExplorerValuesArg(config); await self.chartManager.install(config.namespace, constants.HEDERA_EXPLORER_RELEASE_NAME, constants.HEDERA_EXPLORER_CHART_URL, config.hederaExplorerVersion, exploreValuesArg, ctx.config.clusterContext); // patch explorer ingress to use h1 protocol, haproxy ingress controller default backend protocol is h2 // to support grpc over http/2 await this.k8Factory .getK8(ctx.config.clusterContext) .ingresses() .update(config.namespace, constants.HEDERA_EXPLORER_RELEASE_NAME, { metadata: { annotations: { 'haproxy-ingress.github.io/backend-protocol': 'h1', }, }, }); }, }, { title: 'Check explorer pod is ready', task: async (ctx) => { await self.k8Factory .getK8(ctx.config.clusterContext) .pods() .waitForReadyStatus(ctx.config.namespace, [constants.SOLO_HEDERA_EXPLORER_LABEL], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY); }, }, { title: 'Check haproxy ingress controller pod is ready', task: async (ctx) => { await self.k8Factory .getK8(ctx.config.clusterContext) .pods() .waitForReadyStatus(constants.SOLO_SETUP_NAMESPACE, [ 'app.kubernetes.io/name=haproxy-ingress', `app.kubernetes.io/instance=${constants.SOLO_CLUSTER_SETUP_CHART}`, ], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY); }, skip: ctx => !ctx.config.enableIngress, }, this.addMirrorNodeExplorerComponents(), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); self.logger.debug('explorer deployment has completed'); } catch (e) { const message = `Error deploying explorer: ${e.message}`; self.logger.error(message, e); throw new SoloError(message, e); } finally { await lease.release(); } return true; } async destroy(argv) { const self = this; const lease = await self.leaseManager.create(); 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 explorer?', }); if (!confirm) { process.exit(0); } } self.configManager.update(argv); const namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task); const clusterRef = this.configManager.getFlag(flags.clusterRef); const clusterContext = clusterRef ? this.getLocalConfig().clusterRefs[clusterRef] : this.k8Factory.default().contexts().readCurrent(); ctx.config = { namespace, clusterContext, isChartInstalled: await this.chartManager.isChartInstalled(namespace, constants.HEDERA_EXPLORER_RELEASE_NAME, clusterContext), }; if (!(await self.k8Factory.getK8(ctx.config.clusterContext).namespaces().has(namespace))) { throw new SoloError(`namespace ${namespace.name} does not exist`); } return ListrLease.newAcquireLeaseTask(lease, task); }, }, ListrRemoteConfig.loadRemoteConfig(this, argv), { title: 'Destroy explorer', task: async (ctx) => { await this.chartManager.uninstall(ctx.config.namespace, constants.HEDERA_EXPLORER_RELEASE_NAME, ctx.config.clusterContext); }, skip: ctx => !ctx.config.isChartInstalled, }, this.removeMirrorNodeExplorerComponents(), ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, }); try { await tasks.run(); self.logger.debug('explorer destruction has completed'); } catch (e) { throw new SoloError(`Error destroy explorer: ${e.message}`, e); } finally { await lease.release(); } return true; } /** Return Yargs command definition for 'explorer' command */ getCommandDefinition() { const self = this; return { command: 'explorer', desc: 'Manage Explorer in solo network', builder: yargs => { return yargs .command({ command: 'deploy', desc: 'Deploy explorer', builder: y => flags.setCommandFlags(y, ...ExplorerCommand.DEPLOY_FLAGS_LIST), handler: argv => { self.logger.info("==== Running explorer deploy' ==="); self.logger.info(argv); self .deploy(argv) .then(r => { self.logger.info('==== Finished running explorer deploy`===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .command({ command: 'destroy', desc: 'Destroy explorer', builder: y => flags.setCommandFlags(y, flags.chartDirectory, flags.clusterRef, flags.force, flags.quiet, flags.deployment), handler: argv => { self.logger.info('==== Running explorer destroy ==='); self.logger.info(argv); self .destroy(argv) .then(r => { self.logger.info('==== Finished running explorer destroy ===='); if (!r) process.exit(1); }) .catch(err => { self.logger.showUserError(err); process.exit(1); }); }, }) .demandCommand(1, 'Select a explorer command'); }, }; } /** Removes the explorer components from remote config. */ removeMirrorNodeExplorerComponents() { return { title: 'Remove explorer from remote config', skip: () => !this.remoteConfigManager.isLoaded(), task: async () => { await this.remoteConfigManager.modify(async (remoteConfig) => { remoteConfig.components.remove('mirrorNodeExplorer', ComponentType.MirrorNodeExplorer); }); }, }; } /** Adds the explorer components to remote config. */ addMirrorNodeExplorerComponents() { return { title: 'Add explorer to remote config', skip: () => !this.remoteConfigManager.isLoaded(), task: async (ctx) => { await this.remoteConfigManager.modify(async (remoteConfig) => { const { config: { namespace }, } = ctx; const cluster = this.remoteConfigManager.currentCluster; remoteConfig.components.add('mirrorNodeExplorer', new MirrorNodeExplorerComponent('mirrorNodeExplorer', cluster, namespace.name)); }); }, }; } close() { // no-op return Promise.resolve(); } } //# sourceMappingURL=explorer.js.map