UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

348 lines 18.3 kB
// 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