@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
263 lines • 17 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { BaseCommandTest } from './base-command-test.js';
import { main } from '../../../../src/index.js';
import { Flags } from '../../../../src/commands/flags.js';
import { InjectTokens } from '../../../../src/core/dependency-injection/inject-tokens.js';
import { expect } from 'chai';
import { container } from 'tsyringe-neo';
import { DeploymentCommandDefinition } from '../../../../src/commands/command-definitions/deployment-command-definition.js';
import fs from 'node:fs/promises';
import yaml from 'yaml';
import { PathEx } from '../../../../src/business/utils/path-ex.js';
export class DeploymentTest extends BaseCommandTest {
static soloDeploymentCreateArgv(testName, deployment, namespace, realm, shard) {
const { newArgv, optionFromFlag, argvPushGlobalFlags } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CONFIG_SUBCOMMAND_NAME, DeploymentCommandDefinition.CONFIG_CREATE, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.namespace), namespace.name, optionFromFlag(Flags.realm), String(realm), optionFromFlag(Flags.shard), String(shard));
argvPushGlobalFlags(argv, testName);
return argv;
}
static create(options) {
const { testName, testLogger, deployment, namespace, realm, shard } = options;
const { soloDeploymentCreateArgv } = DeploymentTest;
it(`${testName}: solo deployment config create`, async () => {
testLogger.info(`${testName}: beginning solo deployment config create`);
await main(soloDeploymentCreateArgv(testName, deployment, namespace, realm, shard));
// TODO check that the deployment was created
testLogger.info(`${testName}: finished solo deployment config create`);
});
}
static soloDeploymentAddClusterArgv(testName, deployment, clusterReference, numberOfNodes) {
const { newArgv, optionFromFlag, argvPushGlobalFlags } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CLUSTER_SUBCOMMAND_NAME, DeploymentCommandDefinition.CLUSTER_ATTACH, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.clusterRef), clusterReference, optionFromFlag(Flags.numberOfConsensusNodes), numberOfNodes.toString());
argvPushGlobalFlags(argv, testName);
return argv;
}
static addCluster(options) {
const { testName, testLogger, deployment, clusterReferenceNameArray, consensusNodesCount } = options;
const { soloDeploymentAddClusterArgv } = DeploymentTest;
it(`${testName}: solo deployment cluster attach`, async () => {
testLogger.info(`${testName}: beginning solo deployment cluster attach`);
// Compute distribution
const clusterCount = clusterReferenceNameArray.length;
const base = Math.floor(consensusNodesCount / clusterCount);
const remainder = consensusNodesCount % clusterCount;
const nodeCountsPerCluster = clusterReferenceNameArray.map((_, index) => index < remainder ? base + 1 : base);
// Now attach clusters with correct node count
for (const [index, element] of clusterReferenceNameArray.entries()) {
const nodeCount = nodeCountsPerCluster[index];
await main(soloDeploymentAddClusterArgv(testName, deployment, element, nodeCount));
}
const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState);
expect(remoteConfig.isLoaded(), 'remote config manager should be loaded').to.be.true;
const consensusNodes = remoteConfig.configuration.components.state.consensusNodes;
expect(Object.entries(consensusNodes).length, `consensus node count should be ${consensusNodesCount}`).to.equal(consensusNodesCount);
// Verify each cluster received the expected number of nodes (not a fixed index mapping).
const nodeCountsByCluster = new Map();
for (const node of Object.values(consensusNodes)) {
const cluster = node.metadata.cluster;
nodeCountsByCluster.set(cluster, (nodeCountsByCluster.get(cluster) ?? 0) + 1);
}
for (const [index, element] of clusterReferenceNameArray.entries()) {
expect(nodeCountsByCluster.get(element) ?? 0, `cluster ${element} should have ${nodeCountsPerCluster[index]} node(s)`).to.equal(nodeCountsPerCluster[index]);
}
testLogger.info(`${testName}: finished solo deployment cluster attach`);
});
}
static soloDeploymentDiagnosticsLogsArgv(deployment) {
const { newArgv, optionFromFlag } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.DIAGNOSTICS_SUBCOMMAND_NAME, DeploymentCommandDefinition.DIAGNOSTICS_LOGS, optionFromFlag(Flags.deployment), deployment);
return argv;
}
static soloDeploymentConfigCreateArgv(deployment, namespace) {
const { newArgv, optionFromFlag } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CONFIG_SUBCOMMAND_NAME, DeploymentCommandDefinition.CONFIG_CREATE, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.namespace), namespace.name);
return argv;
}
static soloDeploymentClusterAttachArgv(deployment, clusterReference, consensusNodesCount) {
const { newArgv, optionFromFlag } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CLUSTER_SUBCOMMAND_NAME, DeploymentCommandDefinition.CLUSTER_ATTACH, optionFromFlag(Flags.clusterRef), clusterReference, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.numberOfConsensusNodes), consensusNodesCount.toString());
return argv;
}
static soloDeploymentConfigListArgv(testName, clusterReference) {
const { newArgv, optionFromFlag, argvPushGlobalFlags } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CONFIG_SUBCOMMAND_NAME, DeploymentCommandDefinition.CONFIG_LIST);
if (clusterReference) {
argv.push(optionFromFlag(Flags.clusterRef), clusterReference);
}
argvPushGlobalFlags(argv, testName);
return argv;
}
/**
* Lists all local deployment configurations or deployments in a specific cluster.
* Tests both scenarios:
* 1. Without cluster-ref: Lists all deployments from local configuration
* 2. With cluster-ref: Lists deployments from the specified Kubernetes cluster
*/
static listDeployments(options) {
const { testName, testLogger, clusterReferenceNameArray } = options;
const { soloDeploymentConfigListArgv } = DeploymentTest;
it(`${testName}: solo deployment config list (without cluster-ref)`, async () => {
testLogger.info(`${testName}: beginning solo deployment config list without cluster-ref`);
await main(soloDeploymentConfigListArgv(testName));
testLogger.info(`${testName}: finished solo deployment config list without cluster-ref`);
});
if (clusterReferenceNameArray && clusterReferenceNameArray.length > 0) {
it(`${testName}: solo deployment config list (with cluster-ref)`, async () => {
testLogger.info(`${testName}: beginning solo deployment config list with cluster-ref`);
await main(soloDeploymentConfigListArgv(testName, clusterReferenceNameArray[0]));
testLogger.info(`${testName}: finished solo deployment config list with cluster-ref`);
});
}
}
static soloDeploymentConfigInfoArgv(testName, deployment) {
const { newArgv, optionFromFlag, argvPushGlobalFlags } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CONFIG_SUBCOMMAND_NAME, DeploymentCommandDefinition.CONFIG_INFO, optionFromFlag(Flags.deployment), deployment);
argvPushGlobalFlags(argv, testName);
return argv;
}
static info(options) {
const { testName, testLogger, deployment } = options;
const { soloDeploymentConfigInfoArgv } = DeploymentTest;
it(`${testName}: solo deployment config info`, async () => {
testLogger.info(`${testName}: beginning solo deployment config info`);
await main(soloDeploymentConfigInfoArgv(testName, deployment));
testLogger.info(`${testName}: finished solo deployment config info`);
});
}
static verifyDeploymentConfigInfo(options) {
const { testName, testLogger, deployment, namespace } = options;
const { soloDeploymentConfigInfoArgv, runMainAndCaptureOutputToJson } = DeploymentTest;
it(`${testName}: verify deployment config info output`, async () => {
testLogger.info(`${testName}: beginning deployment config info output verification`);
const { stdout, outputFilePath } = await runMainAndCaptureOutputToJson(soloDeploymentConfigInfoArgv(testName, deployment), {
testName,
outputFileName: 'deployment-config-info-output.json',
metadata: {
command: 'deployment config info',
deployment,
namespace: namespace.name,
},
});
expect(stdout).to.contain('Deployment:');
expect(stdout).to.contain(deployment);
expect(stdout).to.contain('Namespace:');
expect(stdout).to.contain(namespace.name);
testLogger.info(`${testName}: deployment config info output saved to ${outputFilePath}`);
testLogger.info(`${testName}: finished deployment config info output verification`);
});
}
static soloDeploymentConfigPortsArgv(testName, deployment, clusterReference, output, cacheDirectory) {
const { newArgv, optionFromFlag, argvPushGlobalFlags } = DeploymentTest;
const argv = newArgv();
argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.CONFIG_SUBCOMMAND_NAME, DeploymentCommandDefinition.CONFIG_PORTS, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.clusterRef), clusterReference, optionFromFlag(Flags.cacheDir), cacheDirectory);
if (output) {
argv.push(optionFromFlag(Flags.output), output);
}
argvPushGlobalFlags(argv, testName);
return argv;
}
static verifyDeploymentConfigPorts(options) {
const { testName, testLogger, deployment, clusterReferenceNameArray, namespace, testCacheDirectory } = options;
const { soloDeploymentConfigPortsArgv, runMainAndCaptureOutputToJson, assertPortsFile } = DeploymentTest;
it(`${testName}: verify deployment config ports output`, async () => {
testLogger.info(`${testName}: beginning deployment config ports output verification`);
const clusterReference = clusterReferenceNameArray[0];
const outputDirectory = PathEx.join(testCacheDirectory, 'output');
const jsonPortsFile = PathEx.join(outputDirectory, 'forwarded-ports.json');
const yamlPortsFile = PathEx.join(outputDirectory, 'forwarded-ports.yaml');
await fs.rm(jsonPortsFile, { force: true });
await fs.rm(yamlPortsFile, { force: true });
console.log(options);
const wideResult = await runMainAndCaptureOutputToJson(soloDeploymentConfigPortsArgv(testName, deployment, clusterReference, 'wide', testCacheDirectory), {
testName,
outputFileName: 'deployment-config-ports-wide-output.json',
metadata: {
command: 'deployment config ports',
deployment,
namespace: namespace.name,
clusterReference,
output: 'wide',
},
});
expect(wideResult.stdout).to.contain('Port-forwards for deployment');
expect(wideResult.stdout).to.contain(deployment);
expect(wideResult.stdout).to.contain('Cluster:');
expect(wideResult.stdout).to.contain(clusterReference);
expect(wideResult.stdout).to.contain('Namespace:');
expect(wideResult.stdout).to.contain('Consensus node gRPC');
expect(wideResult.stdout).to.contain('Mirror node REST');
expect(wideResult.stdout).to.contain('JSON-RPC relay');
expect(wideResult.stdout).to.contain('Explorer');
const jsonResult = await runMainAndCaptureOutputToJson(soloDeploymentConfigPortsArgv(testName, deployment, clusterReference, 'json', testCacheDirectory), {
testName,
outputFileName: 'deployment-config-ports-json-output.json',
metadata: {
command: 'deployment config ports',
deployment,
namespace: namespace.name,
clusterReference,
output: 'json',
},
});
expect(jsonResult.stdout).to.contain('"deployment"');
expect(jsonResult.stdout).to.contain(deployment);
expect(jsonResult.stdout).to.contain('"clusterReference"');
expect(jsonResult.stdout).to.contain(clusterReference);
expect(jsonResult.stdout).to.contain('"namespace"');
expect(jsonResult.stdout).to.contain('"services"');
expect(jsonResult.stdout).to.contain('"consensusNodeGrpc"');
expect(jsonResult.stdout).to.contain('"mirrorNodeRest"');
expect(jsonResult.stdout).to.contain('"jsonRpcRelay"');
expect(jsonResult.stdout).to.contain('"explorer"');
expect(jsonResult.stdout).to.contain('"blockNode"');
await assertPortsFile(jsonPortsFile, 'json', deployment, clusterReference);
const yamlResult = await runMainAndCaptureOutputToJson(soloDeploymentConfigPortsArgv(testName, deployment, clusterReference, 'yaml', testCacheDirectory), {
testName,
outputFileName: 'deployment-config-ports-yaml-output.json',
metadata: {
command: 'deployment config ports',
deployment,
namespace: namespace.name,
clusterReference,
output: 'yaml',
},
});
expect(yamlResult.stdout).to.contain('deployment:');
expect(yamlResult.stdout).to.contain(deployment);
expect(yamlResult.stdout).to.contain('clusterReference:');
expect(yamlResult.stdout).to.contain(clusterReference);
expect(yamlResult.stdout).to.contain('namespace:');
expect(yamlResult.stdout).to.contain('services:');
expect(yamlResult.stdout).to.contain('consensusNodeGrpc:');
expect(yamlResult.stdout).to.contain('mirrorNodeRest:');
expect(yamlResult.stdout).to.contain('jsonRpcRelay:');
expect(yamlResult.stdout).to.contain('explorer:');
expect(yamlResult.stdout).to.contain('blockNode:');
await assertPortsFile(yamlPortsFile, 'yaml', deployment, clusterReference);
testLogger.info(`${testName}: deployment config ports wide output saved to ${wideResult.outputFilePath}`);
testLogger.info(`${testName}: deployment config ports json output saved to ${jsonResult.outputFilePath}`);
testLogger.info(`${testName}: deployment config ports yaml output saved to ${yamlResult.outputFilePath}`);
testLogger.info(`${testName}: finished deployment config ports output verification`);
});
}
static async assertPortsFile(filePath, format, deployment, clusterReference) {
const raw = await fs.readFile(filePath, 'utf8');
const parsed = format === 'json' ? JSON.parse(raw) : yaml.parse(raw);
expect(parsed.deployment).to.equal(deployment);
expect(parsed.clusterReference).to.equal(clusterReference);
expect(parsed.services).to.be.an('object');
expect(parsed.services).to.have.property('consensusNodeGrpc').that.is.an('array');
expect(parsed.services).to.have.property('mirrorNodeRest').that.is.an('array');
expect(parsed.services).to.have.property('jsonRpcRelay').that.is.an('array');
expect(parsed.services).to.have.property('explorer').that.is.an('array');
expect(parsed.services).to.have.property('blockNode').that.is.an('array');
}
}
//# sourceMappingURL=deployment-test.js.map