UNPKG

@hashgraph/solo

Version:

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

228 lines (194 loc) 9.47 kB
// SPDX-License-Identifier: Apache-2.0 // Define BiFunction type for TypeScript import {type UnInstallChartOptions} from '../model/install/un-install-chart-options.js'; import {type HelmClient} from '../helm-client.js'; import {type HelmExecution} from '../execution/helm-execution.js'; import {HelmExecutionBuilder} from '../execution/helm-execution-builder.js'; import {type Chart} from '../model/chart.js'; import {Repository} from '../model/repository.js'; import {Version} from '../model/version.js'; import {Release} from '../model/chart/release.js'; import {type InstallChartOptions} from '../model/install/install-chart-options.js'; import {type UpgradeChartOptions} from '../model/upgrade/upgrade-chart-options.js'; import {ReleaseItem} from '../model/release/release-item.js'; import {type TestChartOptions} from '../model/test/test-chart-options.js'; import {type HelmRequest} from '../request/helm-request.js'; import {ChartDependencyUpdateRequest} from '../request/chart/chart-dependency-update-request.js'; import {ChartInstallRequest} from '../request/chart/chart-install-request.js'; import {ChartTestRequest} from '../request/chart/chart-test-request.js'; import {ChartUninstallRequest} from '../request/chart/chart-uninstall-request.js'; import {ChartUpgradeRequest} from '../request/chart/chart-upgrade-request.js'; import {VersionRequest} from '../request/common/version-request.js'; import {ReleaseListRequest} from '../request/release/release-list-request.js'; import {RepositoryAddRequest} from '../request/repository/repository-add-request.js'; import {RepositoryListRequest} from '../request/repository/repository-list-request.js'; import {RepositoryRemoveRequest} from '../request/repository/repository-remove-request.js'; import {inject, injectable} from 'tsyringe-neo'; import {InjectTokens} from '../../../core/dependency-injection/inject-tokens.js'; import {patchInject} from '../../../core/dependency-injection/container-helper.js'; import {type SoloLogger} from '../../../core/logging/solo-logger.js'; import {AddRepoOptions} from '../model/add/add-repo-options.js'; import {SoloError} from '../../../core/errors/solo-error.js'; import {RepositoryUpdateRequest} from '../request/repository/repository-update-request.js'; import path from 'node:path'; import {SemanticVersion} from '../../../business/utils/semantic-version.js'; import {ChartPullRequest} from '../request/chart/chart-pull-request.js'; type BiFunction<T, U, R> = (t: T, u: U) => R; @injectable() /** * The default implementation of the HelmClient interface. */ export class DefaultHelmClient implements HelmClient { /** * The name of the namespace argument. */ private static readonly NAMESPACE_ARG_NAME: string = 'namespace'; public constructor( @inject(InjectTokens.SoloLogger) private readonly logger?: SoloLogger, @inject(InjectTokens.HelmInstallationDirectory) private readonly installationDirectory?: string, ) { this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name); this.installationDirectory = patchInject( installationDirectory, InjectTokens.HelmInstallationDirectory, this.constructor.name, ); } private readonly ERROR_401_REGEX: RegExp = /\b401\b.*\bunauthorized\b/i; private readonly ERROR_403_REGEX: RegExp = /\b401\b.*\bunauthorized\b/i; public async version(): Promise<SemanticVersion<string>> { const request: VersionRequest = new VersionRequest(); const builder: HelmExecutionBuilder = new HelmExecutionBuilder(); this.applyBuilderDefaults(builder); request.apply(builder); const execution: HelmExecution = builder.build(); if (execution instanceof Promise) { throw new TypeError('Unexpected async execution'); } const versionClass: typeof Version = Version; const result: Version = await execution.responseAs(versionClass); if (!(result instanceof Version)) { throw new TypeError('Unexpected response type'); } const semanticVersion: SemanticVersion<string> = result.asSemanticVersion(); this.logger.showUser(`helm version: ${semanticVersion.toString()}`); return semanticVersion; } public async listRepositories(): Promise<Repository[]> { return this.executeAsList(new RepositoryListRequest(), Repository); } public async addRepository(repository: Repository, options?: AddRepoOptions): Promise<void> { await this.executeAsync(new RepositoryAddRequest(repository, options)); } public async removeRepository(repository: Repository): Promise<void> { await this.executeAsync(new RepositoryRemoveRequest(repository)); } public async installChart(releaseName: string, chart: Chart, options: InstallChartOptions): Promise<Release> { const release: typeof Release = Release; const request: ChartInstallRequest = new ChartInstallRequest(releaseName, chart, options); return this.executeInternal(options.namespace, request, release, async (execution): Promise<Release> => { return await execution.responseAs(release); }); } public async uninstallChart(releaseName: string, options: UnInstallChartOptions): Promise<void> { await this.executeAsync(new ChartUninstallRequest(releaseName, options)); } public async testChart(releaseName: string, options: TestChartOptions): Promise<void> { await this.executeAsync(new ChartTestRequest(releaseName, options)); } public async listReleases(allNamespaces: boolean, namespace?: string, kubeContext?: string): Promise<ReleaseItem[]> { return this.executeAsList(new ReleaseListRequest(allNamespaces, namespace, kubeContext), ReleaseItem); } public async dependencyUpdate(chartName: string): Promise<void> { await this.executeAsync(new ChartDependencyUpdateRequest(chartName)); } public async upgradeChart(releaseName: string, chart: Chart, options: UpgradeChartOptions): Promise<Release> { const request: ChartUpgradeRequest = new ChartUpgradeRequest(releaseName, chart, options); return this.executeInternal( options.namespace, request, Release, async (execution: HelmExecution): Promise<Release> => execution.responseAs(Release), ); } /** * Applies the default namespace and authentication configuration to the given builder. * @param _builder - The builder to apply to which the defaults should be applied */ // eslint-disable-next-line @typescript-eslint/no-unused-vars private applyBuilderDefaults(_builder: HelmExecutionBuilder): void {} /** * Executes the given request and returns the response as the given class. * The request is executed using the default namespace. * * @param request - The request to execute * @param responseClass - The class of the response * @returns The response */ private async executeAsync<T extends HelmRequest, R>( request: T, responseClass?: new (...arguments_: any[]) => R, ): Promise<R> { return this.executeInternal(undefined, request, responseClass, async (b): Promise<R> => { return await b.responseAs(responseClass); }); } /** * Executes the given request and returns the response as a list of the given class. * The request is executed using the default namespace. * * @param request - The request to execute * @param responseClass - The class of the response * @returns A list of response objects */ private async executeAsList<T extends HelmRequest, R>( request: T, responseClass: new (...arguments_: any[]) => R, ): Promise<R[]> { return this.executeInternal(undefined, request, responseClass, async (b): Promise<R[]> => { return await b.responseAsList(responseClass); }); } private async executeInternal<T extends HelmRequest, R, V>( namespace: string | undefined, request: T, responseClass: new (...arguments_: any[]) => R, responseFunction: BiFunction<HelmExecution, typeof responseClass, Promise<V>>, ): Promise<V> { if (namespace && !namespace.trim()) { throw new Error('namespace must not be blank'); } const builder: HelmExecutionBuilder = new HelmExecutionBuilder(); this.applyBuilderDefaults(builder); request.apply(builder); if (namespace) { builder.argument(DefaultHelmClient.NAMESPACE_ARG_NAME, namespace); } builder.environmentVariable('PATH', `${this.installationDirectory}${path.delimiter}${process.env.PATH}`); const execution: HelmExecution = builder.build(); try { return await responseFunction(execution, responseClass); } catch (error) { const errorMessage: string = error?.message ?? ''; if (!this.ERROR_401_REGEX.test(errorMessage) && !this.ERROR_403_REGEX.test(errorMessage)) { // Throw original for all other cases throw error; } this.logger.showUser( [ 'Detected expired Docker authentication for GHCR (ghcr.io).', 'Fix: run one of the following and retry:', ' - docker logout ghcr.io', ' - docker logout http://ghcr.io/', ].join('\n'), ); throw new SoloError('GHCR stale Docker auth detected'); } } public async updateRepositories(): Promise<void> { await this.executeAsync(new RepositoryUpdateRequest()); } public async pullChartPackage(chart: Chart, version: string, destinationDirectory: string): Promise<void> { await this.executeAsync(new ChartPullRequest(chart, version, destinationDirectory)); } }