@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
231 lines (214 loc) • 9.08 kB
text/typescript
// 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';
()
export class ChartManager {
public constructor(
(InjectTokens.Helm) private readonly helm?: HelmClient,
(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;
}
}