UNPKG

@hashgraph/solo

Version:

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

551 lines 36.4 kB
// SPDX-License-Identifier: Apache-2.0 import { BaseCommandTest } from './base-command-test.js'; import { main } from '../../../../src/index.js'; import { Flags as flags, Flags } from '../../../../src/commands/flags.js'; import fs from 'node:fs'; import { PathEx } from '../../../../src/business/utils/path-ex.js'; import { expect } from 'chai'; import { InjectTokens } from '../../../../src/core/dependency-injection/inject-tokens.js'; import { ContainerReference } from '../../../../src/integration/kube/resources/container/container-reference.js'; import { PodReference } from '../../../../src/integration/kube/resources/pod/pod-reference.js'; import { HEDERA_HAPI_PATH, HEDERA_USER_HOME_DIR, ROOT_CONTAINER } from '../../../../src/core/constants.js'; import { Duration } from '../../../../src/core/time/duration.js'; import { container } from 'tsyringe-neo'; import { Templates } from '../../../../src/core/templates.js'; import * as constants from '../../../../src/core/constants.js'; import { AccountCreateTransaction, AccountId, AccountInfoQuery, Hbar, HbarUnit, PrivateKey, } from '@hiero-ledger/sdk'; import { KeysTest } from './keys-test.js'; import { ConsensusCommandDefinition } from '../../../../src/commands/command-definitions/consensus-command-definition.js'; import { DeploymentCommandDefinition } from '../../../../src/commands/command-definitions/deployment-command-definition.js'; import { sleep } from '../../../../src/core/helpers.js'; import { NodeCommandTasks } from '../../../../src/commands/node/tasks.js'; import { it } from 'mocha'; import { createAccount, queryBalance, getTemporaryDirectory, getTestCacheDirectory, HEDERA_PLATFORM_VERSION_TAG, } from '../../../test-utility.js'; import { TEST_UPGRADE_FROM_VERSION, TEST_UPGRADE_TO_VERSION } from '../../../../version-test.js'; import { SemanticVersion } from '../../../../src/business/utils/semantic-version.js'; import { Zippy } from '../../../../src/core/zippy.js'; import { NodeStatusCodes } from '../../../../src/core/enumerations.js'; export class ConsensusNodeTest extends BaseCommandTest { static keys(options) { const { testName, testLogger, deployment, testCacheDirectory } = options; const { soloKeysConsensusGenerateArgv } = KeysTest; it(`${testName}: keys consensus generate`, async () => { testLogger.info(`${testName}: beginning keys consensus generate command`); await main(soloKeysConsensusGenerateArgv(testName, deployment)); const node1Key = fs.readFileSync(PathEx.joinWithRealPath(testCacheDirectory, 'keys', 's-private-node1.pem')); expect(node1Key).to.not.be.null; testLogger.info(`${testName}: finished keys consensus generate command`); }); } static soloConsensusNodeSetupArgv(testName, deployment, enableLocalBuildPathTesting, localBuildPath, localBuildReleaseTag) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_SETUP, optionFromFlag(Flags.deployment), deployment); if (enableLocalBuildPathTesting) { argv.push(optionFromFlag(Flags.localBuildPath), localBuildPath); } // Allow version-pinned setup in E2E tests even when local-build mode is off. if (localBuildReleaseTag) { argv.push(optionFromFlag(Flags.releaseTag), localBuildReleaseTag); } argvPushGlobalFlags(argv, testName, true); return argv; } static soloConsensusNodeSetup(deployment, cacheDirectory, localBuildPath, app, appConfig) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_SETUP, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.cacheDir), cacheDirectory); if (localBuildPath) { argv.push(optionFromFlag(Flags.localBuildPath), localBuildPath); } if (app) { argv.push(optionFromFlag(Flags.app), app); } if (appConfig) { argv.push(optionFromFlag(Flags.appConfig), appConfig); } return argv; } static soloConsensusNodeAddArgv(options, useFqdn = true) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName } = options; const firstClusterReference = [...options.clusterReferences.keys()][0]; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_ADD, optionFromFlag(Flags.deployment), options.deployment, optionFromFlag(flags.persistentVolumeClaims), optionFromFlag(Flags.clusterRef), firstClusterReference, optionFromFlag(flags.generateGossipKeys), optionFromFlag(flags.generateTlsKeys)); if (options.enableLocalBuildPathTesting) { argv.push(optionFromFlag(Flags.localBuildPath), options.localBuildPath, optionFromFlag(Flags.releaseTag), options.localBuildReleaseTag); } if (!useFqdn) { argv.push(optionFromFlag(Flags.endpointType), constants.ENDPOINT_TYPE_IP); } argvPushGlobalFlags(argv, testName, true, true); return argv; } static soloConsensusNetworkDeployArgv(deployment, nodeAliases, pvcsEnabled, cacheDirectory, app) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NETWORK_SUBCOMMAND_NAME, ConsensusCommandDefinition.NETWORK_DEPLOY, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.nodeAliasesUnparsed), nodeAliases, optionFromFlag(Flags.persistentVolumeClaims), pvcsEnabled ? 'true' : 'false', optionFromFlag(Flags.cacheDir), cacheDirectory); if (app) { argv.push(optionFromFlag(Flags.app), app); } return argv; } static soloConsensusNodeUpdateArgv(options, useFqdn = true) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment, enableLocalBuildPathTesting, localBuildPath, localBuildReleaseTag, consensusNodesCount, } = options; const nodeAlias = Templates.renderNodeAliasFromNumber(consensusNodesCount + 1); const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_UPDATE, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.nodeAlias), nodeAlias); if (enableLocalBuildPathTesting) { argv.push(optionFromFlag(Flags.localBuildPath), localBuildPath, optionFromFlag(Flags.releaseTag), localBuildReleaseTag); } if (!useFqdn) { argv.push(optionFromFlag(Flags.endpointType), constants.ENDPOINT_TYPE_IP); } argvPushGlobalFlags(argv, testName, true); return argv; } static soloConsensusNodeUpgradeArgv(options, zipFile, applicationPropertiesPath) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment } = options; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NETWORK_SUBCOMMAND_NAME, ConsensusCommandDefinition.NETWORK_UPGRADE, optionFromFlag(Flags.deployment), deployment, optionFromFlag(flags.quiet), optionFromFlag(flags.force), optionFromFlag(flags.upgradeVersion), TEST_UPGRADE_TO_VERSION); if (zipFile) { argv.push(optionFromFlag(flags.upgradeZipFile), zipFile); } if (applicationPropertiesPath) { argv.push(optionFromFlag(flags.applicationProperties), applicationPropertiesPath); } argvPushGlobalFlags(argv, testName, true, true); return argv; } static soloConsensusNodeDestroyArgv(options) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment, consensusNodesCount, enableLocalBuildPathTesting, localBuildPath, localBuildReleaseTag, } = options; const nodeAlias = Templates.renderNodeAliasFromNumber(consensusNodesCount + 1); const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_DESTROY, optionFromFlag(Flags.deployment), deployment, optionFromFlag(flags.nodeAlias), nodeAlias, optionFromFlag(flags.force), optionFromFlag(flags.quiet)); if (enableLocalBuildPathTesting) { argv.push(optionFromFlag(Flags.localBuildPath), localBuildPath, optionFromFlag(Flags.releaseTag), localBuildReleaseTag); } argvPushGlobalFlags(argv, testName, true); return argv; } static soloDeploymentDiagnosticsConnectionsArgv(options) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment } = options; const argv = newArgv(); argv.push(DeploymentCommandDefinition.COMMAND_NAME, DeploymentCommandDefinition.DIAGNOSTICS_SUBCOMMAND_NAME, DeploymentCommandDefinition.DIAGNOSTICS_CONNECTIONS, optionFromFlag(Flags.deployment), deployment, optionFromFlag(flags.quiet)); argvPushGlobalFlags(argv, testName, false); return argv; } static soloConsensusNodeRefreshArgv(options) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment, enableLocalBuildPathTesting, localBuildPath, localBuildReleaseTag } = options; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_REFRESH, optionFromFlag(Flags.deployment), deployment, optionFromFlag(flags.quiet), optionFromFlag(flags.nodeAliasesUnparsed)); if (enableLocalBuildPathTesting) { argv.push(optionFromFlag(Flags.localBuildPath), localBuildPath, optionFromFlag(Flags.releaseTag), localBuildReleaseTag); } argvPushGlobalFlags(argv, testName, true, true); return argv; } static soloConsensusNodeStopArgv(options, nodeAlias) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const { testName, deployment } = options; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_STOP, optionFromFlag(Flags.deployment), deployment, optionFromFlag(flags.quiet)); if (nodeAlias) { argv.push(optionFromFlag(Flags.nodeAliasesUnparsed), nodeAlias); } argvPushGlobalFlags(argv, testName, false); return argv; } static setup(options, version) { const { testName, deployment, namespace, contexts, enableLocalBuildPathTesting, localBuildPath, localBuildReleaseTag, consensusNodesCount, } = options; const { soloConsensusNodeSetupArgv } = ConsensusNodeTest; it(`${testName}: consensus node setup`, async () => { await main(soloConsensusNodeSetupArgv(testName, deployment, enableLocalBuildPathTesting, localBuildPath, version ?? localBuildReleaseTag)); const k8Factory = container.resolve(InjectTokens.K8Factory); const clusterCount = contexts.length; const base = Math.floor(consensusNodesCount / clusterCount); const remainder = consensusNodesCount % clusterCount; for (const [index, context_] of contexts.entries()) { const expectedNodeCount = index < remainder ? base + 1 : base; const k8 = k8Factory.getK8(context_); const pods = await k8.pods().list(namespace, ['solo.hedera.com/type=network-node']); expect(pods.length, `expect this cluster (${context_}) to have ${expectedNodeCount} network node(s) in namespace ${namespace}`).to.equal(expectedNodeCount); const rootContainer = ContainerReference.of(PodReference.of(namespace, pods[0].podReference.name), ROOT_CONTAINER); if (!enableLocalBuildPathTesting) { expect(await k8.containers().readByRef(rootContainer).hasFile(`${HEDERA_USER_HOME_DIR}/extract-platform.sh`), 'expect extract-platform.sh to be present on the pods').to.be.true; } expect(await k8.containers().readByRef(rootContainer).hasFile(`${HEDERA_HAPI_PATH}/data/apps/HederaNode.jar`)) .to.be.true; expect(await k8 .containers() .readByRef(rootContainer) .hasFile(`${HEDERA_HAPI_PATH}/data/config/genesis-network.json`)).to.be.true; expect(await k8 .containers() .readByRef(rootContainer) .execContainer(['bash', '-c', `ls -al ${HEDERA_HAPI_PATH} | grep output`])).to.includes('hedera'); } }).timeout(Duration.ofMinutes(2).toMillis()); } static alphaClusterGrpcWebAddress = 'localhost'; static betaClusterGrpcWebAddress = 'remote.cluster.address'; static baseGrpcWebPort = 4444; // Legacy aliases kept for external references static firstNodeCustomGrpcWebEndpointAddress = ConsensusNodeTest.alphaClusterGrpcWebAddress; static firstNodeCustomGrpcWebEndpointPort = ConsensusNodeTest.baseGrpcWebPort; static secondNodeCustomGrpcWebEndpointAddress = ConsensusNodeTest.betaClusterGrpcWebAddress; static secondNodeCustomGrpcWebEndpointPort = ConsensusNodeTest.baseGrpcWebPort + 1; // Returns the 0-based cluster index (0=alpha, 1=beta) for a 1-based node number // given N total nodes spread across 2 clusters (ceil(N/2) in alpha, floor(N/2) in beta). static clusterIndexForNodeNumber(nodeNumber, totalNodes) { return nodeNumber <= Math.ceil(totalNodes / 2) ? 0 : 1; } // Generates the --grpc-web-endpoints value for all N nodes. // Alpha nodes get alphaClusterGrpcWebAddress and beta nodes get betaClusterGrpcWebAddress. // Each node gets a unique port starting at baseGrpcWebPort. static grpcWebEndpointsForNodes(consensusNodesCount) { const alphaCount = Math.ceil(consensusNodesCount / 2); const endpoints = []; for (let index = 1; index <= consensusNodesCount; index++) { const address = index <= alphaCount ? ConsensusNodeTest.alphaClusterGrpcWebAddress : ConsensusNodeTest.betaClusterGrpcWebAddress; const port = ConsensusNodeTest.baseGrpcWebPort + index - 1; endpoints.push(`node${index}=${address}:${port}`); } return endpoints.join(','); } static soloNodeStartArgv(testName, deployment, consensusNodesCount, nodeAliases, setCustomGrpcWebAddress) { const { newArgv, argvPushGlobalFlags, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_START, optionFromFlag(Flags.deployment), deployment); if (nodeAliases) { argv.push(optionFromFlag(Flags.nodeAliasesUnparsed), nodeAliases); } argvPushGlobalFlags(argv, testName); if (setCustomGrpcWebAddress) { argv.push(optionFromFlag(flags.grpcWebEndpoints), ConsensusNodeTest.grpcWebEndpointsForNodes(consensusNodesCount)); } return argv; } static soloNodeStart(deployment, nodeAliases, app) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_START, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.nodeAliasesUnparsed), nodeAliases); if (app) { argv.push(optionFromFlag(Flags.app), app); } return argv; } static async verifyAccountCreateWasSuccessful(namespace, clusterReferences, deployment) { const accountManager = container.resolve(InjectTokens.AccountManager); try { await accountManager.refreshNodeClient(namespace, clusterReferences, undefined, deployment); expect(accountManager._nodeClient).not.to.be.null; const privateKey = PrivateKey.generate(); const amount = 777; const newAccount = await new AccountCreateTransaction() .setKeyWithoutAlias(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; expect(accountInfo.balance).to.equal(amount); return accountInfo.accountId; } finally { await accountManager.close(); expect( // @ts-expect-error - TS2341: Property _portForwards is private and only accessible within class AccountManager accountManager._portForwards, 'port forwards should be empty after accountManager.close()').to.have.lengthOf(0); } } static start(options, setCustomGrpcWebAddress = false) { const { testName, deployment, namespace, contexts, createdAccountIds, clusterReferences, consensusNodesCount } = options; const { soloNodeStartArgv, verifyAccountCreateWasSuccessful } = ConsensusNodeTest; it(`${testName}: consensus node start`, async () => { await main(soloNodeStartArgv(testName, deployment, consensusNodesCount, undefined, setCustomGrpcWebAddress)); const k8Factory = container.resolve(InjectTokens.K8Factory); const clusterCount = contexts.length; const base = Math.floor(consensusNodesCount / clusterCount); const remainder = consensusNodesCount % clusterCount; for (const [index, context_] of contexts.entries()) { const k8 = k8Factory.getK8(context_); const expectedNodeCount = index < remainder ? base + 1 : base; const networkNodePods = await k8.pods().list(namespace, ['solo.hedera.com/type=network-node']); expect(networkNodePods.length, `expected ${expectedNodeCount} network-node pod(s) in namespace ${namespace} for context ${context_}`).to.equal(expectedNodeCount); const haProxyPod = await k8 .pods() .waitForReadyStatus(namespace, [ `app=haproxy-${Templates.extractNodeAliasFromPodName(networkNodePods[0].podReference.name)}`, 'solo.hedera.com/type=haproxy', ], constants.NETWORK_PROXY_MAX_ATTEMPTS, constants.NETWORK_PROXY_DELAY); expect(haProxyPod).to.have.lengthOf(1); createdAccountIds.push(await verifyAccountCreateWasSuccessful(namespace, clusterReferences, deployment), await verifyAccountCreateWasSuccessful(namespace, clusterReferences, deployment)); } // create one more account to make sure that the last one gets pushed to mirror node await verifyAccountCreateWasSuccessful(namespace, clusterReferences, deployment); }).timeout(Duration.ofMinutes(10).toMillis()); } static add(options, useFqdn = true) { const { testName } = options; const { soloConsensusNodeAddArgv } = ConsensusNodeTest; it(`${testName}: consensus node add`, async () => { await main(soloConsensusNodeAddArgv(options, useFqdn)); }).timeout(Duration.ofMinutes(10).toMillis()); } static update(options, useFqdn = true) { const { testName } = options; const { soloConsensusNodeUpdateArgv } = ConsensusNodeTest; it(`${testName}: consensus node update`, async () => { await main(soloConsensusNodeUpdateArgv(options, useFqdn)); }).timeout(Duration.ofMinutes(10).toMillis()); } static upgrade(options) { const { testName, namespace, contexts, testLogger: logger, shard, realm } = options; const { soloConsensusNodeUpgradeArgv } = ConsensusNodeTest; it(`${testName}: consensus node upgrade`, async () => { const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState); await localConfig.load(); const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState); await remoteConfig.load(namespace, contexts[0]); await main(soloConsensusNodeUpgradeArgv(options)); const k8Factory = container.resolve(InjectTokens.K8Factory); { // copy the version.txt file from the pod data/upgrade/current directory const temporaryDirectory = getTemporaryDirectory(); const pods = await k8Factory.default().pods().list(namespace, ['solo.hedera.com/type=network-node']); const containerReference = k8Factory .default() .containers() .readByRef(ContainerReference.of(PodReference.of(namespace, pods[0].podReference.name), ROOT_CONTAINER)); await containerReference.copyFrom(`${HEDERA_HAPI_PATH}/VERSION`, temporaryDirectory); const versionFile = fs.readFileSync(`${temporaryDirectory}/VERSION`, 'utf8'); const versionLine = versionFile.split('\n')[0].trim(); expect(versionLine).to.equal(`VERSION=${TEST_UPGRADE_TO_VERSION.replace('v', '')}`); } { const zipFile = 'upgrade.zip'; const cacheDirectory = getTestCacheDirectory(testName); // Remove the staging directory to make sure the command works if it doesn't exist const stagingDirectory = Templates.renderStagingDir(cacheDirectory, HEDERA_PLATFORM_VERSION_TAG); fs.rmSync(stagingDirectory, { recursive: true, force: true }); // Download application.properties from the pod const temporaryDirectory = getTemporaryDirectory(); const pods = await k8Factory.default().pods().list(namespace, ['solo.hedera.com/type=network-node']); const container = k8Factory .default() .containers() .readByRef(ContainerReference.of(PodReference.of(namespace, pods[0].podReference.name), ROOT_CONTAINER)); await container.copyFrom(`${HEDERA_HAPI_PATH}/data/config/${constants.APPLICATION_PROPERTIES}`, temporaryDirectory); const applicationPropertiesPath = PathEx.join(temporaryDirectory, constants.APPLICATION_PROPERTIES); const applicationProperties = fs.readFileSync(applicationPropertiesPath, 'utf8'); const updatedContent = applicationProperties.replaceAll('contracts.chainId=298', 'contracts.chainId=299'); fs.writeFileSync(applicationPropertiesPath, updatedContent); // create upgrade.zip file from tmp directory using zippy.ts const zipper = new Zippy(logger); await zipper.zip(temporaryDirectory, zipFile); await main(soloConsensusNodeUpgradeArgv(options, zipFile)); const modifiedApplicationProperties = fs.readFileSync(applicationPropertiesPath, 'utf8'); await container.copyFrom(`${HEDERA_HAPI_PATH}/data/upgrade/current/${constants.APPLICATION_PROPERTIES}`, temporaryDirectory); const upgradedApplicationProperties = fs.readFileSync(applicationPropertiesPath, 'utf8'); expect(modifiedApplicationProperties).to.equal(upgradedApplicationProperties); } { const pods = await k8Factory.default().pods().list(namespace, ['solo.hedera.com/type=network-node']); const response = await container .resolve(InjectTokens.NetworkNodes) .getNetworkNodePodStatus(PodReference.of(namespace, pods[0].podReference.name)); expect(response).to.not.be.undefined; const statusLine = response .split('\n') .find((line) => line.startsWith('platform_PlatformStatus')); expect(statusLine).to.not.be.undefined; const statusNumber = Number.parseInt(statusLine.split(' ').pop()); expect(statusNumber).to.equal(NodeStatusCodes.ACTIVE, 'All network nodes are running'); } const accountManager = container.resolve(InjectTokens.AccountManager); await queryBalance(accountManager, namespace, remoteConfig, logger); await createAccount(accountManager, namespace, remoteConfig, logger); const accountInfo1 = await new AccountInfoQuery() .setAccountId(new AccountId(shard, realm, 1001)) .execute(accountManager._nodeClient); expect(accountInfo1).not.to.be.null; const accountInfo2 = await new AccountInfoQuery() .setAccountId(new AccountId(shard, realm, 1002)) .execute(accountManager._nodeClient); expect(accountInfo2).not.to.be.null; }).timeout(Duration.ofMinutes(10).toMillis()); } static upgradeConfigs(options) { const { testName, namespace, contexts } = options; const { soloConsensusNodeUpgradeArgv } = ConsensusNodeTest; const temporaryDirectory = getTemporaryDirectory(); it(`${testName}: consensus node upgrade [upgrade configs]`, async () => { const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState); await localConfig.load(); const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState); await remoteConfig.load(namespace, contexts[0]); const k8Factory = container.resolve(InjectTokens.K8Factory); const pods = await k8Factory.default().pods().list(namespace, ['solo.hedera.com/type=network-node']); const containerReference = k8Factory .default() .containers() .readByRef(ContainerReference.of(PodReference.of(namespace, pods[0].podReference.name), ROOT_CONTAINER)); const applicationPropertiesFilePath = `${constants.HEDERA_HAPI_PATH}/data/config/${constants.APPLICATION_PROPERTIES}`; // prepare temporary application.properties to utilize for argv await containerReference.copyFrom(applicationPropertiesFilePath, temporaryDirectory); const testApplicationPropertiesPath = PathEx.join(temporaryDirectory, constants.APPLICATION_PROPERTIES); const applicationProperties = fs.readFileSync(testApplicationPropertiesPath, 'utf8'); const updatedContent = applicationProperties.replaceAll('contracts.chainId=298', 'contracts.chainId=299'); fs.writeFileSync(testApplicationPropertiesPath, updatedContent); // Set the consensus node version in remote config to TEST_UPGRADE_FROM_VERSION // so the downgrade guard allows upgrading to TEST_UPGRADE_TO_VERSION (which must be newer). remoteConfig.configuration.versions.consensusNode = new SemanticVersion(TEST_UPGRADE_FROM_VERSION); await remoteConfig.persist(); await main(soloConsensusNodeUpgradeArgv(options, undefined, testApplicationPropertiesPath)); await containerReference.copyFrom(applicationPropertiesFilePath, temporaryDirectory); const upgradedApplicationProperties = fs.readFileSync(testApplicationPropertiesPath, 'utf8'); expect(updatedContent).to.equal(upgradedApplicationProperties); }).timeout(Duration.ofMinutes(10).toMillis()); } static destroy(options) { const { testName } = options; const { soloConsensusNodeDestroyArgv } = ConsensusNodeTest; it(`${testName}: consensus node destroy`, async () => { await main(soloConsensusNodeDestroyArgv(options)); }).timeout(Duration.ofMinutes(10).toMillis()); } static async refresh(options) { const { soloConsensusNodeRefreshArgv } = ConsensusNodeTest; await main(soloConsensusNodeRefreshArgv(options)); await sleep(Duration.ofSeconds(15)); // sleep to wait for node to finish starting } static async verifyPodShouldBeRunning(namespace, nodeAlias, context) { const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState); await localConfig.load(); const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState); await remoteConfig.load(namespace, context); const podName = await container .resolve(NodeCommandTasks) // @ts-expect-error - TS2341: to access private property .checkNetworkNodePod(namespace, nodeAlias) .then((pod) => pod.name.toString()); expect(podName).to.equal(`network-${nodeAlias}-0`); } static async verifyPodShouldNotBeActive(namespace, nodeAlias, context) { const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState); await localConfig.load(); const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState); await remoteConfig.load(namespace, context); await expect(container .resolve(NodeCommandTasks) .checkNetworkNodeActiveness(namespace, nodeAlias, { title: '' }, '', undefined, 15)).to.be.rejected; } static PemKill(options) { const { namespace, testName, testLogger, consensusNodesCount } = options; const { checkNetwork, soloConsensusNodeStopArgv, refresh, verifyPodShouldBeRunning } = ConsensusNodeTest; const nodeAlias = 'node2'; it(`${testName}: perform PEM kill`, async () => { // Determine which cluster node2 belongs to based on the distribution formula. const clusterIndex = ConsensusNodeTest.clusterIndexForNodeNumber(2, consensusNodesCount); const context = [...options.clusterReferences.values()][clusterIndex]; const pods = await container .resolve(InjectTokens.K8Factory) .getK8(context) .pods() .list(namespace, ['solo.hedera.com/type=network-node', `solo.hedera.com/node-name=${nodeAlias}`]); await container .resolve(InjectTokens.K8Factory) .getK8(context) .pods() .readByReference(pods[0].podReference) .killPod(); testLogger.showUser('Sleeping for 20 seconds'); await sleep(Duration.ofSeconds(20)); // give time for node to stop and update its logs await verifyPodShouldBeRunning(namespace, nodeAlias, context); // With autostart enabled (0.44.0+), killing a pod causes the platform to // auto-restart via the network-node-autostart oneshot when the pod comes back. // Stop it explicitly so we can do a controlled Solo-managed refresh below. await main(soloConsensusNodeStopArgv(options, nodeAlias)); await sleep(Duration.ofSeconds(20)); // give time for node to stop and update its logs await refresh(options); await checkNetwork(testName, namespace, testLogger); }).timeout(Duration.ofMinutes(10).toMillis()); } static PemStop(options) { const { namespace, testName, testLogger, consensusNodesCount, deployment, contexts } = options; const { checkNetwork, refresh, verifyPodShouldNotBeActive, verifyPodShouldBeRunning, soloNodeStartArgv, soloConsensusNodeStopArgv, } = ConsensusNodeTest; const nodeAlias = 'node2'; it(`${testName}: perform PEM stop`, async () => { await main(soloConsensusNodeStopArgv(options, nodeAlias)); await sleep(Duration.ofSeconds(30)); // give time for node to stop and update its logs // Only check the stopped node's status on its own cluster context. // With N >= 3 nodes the remaining nodes retain quorum and stay ACTIVE. const clusterIndex = ConsensusNodeTest.clusterIndexForNodeNumber(2, consensusNodesCount); const node2Context = contexts ? contexts[clusterIndex] : undefined; await verifyPodShouldBeRunning(namespace, nodeAlias, node2Context); await verifyPodShouldNotBeActive(namespace, nodeAlias, node2Context); await refresh(options); await checkNetwork(testName, namespace, testLogger); await main(soloNodeStartArgv(testName, deployment, consensusNodesCount, undefined, false)); testLogger.showUser('Sleeping for 20 seconds'); await sleep(Duration.ofSeconds(20)); }).timeout(Duration.ofMinutes(10).toMillis()); } static async checkNetwork(testName, namespace, logger) { const accountManager = container.resolve(InjectTokens.AccountManager); const remoteConfig = container.resolve(InjectTokens.RemoteConfigRuntimeState); await remoteConfig.load(namespace); await queryBalance(accountManager, namespace, remoteConfig, logger); await createAccount(accountManager, namespace, remoteConfig, logger); } // TODO: I think this should be used, but it isn't being called static connections(options) { const { testName } = options; const { soloDeploymentDiagnosticsConnectionsArgv } = ConsensusNodeTest; it(`${testName}: deployment diagnostics connections`, async () => { await main(soloDeploymentDiagnosticsConnectionsArgv(options)); }).timeout(Duration.ofMinutes(10).toMillis()); } static soloConsensusNetworkDestroyArgv(deployment) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NETWORK_SUBCOMMAND_NAME, ConsensusCommandDefinition.NETWORK_DESTROY, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.deletePvcs), optionFromFlag(Flags.deleteSecrets), optionFromFlag(Flags.force)); return argv; } static soloConsensusNetworkFreezeArgv(deployment) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NETWORK_SUBCOMMAND_NAME, ConsensusCommandDefinition.NETWORK_FREEZE, optionFromFlag(Flags.deployment), deployment); return argv; } static soloConsensusStateDownloadArgv(deployment, nodeAliasesUnparsed) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.STATE_SUBCOMMAND_NAME, ConsensusCommandDefinition.STATE_DOWNLOAD, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.nodeAliasesUnparsed), nodeAliasesUnparsed); return argv; } static soloConsensusNodeRestartArgv(deployment, nodeAliasesUnparsed) { const { newArgv, optionFromFlag } = ConsensusNodeTest; const argv = newArgv(); argv.push(ConsensusCommandDefinition.COMMAND_NAME, ConsensusCommandDefinition.NODE_SUBCOMMAND_NAME, ConsensusCommandDefinition.NODE_RESTART, optionFromFlag(Flags.deployment), deployment); if (nodeAliasesUnparsed) { argv.push(optionFromFlag(Flags.nodeAliasesUnparsed), nodeAliasesUnparsed); } return argv; } } //# sourceMappingURL=consensus-node-test.js.map