Skip to content

Commit

Permalink
feat(local-remote-config): added solo version to local config and rem…
Browse files Browse the repository at this point in the history
…ote config (#1384)

Signed-off-by: instamenta <instamenta@abv.bg>
  • Loading branch information
instamenta authored Feb 14, 2025
1 parent 23e17aa commit 2dca095
Show file tree
Hide file tree
Showing 21 changed files with 351 additions and 158 deletions.
2 changes: 1 addition & 1 deletion src/commands/cluster/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class ClusterCommandTasks {
const localDeployments = localConfig.deployments;
const remoteClusterList: string[] = [];
let deploymentName;
const remoteNamespace = remoteConfig.metadata.name;
const remoteNamespace = remoteConfig.metadata.namespace;
for (const deployment in localConfig.deployments) {
if (localConfig.deployments[deployment].namespace === remoteNamespace) {
deploymentName = deployment;
Expand Down
15 changes: 9 additions & 6 deletions src/commands/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,12 +539,11 @@ export class NetworkCommand extends BaseCommand {
}
}

async prepareConfig(task: any, argv: any) {
async prepareConfig(task: any, argv: any, promptForNodeAliases: boolean = false) {
this.configManager.update(argv);
this.logger.debug('Updated config with argv', {config: this.configManager.config});

// disable the prompts that we don't want to prompt the user for
flags.disablePrompts([
const flagsWithDisabledPrompts = [
flags.apiPermissionProperties,
flags.app,
flags.applicationEnv,
Expand All @@ -557,7 +556,6 @@ export class NetworkCommand extends BaseCommand {
flags.debugNodeAlias,
flags.loadBalancerEnabled,
flags.log4j2Xml,
flags.nodeAliasesUnparsed,
flags.persistentVolumeClaims,
flags.profileName,
flags.profileFile,
Expand All @@ -574,7 +572,12 @@ export class NetworkCommand extends BaseCommand {
flags.gcsEndpoint,
flags.gcsBucket,
flags.gcsBucketPrefix,
]);
];

if (promptForNodeAliases) flagsWithDisabledPrompts.push(flags.nodeAliasesUnparsed);

// disable the prompts that we don't want to prompt the user for
flags.disablePrompts(flagsWithDisabledPrompts);

await this.configManager.executePrompt(task, NetworkCommand.DEPLOY_FLAGS_LIST);
let namespace = await resolveNamespaceFromDeployment(this.localConfig, this.configManager, task);
Expand Down Expand Up @@ -714,7 +717,7 @@ export class NetworkCommand extends BaseCommand {
{
title: 'Initialize',
task: async (ctx, task) => {
ctx.config = await self.prepareConfig(task, argv);
ctx.config = await self.prepareConfig(task, argv, true);
return ListrLease.newAcquireLeaseTask(lease, task);
},
},
Expand Down
24 changes: 18 additions & 6 deletions src/core/config/local_config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* SPDX-License-Identifier: Apache-2.0
*/
import {IsEmail, IsNotEmpty, IsObject, validateSync} from 'class-validator';
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';
Expand All @@ -10,10 +10,10 @@ 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 ClusterRefs, type DeploymentName, type EmailAddress} from './remote/types.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 {splitFlagInput} from '../helpers.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';
Expand All @@ -30,6 +30,10 @@ export class LocalConfig implements LocalConfigData {
)
userEmailAddress: EmailAddress;

@IsString({message: ErrorMessages.LOCAL_CONFIG_INVALID_SOLO_VERSION})
@IsNotEmpty({message: ErrorMessages.LOCAL_CONFIG_INVALID_SOLO_VERSION})
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
@IsDeployments({
Expand Down Expand Up @@ -60,7 +64,7 @@ export class LocalConfig implements LocalConfigData {

if (!this.filePath || this.filePath === '') throw new MissingArgumentError('a valid filePath is required');

const allowedKeys = ['userEmailAddress', 'deployments', 'clusterRefs'];
const allowedKeys = ['userEmailAddress', 'deployments', 'clusterRefs', 'soloVersion'];
if (this.configFileExists()) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const parsedConfig = yaml.parse(fileContent);
Expand Down Expand Up @@ -109,6 +113,12 @@ export class LocalConfig implements LocalConfigData {
return this;
}

public setSoloVersion(version: Version): this {
this.soloVersion = version;
this.validate();
return this;
}

public configFileExists(): boolean {
return fs.existsSync(this.filePath);
}
Expand All @@ -118,6 +128,7 @@ export class LocalConfig implements LocalConfigData {
userEmailAddress: this.userEmailAddress,
deployments: this.deployments,
clusterRefs: this.clusterRefs,
soloVersion: this.soloVersion,
});
await fs.promises.writeFile(this.filePath, yamlContent);

Expand Down Expand Up @@ -159,7 +170,7 @@ export class LocalConfig implements LocalConfigData {
self.configManager.setFlag(flags.deploymentClusters, deploymentClusters);
}

const parsedClusterRefs = splitFlagInput(deploymentClusters);
const parsedClusterRefs = helpers.splitFlagInput(deploymentClusters);

const deployments: Deployments = {
[deploymentName]: {
Expand All @@ -168,7 +179,7 @@ export class LocalConfig implements LocalConfigData {
},
};

const parsedContexts = splitFlagInput(contexts);
const parsedContexts = helpers.splitFlagInput(contexts);

if (parsedContexts.length < parsedClusterRefs.length) {
if (!isQuiet) {
Expand Down Expand Up @@ -200,6 +211,7 @@ export class LocalConfig implements LocalConfigData {

self.userEmailAddress = userEmailAddress;
self.deployments = deployments;
self.soloVersion = helpers.getSoloVersion();

self.validate();
await self.write();
Expand Down
4 changes: 4 additions & 0 deletions src/core/config/local_config_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type DeploymentName,
type EmailAddress,
type NamespaceNameAsString,
type Version,
} from './remote/types.js';

export interface DeploymentStructure {
Expand All @@ -26,4 +27,7 @@ export interface LocalConfigData {

// Every cluster must have a kubectl context associated to it, which is used to establish a connection.
clusterRefs: ClusterRefs;

// Solo CLI version
soloVersion: Version;
}
71 changes: 20 additions & 51 deletions src/core/config/remote/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,45 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {type ToObject} from '../../../types/index.js';
import {type ClusterRef, type ICluster, type NamespaceNameAsString} from './types.js';
import {type ClusterRef, type DeploymentName, type ICluster, type NamespaceNameAsString} from './types.js';
import {SoloError} from '../../errors.js';

export class Cluster implements ICluster, ToObject<ICluster> {
private readonly _name: string;
private readonly _namespace: string;
private readonly _dnsBaseDomain: string = 'cluster.local'; // example: 'us-west-2.gcp.charlie.sphere'`
private readonly _dnsConsensusNodePattern: string = 'network-${nodeAlias}-svc.${namespace}.svc'; // example: '${nodeId}.consensus.prod'`

public constructor(
name: string,
namespace: NamespaceNameAsString,
dnsBaseDomain?: string,
dnsConsensusNodePattern?: string,
public readonly name: string,
public readonly namespace: NamespaceNameAsString,
public readonly deployment: DeploymentName,
public readonly dnsBaseDomain: string = 'cluster.local', // example: 'us-west-2.gcp.charlie.sphere'`
public readonly dnsConsensusNodePattern: string = 'network-${nodeAlias}-svc.${namespace}.svc', // example: '${nodeId}.consensus.prod'`
) {
if (!name) {
throw new SoloError('name is required');
}

if (typeof name !== 'string') {
throw new SoloError(`Invalid type for name: ${typeof name}`);
}

if (!namespace) {
throw new SoloError('namespace is required');
}

if (typeof namespace !== 'string') {
throw new SoloError(`Invalid type for namespace: ${typeof namespace}`);
}
if (!name) throw new SoloError('name is required');
if (typeof name !== 'string') throw new SoloError(`Invalid type for name: ${typeof name}`);

this._name = name;
this._namespace = namespace;

if (dnsBaseDomain) {
this._dnsBaseDomain = dnsBaseDomain;
}

if (dnsConsensusNodePattern) {
this._dnsConsensusNodePattern = dnsConsensusNodePattern;
}
}

public get name(): string {
return this._name;
}

public get namespace(): string {
return this._namespace;
}

public get dnsBaseDomain(): string {
return this._dnsBaseDomain;
}
if (!namespace) throw new SoloError('namespace is required');
if (typeof namespace !== 'string') throw new SoloError(`Invalid type for namespace: ${typeof namespace}`);

public get dnsConsensusNodePattern(): string {
return this._dnsConsensusNodePattern;
if (!deployment) throw new SoloError('deployment is required');
if (typeof deployment !== 'string') throw new SoloError(`Invalid type for deployment: ${typeof deployment}`);
}

public toObject(): ICluster {
return {
name: this.name,
namespace: this.namespace,
deployment: this.deployment,
dnsBaseDomain: this.dnsBaseDomain,
dnsConsensusNodePattern: this.dnsConsensusNodePattern,
};
}

public static fromObject(cluster: ICluster) {
return new Cluster(cluster.name, cluster.namespace, cluster.dnsBaseDomain, cluster.dnsConsensusNodePattern);
return new Cluster(
cluster.name,
cluster.namespace,
cluster.deployment,
cluster.dnsBaseDomain,
cluster.dnsConsensusNodePattern,
);
}

public static toClustersMapObject(clustersMap: Record<ClusterRef, Cluster>) {
Expand Down
76 changes: 47 additions & 29 deletions src/core/config/remote/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Migration} from './migration.js';
import {SoloError} from '../../errors.js';
import * as k8s from '@kubernetes/client-node';
import {
type DeploymentName,
type EmailAddress,
type NamespaceNameAsString,
type RemoteConfigMetadataStructure,
Expand All @@ -22,20 +23,21 @@ import {type Optional, type ToObject, type Validate} from '../../../types/index.
export class RemoteConfigMetadata
implements RemoteConfigMetadataStructure, Validate, ToObject<RemoteConfigMetadataStructure>
{
private readonly _name: NamespaceNameAsString;
private readonly _lastUpdatedAt: Date;
private readonly _lastUpdateBy: EmailAddress;
private _migration?: Migration;

public constructor(
name: NamespaceNameAsString,
lastUpdatedAt: Date,
lastUpdateBy: EmailAddress,
public readonly namespace: NamespaceNameAsString,
public readonly deploymentName: DeploymentName,
public readonly lastUpdatedAt: Date,
public readonly lastUpdateBy: EmailAddress,
public readonly soloVersion: Version,
public soloChartVersion: Version = '',
public hederaPlatformVersion: Version = '',
public hederaMirrorNodeChartVersion: Version = '',
public hederaExplorerChartVersion: Version = '',
public hederaJsonRpcRelayChartVersion: Version = '',
migration?: Migration,
) {
this._name = name;
this._lastUpdatedAt = lastUpdatedAt;
this._lastUpdateBy = lastUpdateBy;
this._migration = migration;
this.validate();
}
Expand All @@ -49,29 +51,14 @@ export class RemoteConfigMetadata

/* -------- Getters -------- */

/** Retrieves the namespace */
public get name(): NamespaceNameAsString {
return this._name;
}

/** Retrieves the date when the remote config metadata was last updated */
public get lastUpdatedAt(): Date {
return this._lastUpdatedAt;
}

/** Retrieves the email of the user who last updated the remote config metadata */
public get lastUpdateBy(): EmailAddress {
return this._lastUpdateBy;
}

/** Retrieves the migration if such exists */
public get migration(): Optional<Migration> {
return this._migration;
}

/* -------- Utilities -------- */

/** Handles conversion from plain object to instance */
/** Handles conversion from a plain object to instance */
public static fromObject(metadata: RemoteConfigMetadataStructure): RemoteConfigMetadata {
let migration: Optional<Migration> = undefined;

Expand All @@ -82,12 +69,32 @@ export class RemoteConfigMetadata
migration = new Migration(new Date(migratedAt), migratedBy, fromVersion);
}

return new RemoteConfigMetadata(metadata.name, new Date(metadata.lastUpdatedAt), metadata.lastUpdateBy, migration);
return new RemoteConfigMetadata(
metadata.namespace,
metadata.deploymentName,
new Date(metadata.lastUpdatedAt),
metadata.lastUpdateBy,
metadata.soloVersion,
metadata.soloChartVersion,
metadata.hederaPlatformVersion,
metadata.hederaMirrorNodeChartVersion,
metadata.hederaExplorerChartVersion,
metadata.hederaJsonRpcRelayChartVersion,
migration,
);
}

public validate(): void {
if (!this.name || !(typeof this.name === 'string')) {
throw new SoloError(`Invalid name: ${this.name}, is type string: ${typeof this.name === 'string'}`);
if (!this.namespace || !(typeof this.namespace === 'string')) {
throw new SoloError(
`Invalid namespace: ${this.namespace}, is type string: ${typeof this.namespace === 'string'}`,
);
}

if (!this.deploymentName || !(typeof this.deploymentName === 'string')) {
throw new SoloError(
`Invalid deploymentName: ${this.deploymentName}, is type string: ${typeof this.deploymentName === 'string'}`,
);
}

if (!(this.lastUpdatedAt instanceof Date)) {
Expand All @@ -98,16 +105,27 @@ export class RemoteConfigMetadata
throw new SoloError(`Invalid lastUpdateBy: ${this.lastUpdateBy}`);
}

if (!this.soloVersion || typeof this.soloVersion !== 'string') {
throw new SoloError(`Invalid soloVersion: ${this.soloVersion}`);
}

if (this.migration && !(this.migration instanceof Migration)) {
throw new SoloError(`Invalid migration: ${this.migration}`);
}
}

public toObject(): RemoteConfigMetadataStructure {
const data = {
name: this.name,
namespace: this.namespace,
deploymentName: this.deploymentName,
lastUpdatedAt: new k8s.V1MicroTime(this.lastUpdatedAt),
lastUpdateBy: this.lastUpdateBy,
soloChartVersion: this.soloChartVersion,
hederaPlatformVersion: this.hederaPlatformVersion,
hederaMirrorNodeChartVersion: this.hederaMirrorNodeChartVersion,
hederaExplorerChartVersion: this.hederaExplorerChartVersion,
hederaJsonRpcRelayChartVersion: this.hederaJsonRpcRelayChartVersion,
soloVersion: this.soloVersion,
} as RemoteConfigMetadataStructure;

if (this.migration) data.migration = this.migration.toObject() as any;
Expand Down
Loading

0 comments on commit 2dca095

Please sign in to comment.