UNPKG

@hashgraph/solo

Version:

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

1,057 lines (919 loc) 38.1 kB
// SPDX-License-Identifier: Apache-2.0 import {Listr} from 'listr2'; import {ListrInquirerPromptAdapter} from '@listr2/prompt-adapter-inquirer'; import {confirm as confirmPrompt} from '@inquirer/prompts'; import {SoloError} from '../core/errors/solo-error.js'; import {UserBreak} from '../core/errors/user-break.js'; import * as constants from '../core/constants.js'; import {BaseCommand} from './base.js'; import {Flags as flags} from './flags.js'; import {type AnyListrContext, type ArgvStruct} from '../types/aliases.js'; import {ListrLock} from '../core/lock/listr-lock.js'; import * as helpers from '../core/helpers.js'; import {prepareValuesFiles, showVersionBanner, sleep} from '../core/helpers.js'; import { type ClusterReferenceName, type ComponentId, type Context, NamespaceNameAsString, type Optional, type SoloListr, type SoloListrTask, } from '../types/index.js'; import {NamespaceName} from '../types/namespace/namespace-name.js'; import {type ClusterChecks} from '../core/cluster-checks.js'; import {inject, injectable} from 'tsyringe-neo'; import {InjectTokens} from '../core/dependency-injection/inject-tokens.js'; import {KeyManager} from '../core/key-manager.js'; import {INGRESS_CONTROLLER_VERSION} from '../../version.js'; import {patchInject} from '../core/dependency-injection/container-helper.js'; import {ComponentTypes} from '../core/config/remote/enumerations/component-types.js'; import {Lock} from '../core/lock/lock.js'; import {IngressClass} from '../integration/kube/resources/ingress-class/ingress-class.js'; import {CommandFlag, CommandFlags} from '../types/flag-types.js'; import {Templates} from '../core/templates.js'; import {PodReference} from '../integration/kube/resources/pod/pod-reference.js'; import {Pod} from '../integration/kube/resources/pod/pod.js'; import {SemanticVersion} from '../business/utils/semantic-version.js'; import {assertUpgradeVersionNotOlder} from '../core/upgrade-version-guard.js'; import {Duration} from '../core/time/duration.js'; import {ExplorerStateSchema} from '../data/schema/model/remote/state/explorer-state-schema.js'; import {K8} from '../integration/kube/k8.js'; import {createHash} from 'node:crypto'; import {DeploymentPhase} from '../data/schema/model/remote/deployment-phase.js'; import {optionFromFlag} from './command-helpers.js'; interface ExplorerDeployConfigClass { cacheDir: string; chartDirectory: string; explorerChartDirectory: string; clusterRef: ClusterReferenceName; clusterContext: string; enableIngress: boolean; enableExplorerTls: boolean; ingressControllerValueFile: string; explorerTlsHostName: string; explorerStaticIp: string | ''; explorerVersion: string; namespace: NamespaceName; tlsClusterIssuerType: string; valuesFile: string; valuesArg: string; clusterSetupNamespace: NamespaceName; getUnusedConfigs: () => string[]; soloChartVersion: string; domainName: Optional<string>; releaseName: string; ingressReleaseName: string; newExplorerComponent: ExplorerStateSchema; id: ComponentId; forcePortForward: Optional<boolean>; isChartInstalled: boolean; isLegacyChartInstalled: false; // Mirror Node mirrorNodeId: ComponentId; mirrorNamespace: NamespaceNameAsString; mirrorNodeReleaseName: string; isMirrorNodeLegacyChartInstalled: boolean; } interface ExplorerDeployContext { config: ExplorerDeployConfigClass; addressBook: string; } interface ExplorerUpgradeConfigClass { cacheDir: string; chartDirectory: string; explorerChartDirectory: string; clusterRef: ClusterReferenceName; clusterContext: string; enableIngress: boolean; enableExplorerTls: boolean; ingressControllerValueFile: string; explorerTlsHostName: string; explorerStaticIp: string | ''; explorerVersion: string; namespace: NamespaceName; tlsClusterIssuerType: string; valuesFile: string; valuesArg: string; clusterSetupNamespace: NamespaceName; getUnusedConfigs: () => string[]; soloChartVersion: string; domainName: Optional<string>; releaseName: string; ingressReleaseName: string; forcePortForward: Optional<boolean>; id: ComponentId; isChartInstalled: boolean; isLegacyChartInstalled: boolean; // Mirror Node mirrorNodeId: ComponentId; mirrorNamespace: NamespaceNameAsString; mirrorNodeReleaseName: string; isMirrorNodeLegacyChartInstalled: boolean; } interface ExplorerUpgradeContext { config: ExplorerUpgradeConfigClass; addressBook: string; } interface ExplorerDestroyContext { config: { clusterContext: string; clusterReference: ClusterReferenceName; namespace: NamespaceName; isChartInstalled: boolean; id: ComponentId; releaseName: string; ingressReleaseName: string; isLegacyChartInstalled: boolean; }; } interface InferredData { id: ComponentId; releaseName: string; ingressReleaseName: string; isChartInstalled: boolean; isLegacyChartInstalled: boolean; } enum ExplorerCommandType { ADD = 'add', UPGRADE = 'upgrade', DESTROY = 'destroy', } @injectable() export class ExplorerCommand extends BaseCommand { public constructor(@inject(InjectTokens.ClusterChecks) private readonly clusterChecks: ClusterChecks) { super(); this.clusterChecks = patchInject(clusterChecks, InjectTokens.ClusterChecks, this.constructor.name); } private static readonly DEPLOY_CONFIGS_NAME: string = 'deployConfigs'; private static readonly UPGRADE_CONFIGS_NAME: string = 'upgradeConfigs'; public static readonly DEPLOY_FLAGS_LIST: CommandFlags = { required: [flags.deployment], optional: [ flags.cacheDir, flags.chartDirectory, flags.explorerChartDirectory, flags.clusterRef, flags.enableIngress, flags.ingressControllerValueFile, flags.enableExplorerTls, flags.explorerTlsHostName, flags.explorerStaticIp, flags.explorerVersion, flags.namespace, flags.quiet, flags.soloChartVersion, flags.tlsClusterIssuerType, flags.valuesFile, flags.clusterSetupNamespace, flags.domainName, flags.forcePortForward, flags.externalAddress, // Mirror Node flags.mirrorNodeId, flags.mirrorNamespace, ], }; public static readonly UPGRADE_FLAGS_LIST: CommandFlags = { required: [flags.deployment], optional: [ flags.clusterRef, flags.cacheDir, flags.chartDirectory, flags.explorerChartDirectory, flags.enableIngress, flags.ingressControllerValueFile, flags.enableExplorerTls, flags.explorerTlsHostName, flags.explorerStaticIp, flags.explorerVersion, flags.namespace, flags.quiet, flags.soloChartVersion, flags.tlsClusterIssuerType, flags.valuesFile, flags.clusterSetupNamespace, flags.domainName, flags.forcePortForward, flags.externalAddress, flags.id, // Mirror Node flags.mirrorNodeId, flags.mirrorNamespace, ], }; public static readonly DESTROY_FLAGS_LIST: CommandFlags = { required: [flags.deployment], optional: [flags.chartDirectory, flags.clusterRef, flags.force, flags.quiet, flags.devMode], }; private async prepareHederaExplorerValuesArg( config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass, ): Promise<string> { let valuesArgument: string = ''; if (config.valuesFile) { valuesArgument += prepareValuesFiles(config.valuesFile); } if (config.enableIngress) { valuesArgument += ' --set ingress.enabled=true'; valuesArgument += ` --set ingressClassName=${config.ingressReleaseName}`; } valuesArgument += ` --set fullnameOverride=${config.releaseName}-${config.namespace.name}`; valuesArgument += ` --set proxyPass./api="http://${config.mirrorNodeReleaseName}-rest.${config.mirrorNamespace}.svc.cluster.local" `; if (config.domainName) { valuesArgument += helpers.populateHelmArguments({ 'ingress.enabled': true, 'ingress.hosts[0].host': config.domainName, }); if (config.tlsClusterIssuerType === 'self-signed') { // Create TLS secret for Explorer await KeyManager.createTlsSecret( this.k8Factory, config.namespace, config.domainName, config.cacheDir, constants.EXPLORER_INGRESS_TLS_SECRET_NAME, ); if (config.enableIngress) { valuesArgument += ` --set ingress.tls[0].hosts[0]=${config.domainName}`; } } } return valuesArgument; } private async prepareCertManagerChartValuesArg( config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass, ): Promise<string> { const {tlsClusterIssuerType, namespace} = config; let valuesArgument: string = ' --install '; 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"`, ); } if (!(await this.clusterChecks.isCertManagerInstalled())) { valuesArgument += ' --set cert-manager.installCRDs=true'; } if (tlsClusterIssuerType === 'self-signed') { valuesArgument += ' --set selfSignedClusterIssuer.enabled=true'; } else { valuesArgument += ` --set global.explorerNamespace=${namespace}`; valuesArgument += ' --set acmeClusterIssuer.enabled=true'; valuesArgument += ` --set certClusterIssuerType=${tlsClusterIssuerType}`; } if (config.valuesFile) { valuesArgument += prepareValuesFiles(config.valuesFile); } return valuesArgument; } private async prepareValuesArg(config: ExplorerDeployConfigClass | ExplorerUpgradeConfigClass): Promise<string> { let valuesArgument: string = ''; if (config.valuesFile) { valuesArgument += prepareValuesFiles(config.valuesFile); } return valuesArgument; } private installCertManagerTask(commandType: ExplorerCommandType): SoloListrTask<AnyListrContext> { return { title: 'Install cert manager', skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableExplorerTls, task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { config.soloChartVersion = SemanticVersion.getValidSemanticVersion( config.soloChartVersion, false, 'Solo chart version', ); const {soloChartVersion} = config; const soloCertManagerValuesArgument: string = await this.prepareCertManagerChartValuesArg(config); // check if CRDs of cert-manager are already installed let needInstall: boolean = false; for (const crd of constants.CERT_MANAGER_CRDS) { const crdExists: boolean = await this.k8Factory.getK8(config.clusterContext).crds().ifExists(crd); if (!crdExists) { needInstall = true; break; } } if (needInstall) { // 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 await this.chartManager.upgrade( NamespaceName.of(constants.CERT_MANAGER_NAME_SPACE), constants.SOLO_CERT_MANAGER_CHART, constants.SOLO_CERT_MANAGER_CHART, config.chartDirectory || constants.SOLO_TESTING_CHART_URL, soloChartVersion, ' --install --create-namespace --set cert-manager.installCRDs=true', config.clusterContext, commandType !== ExplorerCommandType.ADD, ); showVersionBanner(this.logger, constants.SOLO_CERT_MANAGER_CHART, soloChartVersion); } // wait cert-manager to be ready to proceed, otherwise may get error of "failed calling webhook" await this.k8Factory .getK8(config.clusterContext) .pods() .waitForReadyStatus( constants.DEFAULT_CERT_MANAGER_NAMESPACE, ['app.kubernetes.io/component=webhook', `app.kubernetes.io/instance=${constants.SOLO_CERT_MANAGER_CHART}`], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY, ); // sleep for a few seconds to allow cert-manager to be ready if (commandType === ExplorerCommandType.UPGRADE) { await sleep(Duration.ofSeconds(10)); } await this.chartManager.upgrade( NamespaceName.of(constants.CERT_MANAGER_NAME_SPACE), constants.SOLO_CERT_MANAGER_CHART, constants.SOLO_CERT_MANAGER_CHART, config.chartDirectory || constants.SOLO_TESTING_CHART_URL, soloChartVersion, soloCertManagerValuesArgument, config.clusterContext, ); showVersionBanner(this.logger, constants.SOLO_CERT_MANAGER_CHART, soloChartVersion, 'Upgraded'); }, }; } private installExplorerTask(commandType: ExplorerCommandType): SoloListrTask<AnyListrContext> { return { title: 'Install explorer', task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { config.explorerVersion = SemanticVersion.getValidSemanticVersion( config.explorerVersion, false, 'Explorer version', ); let exploreValuesArgument: string = ' --install '; exploreValuesArgument += prepareValuesFiles(constants.EXPLORER_VALUES_FILE); exploreValuesArgument += await this.prepareHederaExplorerValuesArg(config); // Local chart checkouts can keep appVersion/tag at placeholder values (for example 0.0.1), // so pin the runtime image tag explicitly to the requested explorer version. if (config.explorerChartDirectory) { exploreValuesArgument += helpers.populateHelmArguments({'image.tag': config.explorerVersion}); } await this.chartManager.upgrade( config.namespace, config.releaseName, '', config.explorerChartDirectory || constants.EXPLORER_CHART_URL, config.explorerVersion, exploreValuesArgument, config.clusterContext, ); if (commandType === ExplorerCommandType.ADD) { this.remoteConfig.configuration.components.changeComponentPhase( (config as ExplorerDeployConfigClass).newExplorerComponent.metadata.id, ComponentTypes.Explorer, DeploymentPhase.DEPLOYED, ); await this.remoteConfig.persist(); } else if (commandType === ExplorerCommandType.UPGRADE) { // update explorer version in remote config after successful upgrade this.remoteConfig.updateComponentVersion( ComponentTypes.Explorer, new SemanticVersion<string>(config.explorerVersion), ); await this.remoteConfig.persist(); } showVersionBanner(this.logger, config.releaseName, config.explorerVersion); }, }; } private installExplorerIngressControllerTask(): SoloListrTask<AnyListrContext> { return { title: 'Install explorer ingress controller', skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableIngress, task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { let explorerIngressControllerValuesArgument: string = ' --install '; if (config.explorerStaticIp !== '') { explorerIngressControllerValuesArgument += ` --set controller.service.loadBalancerIP=${config.explorerStaticIp}`; } explorerIngressControllerValuesArgument += ` --set fullnameOverride=${config.ingressReleaseName}`; explorerIngressControllerValuesArgument += ` --set controller.ingressClass=${config.ingressReleaseName}`; explorerIngressControllerValuesArgument += ` --set controller.extraArgs.controller-class=${config.ingressReleaseName}`; if (config.tlsClusterIssuerType === 'self-signed') { explorerIngressControllerValuesArgument += prepareValuesFiles(config.ingressControllerValueFile); } await this.chartManager.upgrade( config.namespace, config.ingressReleaseName, constants.INGRESS_CONTROLLER_RELEASE_NAME, constants.INGRESS_CONTROLLER_RELEASE_NAME, INGRESS_CONTROLLER_VERSION, explorerIngressControllerValuesArgument, config.clusterContext, ); showVersionBanner(this.logger, config.ingressReleaseName, INGRESS_CONTROLLER_VERSION); const k8: K8 = this.k8Factory.getK8(config.clusterContext); // patch explorer ingress to use h1 protocol, haproxy ingress controller default backend protocol is h2 // to support grpc over http/2 await k8.ingresses().update(config.namespace, config.releaseName, { metadata: { annotations: { 'haproxy-ingress.github.io/backend-protocol': 'h1', }, }, }); const ingressClasses: IngressClass[] = await k8.ingressClasses().list(); if (ingressClasses.some((ingressClass): boolean => ingressClass.name === config.ingressReleaseName)) { return; } await k8 .ingressClasses() .create(config.ingressReleaseName, constants.INGRESS_CONTROLLER_PREFIX + config.ingressReleaseName); }, }; } private checkExplorerPodIsReadyTask(): SoloListrTask<AnyListrContext> { return { title: 'Check explorer pod is ready', task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { await this.k8Factory .getK8(config.clusterContext) .pods() .waitForReadyStatus( config.namespace, Templates.renderExplorerLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined), constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY, ); }, }; } private checkExplorerIngressControllerPodIsReadyTask(): SoloListrTask<AnyListrContext> { return { title: 'Check haproxy ingress controller pod is ready', skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.enableIngress, task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { await this.k8Factory .getK8(config.clusterContext) .pods() .waitForReadyStatus( config.namespace, [ `app.kubernetes.io/name=${constants.INGRESS_CONTROLLER_RELEASE_NAME}`, `app.kubernetes.io/instance=${config.ingressReleaseName}`, ], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY, ); }, }; } private enablePortForwardingTask(): SoloListrTask<AnyListrContext> { return { title: 'Enable port forwarding for explorer', skip: ({config}: ExplorerDeployContext | ExplorerUpgradeContext): boolean => !config.forcePortForward, task: async ({config}: ExplorerDeployContext | ExplorerUpgradeContext): Promise<void> => { const externalAddress: string = this.configManager.getFlag<string>(flags.externalAddress); const pods: Pod[] = await this.k8Factory .getK8(config.clusterContext) .pods() .list( config.namespace, Templates.renderExplorerLabels(config.id, config.isLegacyChartInstalled ? config.releaseName : undefined), ); if (pods.length === 0) { throw new SoloError('No Hiero Explorer pod found'); } const podReference: PodReference = pods[0].podReference; await this.remoteConfig.configuration.components.stopPortForwards( config.clusterRef, podReference, constants.EXPLORER_PORT, // Pod port constants.EXPLORER_LOCAL_PORT, // Local port this.k8Factory.getK8(config.clusterContext), this.logger, ComponentTypes.Explorer, 'Explorer', ); await this.remoteConfig.persist(); await this.remoteConfig.configuration.components.managePortForward( config.clusterRef, podReference, constants.EXPLORER_PORT, // Pod port constants.EXPLORER_LOCAL_PORT, // Local port this.k8Factory.getK8(config.clusterContext), this.logger, ComponentTypes.Explorer, 'Explorer', config.isChartInstalled, // Reuse existing port if chart is already installed undefined, true, // persist: auto-restart on failure using persist-port-forward.js externalAddress, ); await this.remoteConfig.persist(); }, }; } private getReleaseName(): string { return this.renderReleaseName( this.remoteConfig.configuration.components.getNewComponentId(ComponentTypes.Explorer), ); } private getIngressReleaseName(namespaceName: NamespaceName): string { return this.renderIngressReleaseName( this.remoteConfig.configuration.components.getNewComponentId(ComponentTypes.Explorer), namespaceName, ); } private renderReleaseName(id: ComponentId): string { if (typeof id !== 'number') { throw new SoloError(`Invalid component id: ${id}, type: ${typeof id}`); } return `${constants.EXPLORER_RELEASE_NAME}-${id}`; } private renderIngressReleaseName(id: ComponentId, namespaceName: NamespaceName): string { if (typeof id !== 'number') { throw new SoloError(`Invalid component id: ${id}, type: ${typeof id}`); } const maxHelmReleaseNameLength: number = 53; const baseReleaseName: string = `${constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME}-${id}-${namespaceName.name}`; if (baseReleaseName.length <= maxHelmReleaseNameLength) { return baseReleaseName; } // Keep names deterministic and short enough for Helm while preserving readability. const hashSuffixLength: number = 8; const namespaceHash: string = createHash('sha256') .update(namespaceName.name) .digest('hex') .slice(0, hashSuffixLength); const prefix: string = `${constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME}-${id}`; const availableNamespaceLength: number = maxHelmReleaseNameLength - prefix.length - 1 - hashSuffixLength - 1; /* - */ if (availableNamespaceLength <= 0) { return `${prefix}-${namespaceHash}`; } const shortenedNamespace: string = namespaceName.name.slice(0, availableNamespaceLength); return `${prefix}-${shortenedNamespace}-${namespaceHash}`; } public async add(argv: ArgvStruct): Promise<boolean> { let lease: Lock; const tasks: SoloListr<ExplorerDeployContext> = this.taskList.newTaskList<ExplorerDeployContext>( [ { title: 'Initialize', task: async (context_, task): Promise<Listr<AnyListrContext>> => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); if (!this.oneShotState.isActive()) { lease = await this.leaseManager.create(); } this.configManager.update(argv); flags.disablePrompts(ExplorerCommand.DEPLOY_FLAGS_LIST.optional); const allFlags: CommandFlag[] = [ ...ExplorerCommand.DEPLOY_FLAGS_LIST.optional, ...ExplorerCommand.DEPLOY_FLAGS_LIST.required, ]; await this.configManager.executePrompt(task, allFlags); const config: ExplorerDeployConfigClass = this.configManager.getConfig( ExplorerCommand.DEPLOY_CONFIGS_NAME, allFlags, [], ) as ExplorerDeployConfigClass; // In concurrent one-shot execution, configManager may have stale data due to // interleaved updates from other sub-commands. Override with argv values directly. if (this.oneShotState.isActive() && argv[flags.explorerVersion.name]) { config.explorerVersion = argv[flags.explorerVersion.name] as string; } config.isLegacyChartInstalled = false; context_.config = config; config.clusterRef = this.getClusterReference(); config.clusterContext = this.getClusterContext(config.clusterRef); config.releaseName = this.getReleaseName(); config.ingressReleaseName = this.getIngressReleaseName(config.namespace); const {mirrorNodeId, mirrorNamespace, mirrorNodeReleaseName} = await this.inferMirrorNodeData( config.namespace, config.clusterContext, ); config.mirrorNodeId = mirrorNodeId; config.mirrorNamespace = mirrorNamespace; config.mirrorNodeReleaseName = mirrorNodeReleaseName; config.newExplorerComponent = this.componentFactory.createNewExplorerComponent( config.clusterRef, config.namespace, ); config.newExplorerComponent.metadata.phase = DeploymentPhase.REQUESTED; config.id = config.newExplorerComponent.metadata.id; config.valuesArg = await this.prepareValuesArg(context_.config); await this.throwIfNamespaceIsMissing(config.clusterContext, config.namespace); if (!this.oneShotState.isActive()) { return ListrLock.newAcquireLockTask(lease, task); } return ListrLock.newSkippedLockTask(task); }, }, this.loadRemoteConfigTask(argv), this.addExplorerComponents(), this.installCertManagerTask(ExplorerCommandType.ADD), this.installExplorerTask(ExplorerCommandType.ADD), this.installExplorerIngressControllerTask(), this.checkExplorerPodIsReadyTask(), this.checkExplorerIngressControllerPodIsReadyTask(), this.enablePortForwardingTask(), { title: 'Show user messages', skip: (): boolean => !this.oneShotState.isActive(), task: (): void => { this.logger.showAllMessageGroups(); }, }, ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'explorer node add', ); if (tasks.isRoot()) { try { await tasks.run(); this.logger.debug('explorer deployment has completed'); } catch (error) { throw new SoloError(`Error deploying explorer: ${error.message}`, error); } finally { if (!this.oneShotState.isActive()) { await lease?.release(); } } } else { this.taskList.registerCloseFunction(async (): Promise<void> => { if (!this.oneShotState.isActive()) { await lease?.release(); } }); } return true; } public async upgrade(argv: ArgvStruct): Promise<boolean> { let lease: Lock; const tasks: SoloListr<ExplorerUpgradeContext> = this.taskList.newTaskList<ExplorerUpgradeContext>( [ { title: 'Initialize', task: async (context_, task): Promise<Listr<AnyListrContext>> => { await this.localConfig.load(); await this.remoteConfig.loadAndValidate(argv); if (!this.oneShotState.isActive()) { lease = await this.leaseManager.create(); } this.configManager.update(argv); flags.disablePrompts(ExplorerCommand.UPGRADE_FLAGS_LIST.optional); const allFlags: CommandFlag[] = [ ...ExplorerCommand.UPGRADE_FLAGS_LIST.optional, ...ExplorerCommand.UPGRADE_FLAGS_LIST.required, ]; await this.configManager.executePrompt(task, allFlags); const config: ExplorerUpgradeConfigClass = this.configManager.getConfig( ExplorerCommand.UPGRADE_CONFIGS_NAME, allFlags, [], ) as ExplorerUpgradeConfigClass; context_.config = config; config.clusterRef = this.getClusterReference(); config.clusterContext = this.getClusterContext(config.clusterRef); const {id, releaseName, ingressReleaseName, isChartInstalled, isLegacyChartInstalled} = await this.inferExplorerData(config.namespace, config.clusterContext); config.id = id; config.releaseName = releaseName; config.ingressReleaseName = ingressReleaseName; config.isChartInstalled = isChartInstalled; config.isLegacyChartInstalled = isLegacyChartInstalled; const {mirrorNodeId, mirrorNamespace, mirrorNodeReleaseName} = await this.inferMirrorNodeData( config.namespace, config.clusterContext, ); config.mirrorNodeId = mirrorNodeId; config.mirrorNamespace = mirrorNamespace; config.mirrorNodeReleaseName = mirrorNodeReleaseName; config.valuesArg = await this.prepareValuesArg(context_.config); assertUpgradeVersionNotOlder( 'Explorer', config.explorerVersion, this.remoteConfig.getComponentVersion(ComponentTypes.Explorer), optionFromFlag(flags.explorerVersion), ); await this.throwIfNamespaceIsMissing(config.clusterContext, config.namespace); if (!this.oneShotState.isActive()) { return ListrLock.newAcquireLockTask(lease, task); } return ListrLock.newSkippedLockTask(task); }, }, this.loadRemoteConfigTask(argv), this.installCertManagerTask(ExplorerCommandType.UPGRADE), this.installExplorerTask(ExplorerCommandType.UPGRADE), this.installExplorerIngressControllerTask(), this.checkExplorerPodIsReadyTask(), this.checkExplorerIngressControllerPodIsReadyTask(), this.enablePortForwardingTask(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'explorer node upgrade', ); if (tasks.isRoot()) { try { await tasks.run(); this.logger.debug('explorer upgrading has completed'); } catch (error) { throw new SoloError(`Error upgrading explorer: ${error.message}`, error); } finally { if (!this.oneShotState.isActive()) { await lease?.release(); } } } else { this.taskList.registerCloseFunction(async (): Promise<void> => { if (!this.oneShotState.isActive()) { await lease?.release(); } }); } return true; } public async destroy(argv: ArgvStruct): Promise<boolean> { let lease: Lock; const tasks: SoloListr<ExplorerDestroyContext> = this.taskList.newTaskList<ExplorerDestroyContext>( [ { title: 'Initialize', task: async (context_, task): Promise<Listr<AnyListrContext>> => { await this.localConfig.load(); await this.loadRemoteConfigOrWarn(argv); if (!this.oneShotState.isActive()) { lease = await this.leaseManager.create(); } if (!argv.force) { const confirmResult: boolean = await task.prompt(ListrInquirerPromptAdapter).run(confirmPrompt, { default: false, message: 'Are you sure you would like to destroy the explorer?', }); if (!confirmResult) { throw new UserBreak('Aborted application by user prompt'); } } this.configManager.update(argv); const namespace: NamespaceName = await this.getNamespace(task); const clusterReference: ClusterReferenceName = this.getClusterReference(); const clusterContext: Context = this.getClusterContext(clusterReference); const {id, releaseName, ingressReleaseName, isChartInstalled, isLegacyChartInstalled} = await this.inferExplorerData(namespace, clusterContext); context_.config = { namespace, clusterContext, clusterReference, id, releaseName, ingressReleaseName, isChartInstalled, isLegacyChartInstalled, }; await this.throwIfNamespaceIsMissing(clusterContext, namespace); if (!this.oneShotState.isActive()) { return ListrLock.newAcquireLockTask(lease, task); } return ListrLock.newSkippedLockTask(task); }, }, this.loadRemoteConfigTask(argv, true), this.loadRemoteConfigTask(argv), { title: 'Destroy explorer', task: async (context_): Promise<void> => { await this.chartManager.uninstall( context_.config.namespace, context_.config.releaseName, context_.config.clusterContext, ); }, skip: (context_): boolean => !context_.config.isChartInstalled, }, { title: 'Uninstall explorer ingress controller', task: async (context_): Promise<void> => { await this.chartManager.uninstall(context_.config.namespace, context_.config.ingressReleaseName); // destroy ingress class if found one const existingIngressClasses: IngressClass[] = await this.k8Factory .getK8(context_.config.clusterContext) .ingressClasses() .list(); existingIngressClasses.map((ingressClass: IngressClass): void => { if (ingressClass.name === context_.config.ingressReleaseName) { this.k8Factory .getK8(context_.config.clusterContext) .ingressClasses() .delete(context_.config.ingressReleaseName); } }); }, }, this.disableMirrorNodeExplorerComponents(), ], constants.LISTR_DEFAULT_OPTIONS.DEFAULT, undefined, 'explorer node destroy', ); if (tasks.isRoot()) { try { await tasks.run(); } catch (error) { throw new SoloError(`Error destroy explorer: ${error.message}`, error); } finally { if (!this.oneShotState.isActive()) { await lease?.release(); } } } else { this.taskList.registerCloseFunction(async (): Promise<void> => { if (!this.oneShotState.isActive()) { await lease?.release(); } }); } return true; } private loadRemoteConfigTask(argv: ArgvStruct, safe: boolean = false): SoloListrTask<AnyListrContext> { return { title: 'Load remote config', task: async (): Promise<void> => { if (safe) { await this.loadRemoteConfigOrWarn(argv); return; } await this.remoteConfig.loadAndValidate(argv); }, }; } /** Removes the explorer components from remote config. */ private disableMirrorNodeExplorerComponents(): SoloListrTask<ExplorerDestroyContext> { return { title: 'Remove explorer from remote config', skip: (): boolean => !this.remoteConfig.isLoaded(), task: async ({config}): Promise<void> => { this.remoteConfig.configuration.components.removeComponent(config.id, ComponentTypes.Explorer); await this.remoteConfig.persist(); }, }; } /** Adds the explorer components to remote config. */ private addExplorerComponents(): SoloListrTask<ExplorerDeployContext> { return { title: 'Add explorer to remote config', skip: (): boolean => !this.remoteConfig.isLoaded() || this.oneShotState.isActive(), task: async ({config}): Promise<void> => { this.remoteConfig.configuration.components.addNewComponent( config.newExplorerComponent, ComponentTypes.Explorer, ); // update explorer version in remote config this.remoteConfig.updateComponentVersion( ComponentTypes.Explorer, new SemanticVersion<string>(config.explorerVersion), ); await this.remoteConfig.persist(); }, }; } public async close(): Promise<void> {} // no-op private async checkIfLegacyChartIsInstalled( id: ComponentId, namespace: NamespaceName, context: Context, ): Promise<boolean> { return id <= 1 ? await this.chartManager.isChartInstalled(namespace, constants.EXPLORER_RELEASE_NAME, context) : false; } private inferExplorerId(): ComponentId { const id: ComponentId = this.configManager.getFlag(flags.id); if (typeof id === 'number') { return id; } if (!this.remoteConfig.configuration.components.state.explorers[0]) { throw new SoloError('No explorer component found in remote config'); } return this.remoteConfig.configuration.components.state.explorers[0].metadata.id; } private async inferExplorerData(namespace: NamespaceName, context: Context): Promise<InferredData> { const id: ComponentId = this.inferExplorerId(); const isLegacyChartInstalled: boolean = await this.checkIfLegacyChartIsInstalled(id, namespace, context); if (isLegacyChartInstalled) { return { id, releaseName: constants.EXPLORER_RELEASE_NAME, isChartInstalled: true, ingressReleaseName: constants.EXPLORER_INGRESS_CONTROLLER_RELEASE_NAME, isLegacyChartInstalled, }; } const releaseName: string = this.renderReleaseName(id); return { id, releaseName, ingressReleaseName: this.renderIngressReleaseName(id, namespace), isChartInstalled: await this.chartManager.isChartInstalled(namespace, releaseName, context), isLegacyChartInstalled, }; } }