UNPKG

@hashgraph/solo

Version:

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

334 lines 16.5 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import 'chai-as-promised'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { Flags as flags } from '../src/commands/flags.js'; import { ClusterCommand } from '../src/commands/cluster/index.js'; import { InitCommand } from '../src/commands/init.js'; import { NetworkCommand } from '../src/commands/network.js'; import { NodeCommand } from '../src/commands/node/index.js'; import { sleep } from '../src/core/helpers.js'; import { AccountBalanceQuery, AccountCreateTransaction, Hbar, HbarUnit, PrivateKey } from '@hashgraph/sdk'; import { NODE_LOG_FAILURE_MSG, ROOT_CONTAINER, SOLO_LOGS_DIR, SOLO_TEST_CLUSTER } from '../src/core/constants.js'; import crypto from 'crypto'; import { AccountCommand } from '../src/commands/account.js'; import * as NodeCommandConfigs from '../src/commands/node/configs.js'; import * as constants from '../src/core/constants.js'; import { Templates } from '../src/core/templates.js'; import { HEDERA_PLATFORM_VERSION } from '../version.js'; import { Duration } from '../src/core/time/duration.js'; import { container } from 'tsyringe-neo'; import { resetForTest } from './test_container.js'; import { NamespaceName } from '../src/core/kube/resources/namespace/namespace_name.js'; import { PodRef } from '../src/core/kube/resources/pod/pod_ref.js'; import { ContainerRef } from '../src/core/kube/resources/container/container_ref.js'; import { InjectTokens } from '../src/core/dependency_injection/inject_tokens.js'; import { DeploymentCommand } from '../src/commands/deployment.js'; import { K8Client } from '../src/core/kube/k8_client/k8_client.js'; export const TEST_CLUSTER = SOLO_TEST_CLUSTER; export const HEDERA_PLATFORM_VERSION_TAG = HEDERA_PLATFORM_VERSION; export const BASE_TEST_DIR = path.join('test', 'data', 'tmp'); export let testLogger = container.resolve(InjectTokens.SoloLogger); export function getTestCacheDir(testName) { const d = testName ? path.join(BASE_TEST_DIR, testName) : BASE_TEST_DIR; if (!fs.existsSync(d)) { fs.mkdirSync(d, { recursive: true }); } return d; } export function getTmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'solo-')); } /** Get argv with defaults */ export function getDefaultArgv(namespace) { const argv = {}; for (const f of flags.allFlags) { argv[f.name] = f.definition.defaultValue; } const currentDeployment = argv[flags.deployment.name] || `${namespace?.name || argv[flags.namespace.name]}-deployment`; const cacheDir = getTestCacheDir(); argv.cacheDir = cacheDir; argv[flags.cacheDir.name] = cacheDir; argv.deployment = currentDeployment; argv[flags.deployment.name] = currentDeployment; argv[flags.clusterRef.name] = 'cluster-1'; argv[flags.deploymentClusters.name] = ['cluster-1']; argv[flags.context.name] = new K8Client(undefined).contexts().readCurrent(); return argv; } /** Initialize common test variables */ export function bootstrapTestVariables(testName, argv, k8FactoryArg = null, initCmdArg = null, clusterCmdArg = null, networkCmdArg = null, nodeCmdArg = null, accountCmdArg = null, deploymentCmdArg = null) { const namespace = NamespaceName.of(argv[flags.namespace.name] || 'bootstrap-ns'); const deployment = argv[flags.deployment.name] || `${namespace.name}-deployment`; const cacheDir = argv[flags.cacheDir.name] || getTestCacheDir(testName); resetForTest(namespace.name, cacheDir); const configManager = container.resolve(InjectTokens.ConfigManager); configManager.update(argv); 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.LeaseManager); const certificateManager = container.resolve(InjectTokens.CertificateManager); const localConfig = container.resolve(InjectTokens.LocalConfig); const remoteConfigManager = container.resolve(InjectTokens.RemoteConfigManager); testLogger = container.resolve(InjectTokens.SoloLogger); const opts = { logger: testLogger, helm, k8Factory, chartManager, configManager, downloader, platformInstaller, depManager, keyManager, accountManager, cacheDir, profileManager, leaseManager, certificateManager, localConfig, remoteConfigManager, }; const initCmd = initCmdArg || new InitCommand(opts); const clusterCmd = clusterCmdArg || new ClusterCommand(opts); const networkCmd = networkCmdArg || new NetworkCommand(opts); const nodeCmd = nodeCmdArg || new NodeCommand(opts); const accountCmd = accountCmdArg || new AccountCommand(opts, constants.SHORTER_SYSTEM_ACCOUNTS); const deploymentCmd = deploymentCmdArg || new DeploymentCommand(opts); return { namespace, deployment, opts, manager: { accountManager, }, cmd: { initCmd, clusterCmd, networkCmd, nodeCmd, accountCmd, deploymentCmd, }, }; } /** Bootstrap network in a given namespace, then run the test call back providing the bootstrap response */ export function e2eTestSuite(testName, argv, k8FactoryArg = null, initCmdArg = null, clusterCmdArg = null, networkCmdArg = null, nodeCmdArg = null, accountCmdArg = null, startNodes = true, testsCallBack = () => { }) { const bootstrapResp = bootstrapTestVariables(testName, argv, k8FactoryArg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg, accountCmdArg); const namespace = bootstrapResp.namespace; const initCmd = bootstrapResp.cmd.initCmd; const k8Factory = bootstrapResp.opts.k8Factory; const clusterCmd = bootstrapResp.cmd.clusterCmd; const networkCmd = bootstrapResp.cmd.networkCmd; const nodeCmd = bootstrapResp.cmd.nodeCmd; const chartManager = bootstrapResp.opts.chartManager; const deploymentCmd = bootstrapResp.cmd.deploymentCmd; describe(`E2E Test Suite for '${testName}'`, function () { 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[flags.releaseTag.name]}]`, () => { before(() => { bootstrapResp.opts.logger.showUser(`------------------------- START: bootstrap (${testName}) ----------------------------`); }); after(async function () { this.timeout(Duration.ofMinutes(5).toMillis()); await container.resolve(InjectTokens.NetworkNodes).getLogs(namespace); bootstrapResp.opts.logger.showUser(`------------------------- END: bootstrap (${testName}) ----------------------------`); }); it('should cleanup previous deployment', async () => { await initCmd.init(argv); 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.SOLO_CLUSTER_SETUP_CHART))) { await clusterCmd.handlers.setup(argv); } }).timeout(Duration.ofMinutes(2).toMillis()); it('should succeed with deployment create', async () => { expect(await deploymentCmd.create(argv)).to.be.true; }); it('generate key files', async () => { expect(await nodeCmd.handlers.keys(argv)).to.be.true; expect(nodeCmd.getUnusedConfigs(NodeCommandConfigs.KEYS_CONFIGS_NAME)).to.deep.equal([ flags.devMode.constName, flags.quiet.constName, 'consensusNodes', 'contexts', ]); }).timeout(Duration.ofMinutes(2).toMillis()); it('should succeed with network deploy', async () => { await networkCmd.deploy(argv); expect(networkCmd.getUnusedConfigs(NetworkCommand.DEPLOY_CONFIGS_NAME)).to.deep.equal([ flags.apiPermissionProperties.constName, flags.applicationEnv.constName, flags.applicationProperties.constName, flags.bootstrapProperties.constName, flags.chainId.constName, flags.log4j2Xml.constName, flags.deployment.constName, flags.profileFile.constName, flags.profileName.constName, flags.quiet.constName, flags.settingTxt.constName, flags.grpcTlsKeyPath.constName, flags.grpcWebTlsKeyPath.constName, flags.gcsAccessKey.constName, flags.gcsSecrets.constName, flags.gcsEndpoint.constName, flags.awsAccessKey.constName, flags.awsSecrets.constName, flags.awsEndpoint.constName, ]); }).timeout(Duration.ofMinutes(5).toMillis()); if (startNodes) { it('should succeed with node setup command', async () => { // cache this, because `solo node setup.finalize()` will reset it to false try { expect(await nodeCmd.handlers.setup(argv)).to.be.true; expect(nodeCmd.getUnusedConfigs(NodeCommandConfigs.SETUP_CONFIGS_NAME)).to.deep.equal([ flags.quiet.constName, flags.devMode.constName, flags.adminPublicKeys.constName, 'contexts', ]); } catch (e) { nodeCmd.logger.showUserError(e); expect.fail(); } }).timeout(Duration.ofMinutes(4).toMillis()); it('should succeed with node start command', async () => { try { expect(await nodeCmd.handlers.start(argv)).to.be.true; } catch (e) { nodeCmd.logger.showUserError(e); expect.fail(); } }).timeout(Duration.ofMinutes(30).toMillis()); it('node log command should work', async () => { expect(await nodeCmd.handlers.logs(argv)).to.be.true; const soloLogPath = path.join(SOLO_LOGS_DIR, 'solo.log'); const soloLog = fs.readFileSync(soloLogPath, 'utf8'); expect(soloLog).to.not.have.string(NODE_LOG_FAILURE_MSG); }).timeout(Duration.ofMinutes(5).toMillis()); } }); describe(testName, () => { testsCallBack(bootstrapResp); }); }); } export function balanceQueryShouldSucceed(accountManager, cmd, namespace, skipNodeAlias) { it('Balance query should succeed', async () => { try { const argv = getDefaultArgv(namespace); expect(accountManager._nodeClient).to.be.null; await accountManager.refreshNodeClient(namespace, skipNodeAlias, cmd.getClusterRefs(), argv[flags.deployment.name]); expect(accountManager._nodeClient).not.to.be.null; const balance = await new AccountBalanceQuery() .setAccountId(accountManager._nodeClient.getOperator().accountId) .execute(accountManager._nodeClient); expect(balance.hbars).not.be.null; } catch (e) { cmd.logger.showUserError(e); expect.fail(); } await sleep(Duration.ofSeconds(1)); }).timeout(Duration.ofMinutes(2).toMillis()); } export function accountCreationShouldSucceed(accountManager, nodeCmd, namespace, skipNodeAlias) { it('Account creation should succeed', async () => { try { const argv = getDefaultArgv(namespace); await accountManager.refreshNodeClient(namespace, skipNodeAlias, nodeCmd.getClusterRefs(), argv[flags.deployment.name]); 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; expect(accountInfo.balance).to.equal(amount); } catch (e) { nodeCmd.logger.showUserError(e); expect.fail(); } }).timeout(Duration.ofMinutes(2).toMillis()); } export async function getNodeAliasesPrivateKeysHash(networkNodeServicesMap, k8Factory, destDir) { const dataKeysDir = path.join(constants.HEDERA_HAPI_PATH, 'data', 'keys'); const tlsKeysDir = constants.HEDERA_HAPI_PATH; const nodeKeyHashMap = new Map(); for (const networkNodeServices of networkNodeServicesMap.values()) { const keyHashMap = new Map(); const nodeAlias = networkNodeServices.nodeAlias; const uniqueNodeDestDir = path.join(destDir, nodeAlias); if (!fs.existsSync(uniqueNodeDestDir)) { fs.mkdirSync(uniqueNodeDestDir, { recursive: true }); } await addKeyHashToMap(networkNodeServices.namespace, k8Factory, nodeAlias, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPemPrivateKeyFile(nodeAlias)); await addKeyHashToMap(networkNodeServices.namespace, k8Factory, nodeAlias, tlsKeysDir, uniqueNodeDestDir, keyHashMap, 'hedera.key'); nodeKeyHashMap.set(nodeAlias, keyHashMap); } return nodeKeyHashMap; } async function addKeyHashToMap(namespace, k8Factory, nodeAlias, keyDir, uniqueNodeDestDir, keyHashMap, privateKeyFileName) { await k8Factory .default() .containers() .readByRef(ContainerRef.of(PodRef.of(namespace, Templates.renderNetworkPodName(nodeAlias)), ROOT_CONTAINER)) .copyFrom(path.join(keyDir, privateKeyFileName), uniqueNodeDestDir); const keyBytes = fs.readFileSync(path.join(uniqueNodeDestDir, privateKeyFileName)); const keyString = keyBytes.toString(); keyHashMap.set(privateKeyFileName, crypto.createHash('sha256').update(keyString).digest('base64')); } export const testLocalConfigData = { userEmailAddress: 'john.doe@example.com', soloVersion: '1.0.0', deployments: { deployment: { clusters: ['cluster-1'], namespace: 'solo-e2e', }, 'deployment-2': { clusters: ['cluster-2'], namespace: 'solo-2', }, 'deployment-3': { clusters: ['cluster-3'], namespace: 'solo-3', }, }, clusterRefs: { 'cluster-1': 'context-1', 'cluster-2': 'context-2', }, }; //# sourceMappingURL=test_util.js.map