Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(multi-cluster): add the ability to specify inside which cluster to add new node with solo node add #1687

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
revert changes to get logs
Signed-off-by: Zhan Milenkov <instamenta@abv.bg>
instamenta committed Mar 21, 2025
commit 481499f9ccc218fbb2128d0f6071c88f509521cd
122 changes: 58 additions & 64 deletions src/commands/node/tasks.ts
Original file line number Diff line number Diff line change
@@ -58,7 +58,7 @@
type ConfigBuilder,
type NodeAlias,
type NodeAliases,
type NodeId,

Check failure on line 61 in src/commands/node/tasks.ts

GitHub Actions / Code Style / Standard

'NodeId' is defined but never used
type SkipCheck,
} from '../../types/aliases.js';
import {PodName} from '../../core/kube/resources/pod/pod-name.js';
@@ -1682,12 +1682,22 @@
task: async ctx => {
// Prepare parameter and update the network node chart
const config = ctx.config;

const consensusNodes = ctx.config.consensusNodes as ConsensusNode[];
const clusterRefs = this.remoteConfigManager.getClusterRefs();
const valuesArgMap: Record<ClusterRef, string> = {};

// Make sure valuesArgMap is initialized with empty strings
const valuesArgMap: Record<ClusterRef, string> = {};
Object.keys(clusterRefs).forEach(clusterRef => (valuesArgMap[clusterRef] = ''));
if (consensusNodes.length) {
consensusNodes.forEach(node => (valuesArgMap[node.cluster] = ''));
} else {
valuesArgMap[this.k8Factory.default().clusters().readCurrent()] = '';
}

const clusterRefs = this.remoteConfigManager.getClusterRefs();
if (!Object.keys(clusterRefs).length) {
const clusterRef = this.k8Factory.default().clusters().readCurrent();
clusterRefs[clusterRef] = this.localConfig.clusterRefs[clusterRef];
}

if (!config.serviceMap) {
config.serviceMap = await self.accountManager.getNodeServiceMap(
@@ -1704,53 +1714,32 @@
}

const nodeId = maxNodeId + 1;
const index = config.existingNodeAliases.length;

const clusterNodeIndexMap: Record<ClusterRef, Record<NodeId, /* index in the chart -> */ number>> = {};

for (const clusterRef of Object.keys(clusterRefs)) {
clusterNodeIndexMap[clusterRef] = {};

consensusNodes
.filter(node => node.cluster === clusterRef)
.sort((a, b) => a.nodeId - b.nodeId)
.forEach((node, index) => (clusterNodeIndexMap[clusterRef][node.nodeId] = index));
}

for (const consensusNode of consensusNodes) {
if (transactionType !== NodeSubcommandType.DELETE && transactionType !== NodeSubcommandType.UPDATE) {
break;
}
// On Update and Delete
for (let i = 0; i < index; i++) {
const consensusNode = consensusNodes.find(node => node.nodeId === i);
if (!consensusNode) break; // break in the case that no consensus node is found, which can happen from a node delete
const clusterRef = consensusNode ? consensusNode.cluster : this.k8Factory.default().clusters().readCurrent();

const clusterRef = consensusNode.cluster;
const index = clusterNodeIndexMap[clusterRef][consensusNode.nodeId];

// for the case of updating node, use new account number for this node id
// TODO the node array index in the set command will be different from the loop index in the case of multiple clusters
// TODO also, if a node delete has been ran, or a node add, then the node array will still have to be contiguous, but the nodeId will not match the index
if (
transactionType === NodeSubcommandType.UPDATE &&
config.newAccountNumber &&
consensusNode.name === config.nodeAlias
i === Templates.nodeIdFromNodeAlias(config.nodeAlias)
) {
// for the case of updating node
// use new account number for this node id
valuesArgMap[clusterRef] +=
` --set "hedera.nodes[${index}].accountId=${config.newAccountNumber}"` +
` --set "hedera.nodes[${index}].name=${config.nodeAlias}"` +
` --set "hedera.nodes[${index}].nodeId=${consensusNode.nodeId}"`;
}

// Delete if nodeIds don't match
else if (transactionType !== NodeSubcommandType.DELETE || consensusNode.nodeId !== nodeId) {
` --set "hedera.nodes[${i}].accountId=${config.newAccountNumber}" --set "hedera.nodes[${i}].name=${config.nodeAlias}" --set "hedera.nodes[${i}].nodeId=${i}" `;
} else if (transactionType !== NodeSubcommandType.DELETE || i !== nodeId) {
// for the case of deleting node
valuesArgMap[clusterRef] +=
` --set "hedera.nodes[${index}].accountId=${config.serviceMap.get(consensusNode.name).accountId}"` +
` --set "hedera.nodes[${index}].name=${consensusNode.name}"` +
` --set "hedera.nodes[${index}].nodeId=${consensusNode.nodeId}"`;
}

// Delete if nodeIds match
else if (transactionType === NodeSubcommandType.DELETE && consensusNode.nodeId === nodeId) {
` --set "hedera.nodes[${i}].accountId=${config.serviceMap.get(config.existingNodeAliases[i]).accountId}" --set "hedera.nodes[${i}].name=${config.existingNodeAliases[i]}" --set "hedera.nodes[${i}].nodeId=${i}" `;
} else if (transactionType === NodeSubcommandType.DELETE && i === nodeId) {
valuesArgMap[clusterRef] +=
` --set "hedera.nodes[${index}].accountId=${IGNORED_NODE_ACCOUNT_ID}"` +
` --set "hedera.nodes[${index}].name=${consensusNode.name}"` +
` --set "hedera.nodes[${index}].nodeId=${consensusNode.nodeId}" `;
` --set "hedera.nodes[${i}].accountId=${IGNORED_NODE_ACCOUNT_ID}" --set "hedera.nodes[${i}].name=${config.existingNodeAliases[i]}" --set "hedera.nodes[${i}].nodeId=${i}" `;
}
}

@@ -1761,8 +1750,8 @@

// When adding a new node
if (transactionType === NodeSubcommandType.ADD && ctx.newNode && ctx.newNode.accountId) {
const clusterRef = ctx.config.clusterRef;
const index = config.existingNodeAliases.length;
const consensusNode = consensusNodes.find(node => node.nodeId === index);
const clusterRef = consensusNode ? consensusNode.cluster : this.k8Factory.default().clusters().readCurrent();

valuesArgMap[clusterRef] +=
` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}"` +
@@ -1777,18 +1766,31 @@
config.envoyIpsParsed = Templates.parseNodeAliasToIpMapping(config.envoyIps);
}

const nodeIndexInValues = clusterNodeIndexMap[clusterRef][nodeId];
const nodeAlias: NodeAlias = config.nodeAlias;
const nodeIndexInValues = Templates.nodeIdFromNodeAlias(nodeAlias);
const consensusNodeInValues = consensusNodes.find(node => node.name === nodeAlias);
const clusterForConsensusNodeInValues = consensusNodeInValues
? consensusNodeInValues.cluster
: this.k8Factory.default().clusters().readCurrent();

// Set static IPs for HAProxy
if (config.haproxyIpsParsed) {
const ip: string = config.haproxyIpsParsed?.[config.nodeAlias];
if (ip) valuesArgMap[clusterRef] += ` --set "hedera.nodes[${nodeIndexInValues}].haproxyStaticIP=${ip}"`;
const ip: string = config.haproxyIpsParsed?.[nodeAlias];

if (ip) {
valuesArgMap[clusterForConsensusNodeInValues] +=
` --set "hedera.nodes[${nodeIndexInValues}].haproxyStaticIP=${ip}"`;
}
}

// Set static IPs for Envoy Proxy
if (config.envoyIpsParsed) {
const ip: string = config.envoyIpsParsed?.[config.nodeAlias];
if (ip) valuesArgMap[clusterRef] += ` --set "hedera.nodes[${nodeIndexInValues}].envoyProxyStaticIP=${ip}"`;
const ip: string = config.envoyIpsParsed?.[nodeAlias];

if (ip) {
valuesArgMap[clusterForConsensusNodeInValues] +=
` --set "hedera.nodes[${nodeIndexInValues}].envoyProxyStaticIP=${ip}"`;
}
}
}

@@ -1818,26 +1820,18 @@

valuesArgMap[clusterRef] = addDebugOptions(valuesArgMap[clusterRef], config.debugNodeAlias);

console.dir({clusterNodeIndexMap, valuesArgMap, consensusNodes}, {depth: null});
console.dir({config: ctx.config, valuesArgMap, consensusNodes, clusterRefs}, {depth: null});

// Update charts
await Promise.all(
Object.keys(clusterRefs).map(async clusterRef => {
const valuesArgs = valuesArgMap[clusterRef];
const context = this.localConfig.clusterRefs[clusterRef];

await self.chartManager.upgrade(
config.namespace,
constants.SOLO_DEPLOYMENT_CHART,
ctx.config.chartPath,
config.soloChartVersion,
valuesArgs,
context,
);

showVersionBanner(self.logger, constants.SOLO_DEPLOYMENT_CHART, config.soloChartVersion, 'Upgraded');
}),
await self.chartManager.upgrade(
config.namespace,
constants.SOLO_DEPLOYMENT_CHART,
ctx.config.chartPath,
config.soloChartVersion,
valuesArgMap[clusterRef],
this.localConfig.clusterRefs[clusterRef],
);
showVersionBanner(self.logger, constants.SOLO_DEPLOYMENT_CHART, config.soloChartVersion, 'Upgraded');
},
skip,
};

Unchanged files with check annotations Beta

import {SemVer} from 'semver';
export class Version<T extends SemVer | number> {
constructor(public readonly value: T) {

Check warning on line 6 in src/business/utils/version.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition constructor
if (Version.isSemVer(value) && !value) {
throw new RangeError('Invalid version');
}
public readonly localConfig: LocalConfig;
protected readonly remoteConfigManager: RemoteConfigManager;
constructor(opts: Opts) {

Check warning on line 55 in src/commands/base.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition constructor
if (!opts || !opts.helm) throw new Error('An instance of core/Helm is required');
if (!opts || !opts.k8Factory) throw new Error('An instance of core/K8Factory is required');
if (!opts || !opts.chartManager) throw new Error('An instance of core/ChartManager is required');
* @param chartDirectory - the chart directory
* @param profileValuesFile - mapping of clusterRef to the profile values file full path
*/
static prepareValuesFilesMapMulticluster(

Check warning on line 88 in src/commands/base.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition prepareValuesFilesMapMulticluster
clusterRefs: ClusterRefs,
chartDirectory?: string,
profileValuesFile?: Record<ClusterRef, string>,
* @param chartDirectory - the chart directory
* @param profileValuesFile - the profile values file full path
*/
static prepareValuesFilesMap(

Check warning on line 157 in src/commands/base.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition prepareValuesFilesMap
clusterRefs: ClusterRefs,
chartDirectory?: string,
profileValuesFile?: string,
return valuesFiles;
}
abstract close(): Promise<void>;

Check warning on line 224 in src/commands/base.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition close
/**
* Setup home directories
private static readonly CONNECT_CONFIGS_NAME = 'connectConfig';
private static readonly DEFAULT_CONFIGS_NAME = 'defaultConfig';
constructor(

Check warning on line 35 in src/commands/cluster/configs.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition constructor
@inject(InjectTokens.ConfigManager) private readonly configManager: ConfigManager,
@inject(InjectTokens.SoloLogger) private readonly logger: SoloLogger,
@inject(InjectTokens.ChartManager) private readonly chartManager: ChartManager,
@injectable()
export class ClusterCommandHandlers extends CommandHandler {
constructor(

Check warning on line 18 in src/commands/cluster/handlers.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition constructor
@inject(InjectTokens.ClusterCommandTasks) private readonly tasks: ClusterCommandTasks,
@inject(InjectTokens.LocalConfig) private readonly localConfig: LocalConfig,
@inject(InjectTokens.ClusterCommandConfigs) private readonly configs: ClusterCommandConfigs,
export class ClusterCommand extends BaseCommand {
public handlers: ClusterCommandHandlers;
constructor(opts: Opts) {

Check warning on line 17 in src/commands/cluster/index.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition constructor
super(opts);
this.handlers = patchInject(null, InjectTokens.ClusterCommandHandlers, this.constructor.name);
public static readonly COMMAND_NAME = 'cluster-ref';
getCommandDefinition() {

Check warning on line 25 in src/commands/cluster/index.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition getCommandDefinition
return {
command: ClusterCommand.COMMAND_NAME,
desc: 'Manage solo testing cluster',
};
}
close(): Promise<void> {

Check warning on line 102 in src/commands/cluster/index.ts

GitHub Actions / Code Style / Standard

Missing accessibility modifier on method definition close
// no-op
return Promise.resolve();
}