@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
397 lines • 19.4 kB
JavaScript
/**
* 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