@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
348 lines • 18.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import 'chai-as-promised';
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import 'dotenv/config';
import fs from 'node:fs';
import os from 'node:os';
import { Flags as flags } from '../src/commands/flags.js';
import { sleep } from '../src/core/helpers.js';
import { AccountBalanceQuery, AccountCreateTransaction, Hbar, HbarUnit, PrivateKey, } from '@hiero-ledger/sdk';
import * as constants from '../src/core/constants.js';
import { NODE_LOG_FAILURE_MSG, ROOT_CONTAINER, SOLO_LOGS_DIR } from '../src/core/constants.js';
import crypto from 'node:crypto';
import { Templates } from '../src/core/templates.js';
import { Duration } from '../src/core/time/duration.js';
import { container } from 'tsyringe-neo';
import { resetForTest } from './test-container.js';
import { NamespaceName } from '../src/types/namespace/namespace-name.js';
import { PodReference } from '../src/integration/kube/resources/pod/pod-reference.js';
import { ContainerReference } from '../src/integration/kube/resources/container/container-reference.js';
import { InjectTokens } from '../src/core/dependency-injection/inject-tokens.js';
import { Argv } from './helpers/argv-wrapper.js';
import { PathEx } from '../src/business/utils/path-ex.js';
import { HEDERA_PLATFORM_VERSION } from '../version.js';
import { main } from '../src/index.js';
import { BaseCommandTest } from './e2e/commands/tests/base-command-test.js';
import { KeysTest } from './e2e/commands/tests/keys-test.js';
import { ClusterReferenceTest } from './e2e/commands/tests/cluster-reference-test.js';
import { ConsensusNodeTest } from './e2e/commands/tests/consensus-node-test.js';
import { DeploymentTest } from './e2e/commands/tests/deployment-test.js';
import { InitTest } from './e2e/commands/tests/init-test.js';
import { SemanticVersion } from '../src/business/utils/semantic-version.js';
export const BASE_TEST_DIR = PathEx.join('test', 'data', 'tmp');
export function getTestCluster() {
const soloTestCluster = process.env.SOLO_TEST_CLUSTER ||
container.resolve(InjectTokens.K8Factory).default().clusters().readCurrent() ||
'solo-e2e';
return soloTestCluster.startsWith('kind-') ? soloTestCluster : `kind-${soloTestCluster}`;
}
export function getTestLogger() {
return container.resolve(InjectTokens.SoloLogger);
}
export function getTestCacheDirectory(testName) {
const d = testName ? PathEx.join(BASE_TEST_DIR, testName) : BASE_TEST_DIR;
if (!fs.existsSync(d)) {
fs.mkdirSync(d, { recursive: true });
}
return d;
}
export function getTemporaryDirectory() {
return fs.mkdtempSync(PathEx.join(os.tmpdir(), 'solo-'));
}
export function deployNetworkTest(argv) {
it('should succeed with consensus network deploy', async () => {
await main(ConsensusNodeTest.soloConsensusNetworkDeployArgv(argv.getArg(flags.deployment), argv.getArg(flags.nodeAliasesUnparsed), argv.getArg(flags.persistentVolumeClaims), argv.getArg(flags.cacheDir), argv.getArg(flags.app)));
}).timeout(Duration.ofMinutes(5).toMillis());
}
export function startNodesTest(testName, argv) {
it('should succeed with consensus node setup command', async () => {
// cache this, because `solo consensus node setup.finalize()` will reset it to false
const deployment = argv.getArg(flags.deployment);
const cacheDirectory = argv.getArg(flags.cacheDir);
const localBuildPath = argv.getArg(flags.localBuildPath);
const app = argv.getArg(flags.app);
const appConfig = argv.getArg(flags.appConfig);
await main(ConsensusNodeTest.soloConsensusNodeSetup(deployment, cacheDirectory, localBuildPath, app, appConfig));
}).timeout(Duration.ofMinutes(4).toMillis());
it('should succeed with consensus node start command', async () => {
const deployment = argv.getArg(flags.deployment);
const nodeAliases = argv.getArg(flags.nodeAliasesUnparsed);
await main(ConsensusNodeTest.soloNodeStart(deployment, nodeAliases, argv.getArg(flags.app)));
}).timeout(Duration.ofMinutes(30).toMillis());
it('deployment diagnostics logs command should work', async () => {
await main(DeploymentTest.soloDeploymentDiagnosticsLogsArgv(argv.getArg(flags.deployment)));
const soloLogPath = PathEx.joinWithRealPath(SOLO_LOGS_DIR, 'solo.log');
const soloLog = fs.readFileSync(soloLogPath, 'utf8');
expect(soloLog, 'Check solo.log for stale errors from previous runs').to.not.have.string(NODE_LOG_FAILURE_MSG);
}).timeout(Duration.ofMinutes(5).toMillis());
}
function getTestNamespace(argv) {
return NamespaceName.of(argv.getArg(flags.namespace) || 'bootstrap-ns');
}
let shouldReset = true;
/** Initialize common test variables */
export function bootstrapTestVariables(testName, argv, { k8FactoryArg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg, accountCmdArg, deploymentCmdArg }) {
const namespace = getTestNamespace(argv);
const deployment = argv.getArg(flags.deployment) || `${namespace.name}-deployment`;
const cacheDirectory = argv.getArg(flags.cacheDir) || getTestCacheDirectory(testName);
// Make sure the container is reset only once per CI run.
// When multiple test suites are loaded simultaneously, as is the case with `task test-e2e-standard`
// the container will be reset multiple times, which causes issues with the loading of LocalConfigRuntimeState.
// A better solution would be to run bootstrapping during the before hook of the test suite.
if (shouldReset) {
resetForTest(namespace.name, cacheDirectory);
shouldReset = false;
}
const configManager = container.resolve(InjectTokens.ConfigManager);
configManager.update(argv.build());
const downloader = container.resolve(InjectTokens.PackageDownloader);
const depManager = container.resolve(InjectTokens.DependencyManager);
const helm = container.resolve(InjectTokens.Helm);
const chartManager = container.resolve(InjectTokens.ChartManager);
const keyManager = container.resolve(InjectTokens.KeyManager);
const k8Factory = k8FactoryArg || container.resolve(InjectTokens.K8Factory);
const accountManager = container.resolve(InjectTokens.AccountManager);
const platformInstaller = container.resolve(InjectTokens.PlatformInstaller);
const profileManager = container.resolve(InjectTokens.ProfileManager);
const leaseManager = container.resolve(InjectTokens.LockManager);
const certificateManager = container.resolve(InjectTokens.CertificateManager);
const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState);
const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState);
const testLogger = getTestLogger();
const commandInvoker = container.resolve(InjectTokens.CommandInvoker);
const componentFactory = container.resolve(InjectTokens.ComponentFactory);
const options = {
logger: testLogger,
helm,
k8Factory,
chartManager,
configManager,
downloader,
platformInstaller,
depManager,
keyManager,
accountManager,
cacheDir: cacheDirectory,
profileManager,
leaseManager,
certificateManager,
localConfig,
remoteConfig,
commandInvoker,
componentFactory,
};
return {
namespace,
deployment,
opts: options,
manager: {
accountManager,
},
cmd: {
initCmd: initCmdArg || container.resolve(InjectTokens.InitCommand),
clusterCmd: clusterCmdArg || container.resolve(InjectTokens.ClusterCommand),
networkCmd: networkCmdArg || container.resolve(InjectTokens.NetworkCommand),
nodeCmd: nodeCmdArg || container.resolve(InjectTokens.NodeCommand),
accountCmd: accountCmdArg || container.resolve(InjectTokens.AccountCommand),
deploymentCmd: deploymentCmdArg || container.resolve(InjectTokens.DeploymentCommand),
},
};
}
/** Bootstrap network in a given namespace, then run the test call back providing the bootstrap response */
export function endToEndTestSuite(testName, argv, { k8FactoryArg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg, accountCmdArg, startNodes, containerOverrides, deployNetwork, }, testsCallBack = () => { }) {
const testLogger = getTestLogger();
const testNamespace = getTestNamespace(argv);
resetForTest(testNamespace.name, undefined, false, containerOverrides);
if (typeof startNodes !== 'boolean') {
startNodes = true;
}
if (typeof deployNetwork !== 'boolean') {
deployNetwork = true;
}
const bootstrapResp = bootstrapTestVariables(testName, argv, {
k8FactoryArg,
initCmdArg,
clusterCmdArg,
networkCmdArg,
nodeCmdArg,
accountCmdArg,
});
const { namespace, opts: { k8Factory, chartManager }, } = bootstrapResp;
describe(`E2E Test Suite for '${testName}'`, function () {
before(async () => {
const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState);
await localConfig.load();
});
this.bail(true); // stop on first failure, nothing else will matter if network doesn't come up correctly
describe(`Bootstrap network for test [release ${argv.getArg(flags.releaseTag)}]`, () => {
before(async () => {
testLogger.showUser(`------------------------- START: bootstrap (${testName}) ----------------------------`);
InitTest.init({});
});
// TODO: add rest of prerequisites for setup
after(async function () {
this.timeout(Duration.ofMinutes(5).toMillis());
// Use shared diagnostic log collection helper
const deployment = argv.getArg(flags.deployment);
await BaseCommandTest.collectDiagnosticLogs(testName, testLogger, deployment);
testLogger.showUser(`------------------------- END: bootstrap (${testName}) ----------------------------`);
});
it('should cleanup previous deployment', async () => {
if (await k8Factory.default().namespaces().has(namespace)) {
await k8Factory.default().namespaces().delete(namespace);
while (await k8Factory.default().namespaces().has(namespace)) {
testLogger.debug(`Namespace ${namespace} still exist. Waiting...`);
await sleep(Duration.ofSeconds(2));
}
}
if (!(await chartManager.isChartInstalled(constants.SOLO_SETUP_NAMESPACE, constants.MINIO_OPERATOR_RELEASE_NAME))) {
await main(ClusterReferenceTest.soloConfigSetupArgv(testName, constants.SOLO_SETUP_NAMESPACE.name));
}
}).timeout(Duration.ofMinutes(2).toMillis());
it("should success with 'cluster-ref config connect'", async () => {
const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState);
await localConfig.load();
const connectArguments = ClusterReferenceTest.soloConfigConnectArgv(testName, argv.getArg(flags.clusterRef), argv.getArg(flags.clusterRef));
await main(connectArguments);
});
it('should succeed with deployment config create', async () => {
const createArguments = DeploymentTest.soloDeploymentConfigCreateArgv(argv.getArg(flags.deployment), namespace);
await main(createArguments);
});
it("should succeed with 'deployment cluster attach'", async () => {
const attachArguments = DeploymentTest.soloDeploymentClusterAttachArgv(argv.getArg(flags.deployment), argv.getArg(flags.clusterRef), argv.getArg(flags.numberOfConsensusNodes));
await main(attachArguments);
});
it('generate key files', async () => {
const generateArguments = KeysTest.soloKeysConsensusGenerate(argv.getArg(flags.deployment), argv.getArg(flags.nodeAliasesUnparsed), argv.getArg(flags.cacheDir));
await main(generateArguments);
}).timeout(Duration.ofMinutes(2).toMillis());
if (deployNetwork) {
deployNetworkTest(argv);
}
if (deployNetwork && startNodes) {
startNodesTest(testName, argv);
}
});
describe(testName, () => {
testsCallBack(bootstrapResp);
});
});
}
export async function queryBalance(accountManager, namespace, remoteConfig, logger, skipNodeAlias) {
const argv = Argv.getDefaultArgv(namespace);
expect(accountManager._nodeClient).to.be.null;
await accountManager.refreshNodeClient(namespace, remoteConfig.getClusterRefs(), skipNodeAlias, argv.getArg(flags.deployment));
expect(accountManager._nodeClient).to.not.be.null;
const balance = await new AccountBalanceQuery()
.setAccountId(accountManager._nodeClient.getOperator().accountId)
.execute(accountManager._nodeClient);
expect(balance.hbars).to.not.be.null;
await sleep(Duration.ofSeconds(1));
}
export function balanceQueryShouldSucceed(accountManager, namespace, remoteConfig, logger, skipNodeAlias) {
it('Balance query should succeed', async () => {
await queryBalance(accountManager, namespace, remoteConfig, logger, skipNodeAlias);
}).timeout(Duration.ofMinutes(2).toMillis());
}
export async function createAccount(accountManager, namespace, remoteConfig, logger, skipNodeAlias, expectedAccountId) {
const argv = Argv.getDefaultArgv(namespace);
await accountManager.refreshNodeClient(namespace, remoteConfig.getClusterRefs(), skipNodeAlias, argv.getArg(flags.deployment));
expect(accountManager._nodeClient).not.to.be.null;
const privateKey = PrivateKey.generate();
const amount = 100;
const newAccount = await new AccountCreateTransaction()
.setKey(privateKey)
.setInitialBalance(Hbar.from(amount, HbarUnit.Hbar))
.execute(accountManager._nodeClient);
// Get the new account ID
const getReceipt = await newAccount.getReceipt(accountManager._nodeClient);
const accountInfo = {
accountId: getReceipt.accountId.toString(),
privateKey: privateKey.toString(),
publicKey: privateKey.publicKey.toString(),
balance: amount,
};
expect(accountInfo.accountId).not.to.be.null;
if (expectedAccountId) {
expect(accountInfo.accountId).to.equal(expectedAccountId.toString());
}
expect(accountInfo.balance).to.equal(amount);
}
export function accountCreationShouldSucceed(accountManager, namespace, remoteConfig, logger, skipNodeAlias, expectedAccountId) {
it('Account creation should succeed' +
(expectedAccountId ? ` with expected AccountId: ${expectedAccountId.toString()}` : ''), async () => {
await createAccount(accountManager, namespace, remoteConfig, logger, skipNodeAlias, expectedAccountId);
}).timeout(Duration.ofMinutes(2).toMillis());
}
export async function getNodeAliasesPrivateKeysHash(networkNodeServicesMap, k8Factory, destinationDirectory) {
const dataKeysDirectory = `${constants.HEDERA_HAPI_PATH}/data/keys`;
const tlsKeysDirectory = constants.HEDERA_HAPI_PATH;
const nodeKeyHashMap = new Map();
for (const networkNodeServices of networkNodeServicesMap.values()) {
const keyHashMap = new Map();
const nodeAlias = networkNodeServices.nodeAlias;
const uniqueNodeDestinationDirectory = PathEx.join(destinationDirectory, nodeAlias);
if (!fs.existsSync(uniqueNodeDestinationDirectory)) {
fs.mkdirSync(uniqueNodeDestinationDirectory, { recursive: true });
}
await addKeyHashToMap(networkNodeServices.namespace, k8Factory, nodeAlias, dataKeysDirectory, uniqueNodeDestinationDirectory, keyHashMap, Templates.renderGossipPemPrivateKeyFile(nodeAlias));
await addKeyHashToMap(networkNodeServices.namespace, k8Factory, nodeAlias, tlsKeysDirectory, uniqueNodeDestinationDirectory, keyHashMap, 'hedera.key');
nodeKeyHashMap.set(nodeAlias, keyHashMap);
}
return nodeKeyHashMap;
}
async function addKeyHashToMap(namespace, k8Factory, nodeAlias, keyDirectory, uniqueNodeDestinationDirectory, keyHashMap, privateKeyFileName) {
await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(PodReference.of(namespace, Templates.renderNetworkPodName(nodeAlias)), ROOT_CONTAINER))
.copyFrom(PathEx.join(keyDirectory, privateKeyFileName), uniqueNodeDestinationDirectory);
const keyBytes = fs.readFileSync(PathEx.joinWithRealPath(uniqueNodeDestinationDirectory, privateKeyFileName));
const keyString = keyBytes.toString();
keyHashMap.set(privateKeyFileName, crypto.createHash('sha256').update(keyString).digest('base64'));
}
export const testLocalConfigData = {
userIdentity: {
name: 'john',
host: 'doe',
},
soloVersion: '1.0.0',
deployments: {
deployment: {
clusters: ['cluster-1'],
namespace: 'solo-e2e',
realm: 0,
shard: 0,
},
'deployment-2': {
clusters: ['cluster-2'],
namespace: 'solo-2',
realm: 0,
shard: 0,
},
'deployment-3': {
clusters: ['cluster-3'],
namespace: 'solo-3',
realm: 0,
shard: 0,
},
},
clusterRefs: {
'cluster-1': 'context-1',
'cluster-2': 'context-2',
},
};
export { HEDERA_PLATFORM_VERSION as HEDERA_PLATFORM_VERSION_TAG } from '../version.js';
export function hederaPlatformSupportsNonZeroRealms() {
return new SemanticVersion(HEDERA_PLATFORM_VERSION).greaterThanOrEqual('0.61.4');
}
export function destroyEnabled() {
const destroyEnabledEnvironment = process.env.SOLO_E2E_DESTROY !== 'false';
if (!destroyEnabledEnvironment) {
console.log('Skipping destroy of test namespace as SOLO_E2E_DESTROY is set to false');
}
return destroyEnabledEnvironment;
}
//# sourceMappingURL=test-utility.js.map