UNPKG

@hashgraph/solo

Version:

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

231 lines (214 loc) 9.08 kB
// SPDX-License-Identifier: Apache-2.0 import * as constants from './constants.js'; import chalk from 'chalk'; import {SoloError} from './errors/solo-error.js'; import {type SoloLogger} from './logging/solo-logger.js'; import {inject, injectable} from 'tsyringe-neo'; import {patchInject} from './dependency-injection/container-helper.js'; import {type NamespaceName} from '../types/namespace/namespace-name.js'; import {InjectTokens} from './dependency-injection/inject-tokens.js'; import {Repository} from '../integration/helm/model/repository.js'; import {type ReleaseItem} from '../integration/helm/model/release/release-item.js'; import {UpgradeChartOptions} from '../integration/helm/model/upgrade/upgrade-chart-options.js'; import {Chart} from '../integration/helm/model/chart.js'; import {type InstallChartOptions} from '../integration/helm/model/install/install-chart-options.js'; import {InstallChartOptionsBuilder} from '../integration/helm/model/install/install-chart-options-builder.js'; import {type HelmClient} from '../integration/helm/helm-client.js'; import {UnInstallChartOptionsBuilder} from '../integration/helm/model/install/un-install-chart-options-builder.js'; import {AddRepoOptionsBuilder} from '../integration/helm/model/add/add-repo-options-builder.js'; import {AddRepoOptions} from '../integration/helm/model/add/add-repo-options.js'; import {UnInstallChartOptions} from '../integration/helm/model/install/un-install-chart-options.js'; @injectable() export class ChartManager { public constructor( @inject(InjectTokens.Helm) private readonly helm?: HelmClient, @inject(InjectTokens.SoloLogger) private readonly logger?: SoloLogger, ) { this.helm = patchInject(helm, InjectTokens.Helm, this.constructor.name); this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name); } /** * Setup chart repositories * * This must be invoked before calling other methods * * @param repoURLs - a map of name and chart repository URLs * @param force - whether or not to update the repo * @returns the urls */ public async setup( repoURLs: Map<string, string> = constants.DEFAULT_CHART_REPO, force: boolean = true, ): Promise<string[]> { try { const promises: Promise<string>[] = []; for (const [name, url] of repoURLs.entries()) { this.logger.debug(`pushing promise for: add repo ${name} -> ${url}`); promises.push(this.addRepo(name, url, force)); } const urls: string[] = await Promise.all(promises); // urls await this.helm.updateRepositories(); return urls; } catch (error) { throw new SoloError(`failed to setup chart repositories: ${error.message}`, error); } } /** * Check if the required chart repositories are set up * * @param repoURLs - a map of name and chart repository URLs * @returns true if all repos are set up, false otherwise */ public async isSetup(repoURLs: Map<string, string> = constants.DEFAULT_CHART_REPO): Promise<boolean> { try { const existingRepos: Repository[] = await this.helm.listRepositories(); for (const [name, url] of repoURLs.entries()) { const found: Repository = existingRepos.find( (repo: Repository): boolean => repo.name === name && repo.url === url, ); if (!found) { this.logger.debug(`Repo not found: ${name} -> ${url}`); return false; } } return true; } catch (error) { throw new SoloError(`failed to check chart repositories: ${error.message}`, error); } } public async addRepo(name: string, url: string, force: boolean): Promise<string> { // detect if repo already exists for name provided and the url matches, if so, exit, otherwise force update const repositories: Repository[] = await this.helm.listRepositories(); const existingRepo: Repository | undefined = repositories.find((repo): boolean => repo.name === name); if (existingRepo) { if (existingRepo.url === url) { this.logger.debug(`Repo already exists: ${name} -> ${url}`); return url; } this.logger.debug(`Repo URL mismatch for ${name}: existing URL is ${existingRepo.url}, new URL is ${url}`); } this.logger.debug(`Adding repo ${name} -> ${url}`, {repoName: name, repoURL: url}); const options: AddRepoOptions = new AddRepoOptionsBuilder().forceUpdate(force).build(); await this.helm.addRepository(new Repository(name, url), options); return url; } /** List available clusters * * @param namespaceName - the namespace name * @param kubeContext - the kube context */ public async getInstalledCharts(namespaceName: NamespaceName, kubeContext?: string): Promise<string[]> { try { const result: ReleaseItem[] = await this.helm.listReleases(!namespaceName, namespaceName?.name, kubeContext); // convert to string[] return result.map((release): string => `${release.name} [${release.chart}]`); } catch (error) { this.logger.showUserError(error); throw new SoloError(`failed to list installed charts: ${error.message}`, error); } } public async install( namespaceName: NamespaceName, chartReleaseName: string, chartName: string, repoName: string, version: string, valuesArgument: string = '', kubeContext: string, atomic: boolean = false, waitFor: boolean = false, ): Promise<boolean> { try { const isInstalled: boolean = await this.isChartInstalled(namespaceName, chartReleaseName, kubeContext); if (isInstalled) { this.logger.debug(`OK: chart is already installed:${chartReleaseName} (${chartName}) (${repoName})`); } else { this.logger.debug(`> installing chart:${chartName}`); const builder: InstallChartOptionsBuilder = InstallChartOptionsBuilder.builder() .version(version) .kubeContext(kubeContext) .atomic(atomic) .waitFor(waitFor) .extraArgs(valuesArgument); if (namespaceName) { builder.createNamespace(true); builder.namespace(namespaceName.name); } const options: InstallChartOptions = builder.build(); await this.helm.installChart(chartReleaseName, new Chart(chartName, repoName), options); this.logger.debug(`OK: chart is installed: ${chartReleaseName} (${chartName}) (${repoName})`); } } catch (error) { throw new SoloError(`failed to install chart ${chartReleaseName}: ${error.message}`, error); } return true; } public async isChartInstalled( namespaceName: NamespaceName, chartReleaseName: string, kubeContext?: string, ): Promise<boolean> { this.logger.debug( `> checking if chart is installed [ chart: ${chartReleaseName}, namespace: ${namespaceName}, kubeContext: ${kubeContext} ]`, ); const charts: string[] = await this.getInstalledCharts(namespaceName, kubeContext); let match: boolean = false; for (const chart of charts) { if (chart.split(' ')[0] === chartReleaseName) { match = true; break; } } return match; } public async uninstall( namespaceName: NamespaceName, chartReleaseName: string, kubeContext?: string, ): Promise<boolean> { try { const isInstalled: boolean = await this.isChartInstalled(namespaceName, chartReleaseName, kubeContext); if (isInstalled) { this.logger.debug(`uninstalling chart release: ${chartReleaseName}`); const options: UnInstallChartOptions = UnInstallChartOptionsBuilder.builder() .namespace(namespaceName.name) .kubeContext(kubeContext) .build(); await this.helm.uninstallChart(chartReleaseName, options); this.logger.debug(`OK: chart release is uninstalled: ${chartReleaseName}`); } else { this.logger.debug(`OK: chart release is already uninstalled: ${chartReleaseName}`); } } catch (error) { throw new SoloError(`failed to uninstall chart ${chartReleaseName}: ${error.message}`, error); } return true; } public async upgrade( namespaceName: NamespaceName, chartReleaseName: string, chartName: string, repoName: string, version: string = '', valuesArgument: string = '', kubeContext?: string, reuseValues?: boolean, ): Promise<boolean> { try { this.logger.debug(chalk.cyan('> upgrading chart:'), chalk.yellow(`${chartReleaseName}`)); const options: UpgradeChartOptions = new UpgradeChartOptions( namespaceName?.name, kubeContext, reuseValues ?? true, valuesArgument, version, ); const chart: Chart = new Chart(chartName, repoName); await this.helm.upgradeChart(chartReleaseName, chart, options); this.logger.debug(chalk.green('OK'), `chart '${chartReleaseName}' is upgraded`); } catch (error) { throw new SoloError(`failed to upgrade chart ${chartReleaseName}: ${error.message}`, error); } return true; } }