@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
222 lines (188 loc) • 8.09 kB
text/typescript
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {IsEmail, IsNotEmpty, IsObject, IsString, validateSync} from 'class-validator';
import fs from 'fs';
import * as yaml from 'yaml';
import {Flags as flags} from '../../commands/flags.js';
import {type Deployments, type LocalConfigData} from './local_config_data.js';
import {MissingArgumentError, SoloError} from '../errors.js';
import {type SoloLogger} from '../logging.js';
import {IsClusterRefs, IsDeployments} from '../validator_decorators.js';
import {type ConfigManager} from '../config_manager.js';
import {type DeploymentName, type EmailAddress, type Version, type ClusterRefs} from './remote/types.js';
import {ErrorMessages} from '../error_messages.js';
import {type K8Factory} from '../kube/k8_factory.js';
import * as helpers from '../helpers.js';
import {inject, injectable} from 'tsyringe-neo';
import {patchInject} from '../dependency_injection/container_helper.js';
import {type SoloListrTask} from '../../types/index.js';
import {type NamespaceName} from '../kube/resources/namespace/namespace_name.js';
import {InjectTokens} from '../dependency_injection/inject_tokens.js';
export class LocalConfig implements LocalConfigData {
userEmailAddress: EmailAddress;
soloVersion: Version;
// The string is the name of the deployment, will be used as the namespace,
// so it needs to be available in all targeted clusters
public deployments: Deployments;
public clusterRefs: ClusterRefs = {};
private readonly skipPromptTask: boolean = false;
public constructor(
private readonly filePath?: string,
private readonly logger?: SoloLogger,
private readonly configManager?: ConfigManager,
) {
this.filePath = patchInject(filePath, InjectTokens.LocalConfigFilePath, this.constructor.name);
this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name);
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
if (!this.filePath || this.filePath === '') throw new MissingArgumentError('a valid filePath is required');
const allowedKeys = ['userEmailAddress', 'deployments', 'clusterRefs', 'soloVersion'];
if (this.configFileExists()) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const parsedConfig = yaml.parse(fileContent);
for (const key in parsedConfig) {
if (!allowedKeys.includes(key)) {
throw new SoloError(ErrorMessages.LOCAL_CONFIG_GENERIC);
}
this[key] = parsedConfig[key];
}
this.validate();
this.skipPromptTask = true;
}
}
private validate(): void {
const errors = validateSync(this, {});
if (errors.length) {
// throw the first error:
const prop = Object.keys(errors[0]?.constraints);
if (prop[0]) {
throw new SoloError(errors[0].constraints[prop[0]]);
} else {
throw new SoloError(ErrorMessages.LOCAL_CONFIG_GENERIC);
}
}
}
public setUserEmailAddress(emailAddress: EmailAddress): this {
this.userEmailAddress = emailAddress;
this.validate();
return this;
}
public setDeployments(deployments: Deployments): this {
this.deployments = deployments;
this.validate();
return this;
}
public setClusterRefs(clusterRefs: ClusterRefs): this {
this.clusterRefs = clusterRefs;
this.validate();
return this;
}
public setSoloVersion(version: Version): this {
this.soloVersion = version;
this.validate();
return this;
}
public configFileExists(): boolean {
return fs.existsSync(this.filePath);
}
public async write(): Promise<void> {
const yamlContent = yaml.stringify({
userEmailAddress: this.userEmailAddress,
deployments: this.deployments,
clusterRefs: this.clusterRefs,
soloVersion: this.soloVersion,
});
await fs.promises.writeFile(this.filePath, yamlContent);
this.logger.info(`Wrote local config to ${this.filePath}: ${yamlContent}`);
}
public promptLocalConfigTask(k8Factory: K8Factory): SoloListrTask<any> {
const self = this;
return {
title: 'Prompt local configuration',
skip: this.skipPromptTask,
task: async (_, task): Promise<void> => {
if (self.configFileExists()) {
self.configManager.setFlag(flags.userEmailAddress, self.userEmailAddress);
}
const isQuiet = self.configManager.getFlag<boolean>(flags.quiet);
const contexts = self.configManager.getFlag<string>(flags.context);
const deploymentName = self.configManager.getFlag<DeploymentName>(flags.deployment);
const namespace = self.configManager.getFlag<NamespaceName>(flags.namespace);
let userEmailAddress = self.configManager.getFlag<EmailAddress>(flags.userEmailAddress);
let deploymentClusters: string = self.configManager.getFlag<string>(flags.deploymentClusters);
if (!userEmailAddress) {
if (isQuiet) throw new SoloError(ErrorMessages.LOCAL_CONFIG_INVALID_EMAIL);
userEmailAddress = await flags.userEmailAddress.prompt(task, userEmailAddress);
self.configManager.setFlag(flags.userEmailAddress, userEmailAddress);
}
if (!deploymentName) throw new SoloError('Deployment name was not specified');
if (!deploymentClusters) {
if (isQuiet) {
deploymentClusters = k8Factory.default().clusters().readCurrent();
} else {
deploymentClusters = await flags.deploymentClusters.prompt(task, deploymentClusters);
}
self.configManager.setFlag(flags.deploymentClusters, deploymentClusters);
}
const parsedClusterRefs = helpers.splitFlagInput(deploymentClusters);
const deployments: Deployments = {
[deploymentName]: {
clusters: parsedClusterRefs,
namespace: namespace.name,
},
};
const parsedContexts = helpers.splitFlagInput(contexts);
if (parsedContexts.length < parsedClusterRefs.length) {
if (!isQuiet) {
const promptedContexts: string[] = [];
for (const clusterRef of parsedClusterRefs) {
const kubeContexts = k8Factory.default().contexts().list();
const context: string = await flags.context.prompt(task, kubeContexts, clusterRef);
self.clusterRefs[clusterRef] = context;
promptedContexts.push(context);
self.configManager.setFlag(flags.context, context);
}
self.configManager.setFlag(flags.context, promptedContexts.join(','));
} else {
const context = k8Factory.default().contexts().readCurrent();
for (const clusterRef of parsedClusterRefs) {
self.clusterRefs[clusterRef] = context;
}
self.configManager.setFlag(flags.context, context);
}
} else {
for (let i = 0; i < parsedClusterRefs.length; i++) {
const clusterRef = parsedClusterRefs[i];
self.clusterRefs[clusterRef] = parsedContexts[i];
self.configManager.setFlag(flags.context, parsedContexts[i]);
}
}
self.userEmailAddress = userEmailAddress;
self.deployments = deployments;
self.soloVersion = helpers.getSoloVersion();
self.validate();
await self.write();
},
};
}
}