@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
198 lines (173 loc) • 8.41 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {inject, injectable} from 'tsyringe-neo';
import {InjectTokens} from '../../../../core/dependency-injection/inject-tokens.js';
import {LocalConfigSource} from '../../../../data/configuration/impl/local-config-source.js';
import {YamlFileStorageBackend} from '../../../../data/backend/impl/yaml-file-storage-backend.js';
import {ObjectMapper} from '../../../../data/mapper/api/object-mapper.js';
import {patchInject} from '../../../../core/dependency-injection/container-helper.js';
import {ClassToObjectMapper} from '../../../../data/mapper/impl/class-to-object-mapper.js';
import {ConfigKeyFormatter} from '../../../../data/key/config-key-formatter.js';
import {LocalConfigSchemaDefinition} from '../../../../data/schema/migration/impl/local/local-config-schema-definition.js';
import {LocalConfigSchema} from '../../../../data/schema/model/local/local-config-schema.js';
import {RefreshLocalConfigSourceError} from '../../../errors/refresh-local-config-source-error.js';
import {WriteLocalConfigFileError} from '../../../errors/write-local-config-file-error.js';
import {PathEx} from '../../../utils/path-ex.js';
import fs, {existsSync, mkdirSync} from 'node:fs';
import {LocalConfig} from './local-config.js';
import path from 'node:path';
import {Templates} from '../../../../core/templates.js';
import {type ConfigManager} from '../../../../core/config-manager.js';
import {Flags as flags} from '../../../../commands/flags.js';
export class LocalConfigRuntimeState {
private readonly source: LocalConfigSource;
private readonly backend: YamlFileStorageBackend;
private readonly objectMapper: ObjectMapper;
public isLoaded: boolean = false;
private _localConfig: LocalConfig;
public constructor(
private readonly basePath: string,
private readonly fileName: string,
private readonly configManager?: ConfigManager,
) {
this.fileName = patchInject(fileName, InjectTokens.LocalConfigFileName, this.constructor.name);
this.basePath = patchInject(basePath, InjectTokens.HomeDirectory, this.constructor.name);
this.configManager = patchInject(configManager, InjectTokens.ConfigManager, this.constructor.name);
this.backend = new YamlFileStorageBackend(this.basePath);
this.objectMapper = new ClassToObjectMapper(ConfigKeyFormatter.instance());
this.source = new LocalConfigSource(
fileName,
new LocalConfigSchemaDefinition(this.objectMapper),
this.objectMapper,
this.backend,
LocalConfigSchema.EMPTY,
);
}
public get configuration(): LocalConfig {
if (!this.isLoaded) {
throw new Error('configuration: Local configuration is not loaded yet. Please call load() first.');
}
return this._localConfig;
}
// Loads the source data and writes it back in case of migrations.
public async load(): Promise<void> {
// TODO this needs to be a migration, not a load
// check if config from an old version exists under the cache directory
const oldConfigPath: string = PathEx.join(this.basePath, 'cache');
const oldConfigFile: string = PathEx.join(oldConfigPath, this.fileName);
const oldConfigFileExists: boolean = existsSync(oldConfigFile);
if (this.configFileExists() && oldConfigFileExists) {
// if both files exist, remove the old one
fs.rmSync(oldConfigFile);
} else if (existsSync(oldConfigFile)) {
// if only the old file exists, copy it to the new location
mkdirSync(this.basePath, {recursive: true});
fs.copyFileSync(oldConfigFile, PathEx.join(this.basePath, this.fileName));
fs.rmSync(oldConfigFile);
}
this.refresh();
if (!this.configFileExists()) {
return await this.persist();
}
try {
await this.source.refresh();
this.refresh();
} catch (error) {
throw new RefreshLocalConfigSourceError('Failed to refresh local config source', error);
}
await this.persist();
await this.migrateCacheDirectories();
this.isLoaded = true;
}
/**
* Migrates the cache directories to the new structure.
* It will look for directories in the format 'v0.58/staging/v0.58.10' and move them to current staging directory.
*/
private async migrateCacheDirectories(): Promise<void> {
if (!this.isLoaded) {
throw new Error('migrateCacheDirectories: Local configuration is not loaded yet. Please call load() first.');
}
const cacheDirectory: string = PathEx.join(this.basePath, 'cache').toString();
const releaseTag: string = this.configManager.getFlag(flags.releaseTag);
const currentStagingDirectory: string = Templates.renderStagingDir(cacheDirectory, releaseTag);
if (fs.existsSync(currentStagingDirectory)) {
return;
}
// migrate the staging directory if it exists
const foundStagingDirectory: string[] = await this.findMatchingSoloCacheDirectories(
PathEx.join(this.basePath, 'cache').toString(),
);
if (foundStagingDirectory && foundStagingDirectory.length > 0) {
for (const stagingDirectory of foundStagingDirectory) {
// Guard against accidental self-copy when the discovered path already points to
// the current release staging directory.
if (stagingDirectory === currentStagingDirectory) {
continue;
}
// Keep source staging directories intact to avoid deleting another command's active staging path
// when multiple commands run concurrently (for example one-shot parallel subcommands).
fs.cpSync(stagingDirectory, currentStagingDirectory, {recursive: true, force: true});
}
}
}
private async findMatchingSoloCacheDirectories(baseDirectory: string): Promise<string[]> {
if (!this.isLoaded) {
throw new Error(
'findMatchingSoloCacheDirectories: Local configuration is not loaded yet. Please call load() first.',
);
}
// Regex to match directory names like 'v0.58' or 'v0.60'
// This will capture the version number.
const versionDirectionRegex: RegExp = /^v(\d+\.\d+)$/;
// Regex to match the full path structure like 'v0.58/staging/v0.58.10'
// This captures the major.minor version and the patch version.
const fullPathRegex: RegExp = /^v(\d+\.\d+)\/staging\/v(\d+\.\d+\.\d+)$/;
const matchingDirectories: string[] = [];
try {
// 1. Read the contents of the baseCacheDir (e.g., '.solo/cache/')
const versionDirectories: string[] = fs.readdirSync(baseDirectory);
for (const versionDirectory of versionDirectories) {
const versionMatch: string[] | null = versionDirectory.match(versionDirectionRegex);
if (versionMatch) {
// If the version directory matches (e.g., 'v0.58')
const fullVersionPath: string = PathEx.join(baseDirectory, versionDirectory, 'staging');
// Check if 'staging' directory exists within the version directory
if (fs.existsSync(fullVersionPath)) {
// Read the contents of the 'staging' directory
const stagingContents: string[] = fs.readdirSync(fullVersionPath);
for (const stagingItem of stagingContents) {
const fullItemPath: string = PathEx.join(fullVersionPath, stagingItem);
const relativeItemPath: string = path.relative(baseDirectory, fullItemPath); // Get path relative to baseCacheDir
// Check if the full relative path matches the desired pattern
if (fullPathRegex.test(relativeItemPath) && fs.existsSync(fullItemPath)) {
matchingDirectories.push(fullItemPath);
}
}
}
}
}
} catch {
// The Directory isn't found or any other error
return undefined;
}
return matchingDirectories;
}
public async persist(): Promise<void> {
try {
await this.source.persist();
this.isLoaded = true;
} catch (error) {
throw new WriteLocalConfigFileError('Failed to write local config file', error);
}
}
private refresh(): void {
this._localConfig = new LocalConfig(this.source.modelData);
}
public configFileExists(): boolean {
try {
return fs.existsSync(PathEx.join(this.basePath, this.fileName));
} catch {
return false;
}
}
}