From ef778f6d2c25d13f72dd8233188de1e326227716 Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Wed, 26 Jun 2024 09:55:25 -0500 Subject: [PATCH 01/56] save Signed-off-by: Jeffrey Tang --- add_node.sh | 16 ++++++++++ src/commands/node.mjs | 74 ++++++++++++++++++++++++++++++++++++++++--- test/test_util.js | 2 +- version.mjs | 2 +- 4 files changed, 88 insertions(+), 6 deletions(-) create mode 100755 add_node.sh diff --git a/add_node.sh b/add_node.sh new file mode 100755 index 000000000..87235417c --- /dev/null +++ b/add_node.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + + SOLO_CLUSTER_NAME=solo-e2e + SOLO_NAMESPACE=solo-e2e + SOLO_CLUSTER_SETUP_NAMESPACE=solo-e2e-cluster + + kind delete cluster -n "${SOLO_CLUSTER_NAME}" || true + kind create cluster -n "${SOLO_CLUSTER_NAME}" || return + solo init --namespace "${SOLO_NAMESPACE}" -i node0,node1,node2 -s "${SOLO_CLUSTER_SETUP_NAMESPACE}" || return + solo node keys --gossip-keys --tls-keys --key-format pem || return + solo cluster setup || return + solo network deploy || return + solo node setup || return + solo node start || return + +# solo node add -i node3 --gossip-keys --tls-keys || return diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 2443ed61d..0016672a9 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -21,17 +21,18 @@ import { Listr } from 'listr2' import path from 'path' import { FullstackTestingError, IllegalArgumentError } from '../core/errors.mjs' import * as helpers from '../core/helpers.mjs' -import { getNodeLogs, getTmpDir, sleep, validatePath } from '../core/helpers.mjs' +import {getNodeAccountMap, getNodeLogs, getTmpDir, sleep, validatePath} from '../core/helpers.mjs' import { constants, Templates } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as prompts from './prompts.mjs' import { - AccountId, + AccountBalanceQuery, + AccountId, AccountUpdateTransaction, FileContentsQuery, FileId, FreezeTransaction, - FreezeType, + FreezeType, PrivateKey, Timestamp } from '@hashgraph/sdk' import * as crypto from 'crypto' @@ -72,6 +73,49 @@ export class NodeCommand extends BaseCommand { this._portForwards = [] } + async addStake(namespace, accountId) { + try { + await this.accountManager.loadNodeClient(namespace) + const client = this.accountManager._nodeClient + + // get some initial balance + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 100) + + // check balance + const balance = await new AccountBalanceQuery() + .setAccountId(accountId) + .execute(client) + console.log(`Account ${accountId} balance: ${balance.hbars}`) + + + // Create the transaction + const transaction = await new AccountUpdateTransaction() + .setAccountId(accountId) + .setStakedAccountId(accountId) + .freezeWith(client) + + const genesisKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) + + // Sign the transaction with the account's private key + const signTx = await transaction.sign(genesisKey); + + // Submit the transaction to a Hedera network + const txResponse = await signTx.execute(client); + + // Request the receipt of the transaction + const receipt = await txResponse.getReceipt(client); + + // Get the transaction status + const transactionStatus = receipt.status; + console.log("The transaction consensus status is " + transactionStatus.toString()); + + const accountInfo = await this.accountManager.accountInfoQuery(accountId) + console.log(`Account info: ${accountInfo}`) + } catch (e) { + this.logger.error(`Error in adding stake: ${e.message}`) + } + } + async checkNetworkNodePod (namespace, nodeId, maxAttempts = 60, delay = 2000) { nodeId = nodeId.trim() const podName = Templates.renderNetworkPodName(nodeId) @@ -719,6 +763,28 @@ export class NodeCommand extends BaseCommand { }) }, skip: (ctx, _) => self.configManager.getFlag(flags.app) !== '' + }, + { + title: 'Add node stakes', + task: (ctx, task) => { + const subTasks = [] + const accountMap = getNodeAccountMap(ctx.config.nodeIds) + for (const nodeId of ctx.config.nodeIds) { + const accountId = accountMap.get(nodeId) + subTasks.push({ + title: `Adding stake for node: ${chalk.yellow(nodeId)}`, + task: () => self.addStake(ctx.config.namespace, accountId) + }) + } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } + }) + } }], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION @@ -736,7 +802,7 @@ export class NodeCommand extends BaseCommand { return true } - async stop (argv) { + async stop (argv) { const self = this const tasks = new Listr([ diff --git a/test/test_util.js b/test/test_util.js index bb2345279..9cf1025b5 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -46,7 +46,7 @@ import { AccountBalanceQuery } from '@hashgraph/sdk' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.49.0-alpha.2' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.51.0' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index d7a48ab59..b6b71ba7f 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.28.2' -export const HEDERA_PLATFORM_VERSION = 'v0.49.0-alpha.2' +export const HEDERA_PLATFORM_VERSION = 'v0.51.0' From 50dc4fad4d815c42cc64676136055949a0cdae9b Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Mon, 1 Jul 2024 09:31:54 -0500 Subject: [PATCH 02/56] build zip package Signed-off-by: Jeffrey Tang --- src/commands/node.mjs | 49 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 0016672a9..87864b64c 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -26,11 +26,13 @@ import { constants, Templates } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as prompts from './prompts.mjs' +import AdmZip from 'adm-zip' + import { AccountBalanceQuery, AccountId, AccountUpdateTransaction, FileContentsQuery, - FileId, + FileId, FileUpdateTransaction, FreezeTransaction, FreezeType, PrivateKey, Timestamp @@ -1542,14 +1544,41 @@ export class NodeCommand extends BaseCommand { try { // fetch special file const fileId = FileId.fromString('0.0.150') - const fileQuery = new FileContentsQuery().setFileId(fileId) - const addressBookBytes = await fileQuery.execute(client) - const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') + // const fileQuery = new FileContentsQuery().setFileId(fileId) + // const addressBookBytes = await fileQuery.execute(client) + // const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') + + // create a file VERSION with content + // VERSION=0.2 + // Thu Jun 27 11:07:20 UTC 2024 + const versionFile = `${config.stagingDir}/VERSION` + fs.writeFileSync(versionFile, '0.2\n') + fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) + + // bundle config.txt and VERSIO into a zip file + const zipFile = `${config.stagingDir}/freeze.zip` + const zip = AdmZip('', {}) + zip.addLocalFile(`${config.stagingDir}/VERSION`) + zip.addLocalFile(`${config.stagingDir}/config.txt`) + // get byte value of the zip file + const zipBytes = zip.toBuffer() + const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') + // save zip file to local disk + fs.writeFileSync(zipFile, zipBytes) + + this.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) + // create a file upload transaction to upload file to the network + const fileTransaction = new FileUpdateTransaction() + .setFileId(fileId) + .setContents(zipBytes) + + const fileTransactionReceipt = await fileTransaction.execute(client) + this.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) const prepareUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.PrepareUpgrade) .setFileId(fileId) - .setFileHash(fileHash) + .setFileHash(zipHash) .freezeWith(client) .execute(client) @@ -1560,6 +1589,14 @@ export class NodeCommand extends BaseCommand { prepareUpgradeReceipt.status.toString() ) + const fileQuery = new FileContentsQuery().setFileId(fileId) + const updateFileBinary = await fileQuery.execute(client) + // save updateFileBinary to a file + fs.writeFileSync(`${config.stagingDir}/update2.zip`, updateFileBinary) + + await sleep(50000000) + + const futureDate = new Date() this.logger.debug(`Current time: ${futureDate}`) @@ -1570,7 +1607,7 @@ export class NodeCommand extends BaseCommand { .setFreezeType(FreezeType.FreezeUpgrade) .setStartTimestamp(Timestamp.fromDate(futureDate)) .setFileId(fileId) - .setFileHash(fileHash) + .setFileHash(zipHash) .freezeWith(client) .execute(client) From 47e882f79385781e060aeb742ed1291052a8b4c4 Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Mon, 1 Jul 2024 11:21:26 -0500 Subject: [PATCH 03/56] stash Signed-off-by: Jeffrey Tang --- add_node.sh | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/add_node.sh b/add_node.sh index 87235417c..c55c142f5 100755 --- a/add_node.sh +++ b/add_node.sh @@ -13,4 +13,4 @@ solo node setup || return solo node start || return -# solo node add -i node3 --gossip-keys --tls-keys || return + solo node add -i node3 --gossip-keys --tls-keys || return diff --git a/package.json b/package.json index 467d57b8f..f5612386f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test-e2e-node-pfx-kill-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PFX Kill Add Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pfx-kill-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pfx-kill-add' --testRegex=\".*\\/e2e\\/commands\\/node_pfx_kill_add\\.test\\.mjs\"", "test-e2e-node-local-build": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Local Custom Build' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-local-build.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-local-build' --testRegex=\".*\\/e2e\\/commands\\/node-local.*\\.test\\.mjs\"", "test-e2e-relay": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Relay Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-relay.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-relay' --testRegex=\".*\\/e2e\\/commands\\/relay\\.test\\.mjs\"", + "test-file": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand --detectOpenHandles --forceExit --testRegex=\".*\\/e2e\\/commands\\/file\\.test\\.mjs\"", "merge-clean": "rm -rf .nyc_output && mkdir .nyc_output && rm -rf coverage/lcov-report && rm -rf coverage/solo && rm coverage/*.*", "merge-e2e": "nyc merge ./coverage/e2e/ .nyc_output/coverage.json", "merge-unit": "nyc merge ./coverage/unit/ .nyc_output/coverage.json", From 7c0dffd36e6dc50d35c8652bcd36361fe9b9b051 Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Wed, 10 Jul 2024 11:23:16 -0500 Subject: [PATCH 04/56] save Signed-off-by: Jeffrey Tang --- add_node.sh | 5 +- src/commands/node.mjs | 49 +++++++---- src/core/helpers.mjs | 15 +++- test/e2e/commands/file.test.mjs | 140 ++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 test/e2e/commands/file.test.mjs diff --git a/add_node.sh b/add_node.sh index c55c142f5..124a8d283 100755 --- a/add_node.sh +++ b/add_node.sh @@ -10,7 +10,8 @@ solo node keys --gossip-keys --tls-keys --key-format pem || return solo cluster setup || return solo network deploy || return - solo node setup || return - solo node start || return + solo node setup --local-build-path /Users/jeffrey/hedera-services/hedera-node/data/ || return + solo node start || return solo node add -i node3 --gossip-keys --tls-keys || return + solo node logs diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 87864b64c..e5904d323 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -456,6 +456,16 @@ export class NodeCommand extends BaseCommand { }) } + fetchLocalOrReleasedPlatformSoftware (ctx, task) { + const self = this + const localBuildPath = self.configManager.getFlag(flags.localBuildPath) + if (localBuildPath !== '') { + return self.uploadPlatformSoftware(ctx, task, localBuildPath) + } else { + return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) + } + } + fetchPlatformSoftware (ctx, task, platformInstaller) { const config = ctx.config @@ -617,12 +627,7 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into network nodes', task: async (ctx, task) => { - const localBuildPath = self.configManager.getFlag(flags.localBuildPath) - if (localBuildPath !== '') { - return self.uploadPlatformSoftware(ctx, task, localBuildPath) - } else { - return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) - } + return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) } }, { @@ -1018,7 +1023,7 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into network nodes', task: async (ctx, task) => { - return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) + return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) } }, { @@ -1397,7 +1402,7 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into network nodes', task: async (ctx, task) => { - return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) + return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) } }, { @@ -1405,6 +1410,7 @@ export class NodeCommand extends BaseCommand { task: async (ctx, task) => { await this.freezeNetworkNodes(ctx.config) + await this.logs(argv) } }, { @@ -1542,20 +1548,14 @@ export class NodeCommand extends BaseCommand { const client = this.accountManager._nodeClient try { - // fetch special file + const fileId = FileId.fromString('0.0.150') - // const fileQuery = new FileContentsQuery().setFileId(fileId) - // const addressBookBytes = await fileQuery.execute(client) - // const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') - // create a file VERSION with content - // VERSION=0.2 - // Thu Jun 27 11:07:20 UTC 2024 + // bundle config.txt and VERSION into a zip file const versionFile = `${config.stagingDir}/VERSION` fs.writeFileSync(versionFile, '0.2\n') fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) - // bundle config.txt and VERSIO into a zip file const zipFile = `${config.stagingDir}/freeze.zip` const zip = AdmZip('', {}) zip.addLocalFile(`${config.stagingDir}/VERSION`) @@ -1567,11 +1567,14 @@ export class NodeCommand extends BaseCommand { fs.writeFileSync(zipFile, zipBytes) this.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) + // create a file upload transaction to upload file to the network const fileTransaction = new FileUpdateTransaction() .setFileId(fileId) .setContents(zipBytes) + // await sleep(5000) + const fileTransactionReceipt = await fileTransaction.execute(client) this.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) @@ -1589,12 +1592,13 @@ export class NodeCommand extends BaseCommand { prepareUpgradeReceipt.status.toString() ) + // read back file 0.0.150 const fileQuery = new FileContentsQuery().setFileId(fileId) const updateFileBinary = await fileQuery.execute(client) // save updateFileBinary to a file fs.writeFileSync(`${config.stagingDir}/update2.zip`, updateFileBinary) - await sleep(50000000) + await sleep(5000) const futureDate = new Date() @@ -1611,10 +1615,21 @@ export class NodeCommand extends BaseCommand { .freezeWith(client) .execute(client) + await sleep(5000) + const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(client) this.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, freezeUpgradeReceipt.status.toString()) + + // overwrite config.text fro all nodes except ctx.config.nodeIds + for (const nodeId of config.allNodeIds) { + if (!config.nodeIds.includes(nodeId)) { + const podName = config.podNames[nodeId] + this.logger.info(`copy config.txt for node = ${nodeId}`) + await this.k8.copyTo(podName, constants.ROOT_CONTAINER, `${config.stagingDir}/config.txt`, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/`) + } + } } catch (e) { this.logger.error(`Error in freeze upgrade: ${e.message}`, e) throw new FullstackTestingError(`Error in freeze upgrade: ${e.message}`, e) diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 2c99fd0d8..c3bc37c43 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -195,18 +195,27 @@ export function validatePath (input) { export async function getNodeLogs (k8, namespace) { const pods = await k8.getPodsByLabel(['fullstack.hedera.com/type=network-node']) + const timeString = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-') + for (const pod of pods) { const podName = pod.metadata.name const targetDir = `${SOLO_LOGS_DIR}/${namespace}/${podName}` try { - if (fs.existsSync(targetDir)) { - fs.rmdirSync(targetDir, { recursive: true }) + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }) } - fs.mkdirSync(targetDir, { recursive: true }) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/output/swirlds.log`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/output/hgcaa.log`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/config.txt`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settings.txt`, targetDir) + + // rename all files with timeString as prefix to avoid overwrite + fs.readdirSync(targetDir).forEach(file => { + const oldPath = path.join(targetDir, file) + const newPath = path.join(targetDir, `${timeString}-${file}`) + fs.renameSync(oldPath, newPath) + }) + } catch (e) { // not throw error here, so we can continue to finish downloading logs from other pods // and also delete namespace in the end diff --git a/test/e2e/commands/file.test.mjs b/test/e2e/commands/file.test.mjs new file mode 100644 index 000000000..7df5402e8 --- /dev/null +++ b/test/e2e/commands/file.test.mjs @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @jest-environment steps + */ + +import { + AccountId, + FileContentsQuery, + FileId, + FileUpdateTransaction, + FreezeTransaction, + FreezeType, + PrivateKey +} from '@hashgraph/sdk' +import { + afterAll, + beforeAll, + describe, + expect, + it +} from '@jest/globals' +import { + constants +} from '../../../src/core/index.mjs' +import * as version from '../../../version.mjs' +import { + bootstrapNetwork, bootstrapTestVariables, + getDefaultArgv, + HEDERA_PLATFORM_VERSION_TAG, + TEST_CLUSTER, + testLogger +} from '../../test_util.js' +import { AccountCommand } from '../../../src/commands/account.mjs' +import { flags } from '../../../src/commands/index.mjs' +import {getNodeLogs, sleep} from '../../../src/core/helpers.mjs' +import fs from "fs"; +import AdmZip from "adm-zip"; +import crypto from "crypto"; + +describe('AccountCommand', () => { + const testName = 'account-cmd-e2e' + const namespace = testName + const defaultTimeout = 20000 + const testSystemAccounts = [[3, 5]] + const argv = getDefaultArgv() + argv[flags.namespace.name] = namespace + argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG + argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM + argv[flags.nodeIDs.name] = 'node0' + argv[flags.generateGossipKeys.name] = true + argv[flags.generateTlsKeys.name] = true + argv[flags.clusterName.name] = TEST_CLUSTER + argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION + // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts + argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined + const bootstrapResp = bootstrapTestVariables(testName, argv) + const k8 = bootstrapResp.opts.k8 + const accountManager = bootstrapResp.opts.accountManager + const configManager = bootstrapResp.opts.configManager + const nodeCmd = bootstrapResp.cmd.nodeCmd + const accountCmd = new AccountCommand(bootstrapResp.opts, testSystemAccounts) + + + describe('test update file 0.0.150', () => { + let accountId1, accountId2 + + it('should update file 0.0.150', async () => { + await accountManager.loadNodeClient(namespace) + const client = accountManager._nodeClient + + + // fetch special file + // const fileQuery = new FileContentsQuery().setFileId(fileId) + // const addressBookBytes = await fileQuery.execute(client) + // const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') + + // create a file VERSION with content + // VERSION=0.2 + // Thu Jun 27 11:07:20 UTC 2024 + const versionFile = `/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/VERSION` + fs.writeFileSync(versionFile, '0.2\n') + fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) + + // bundle config.txt and VERSIO into a zip file + const zipFile = `/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/freeze.zip` + const zip = AdmZip('', {}) + zip.addLocalFile(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/VERSION`) + zip.addLocalFile(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/config.txt`) + // get byte value of the zip file + const zipBytes = zip.toBuffer() + const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') + + accountManager.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) + // create a file upload transaction to upload file to the network + const fileId = FileId.fromString('0.0.150') + const fileTransaction = new FileUpdateTransaction() + .setFileId(fileId) + .setContents(zipBytes) + + + const fileTransactionReceipt = await fileTransaction.execute(client) + accountManager.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) + + + const prepareUpgradeTx = await new FreezeTransaction() + .setFreezeType(FreezeType.PrepareUpgrade) + .setFileId(fileId) + .setFileHash(zipHash) + .freezeWith(client) + .execute(client) + + const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) + + accountManager.logger.debug( + `Upgrade prepared with transaction id: ${prepareUpgradeTx.transactionId.toString()}`, + prepareUpgradeReceipt.status.toString() + ) + + const fileQuery = new FileContentsQuery().setFileId(fileId) + const updateFileBinary = await fileQuery.execute(client) + // save updateFileBinary to a file + fs.writeFileSync(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0//update.zip`, updateFileBinary) + + }, defaultTimeout) + + }) +}) From 0558513c4e270560c6d5c9216f5ac32b313cde6a Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Wed, 26 Jun 2024 09:55:25 -0500 Subject: [PATCH 05/56] save Signed-off-by: Jeffrey Tang --- add_node.sh | 16 ++++++++++ src/commands/node.mjs | 74 ++++++++++++++++++++++++++++++++++++++++--- test/test_util.js | 2 +- version.mjs | 2 +- 4 files changed, 88 insertions(+), 6 deletions(-) create mode 100755 add_node.sh diff --git a/add_node.sh b/add_node.sh new file mode 100755 index 000000000..87235417c --- /dev/null +++ b/add_node.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + + SOLO_CLUSTER_NAME=solo-e2e + SOLO_NAMESPACE=solo-e2e + SOLO_CLUSTER_SETUP_NAMESPACE=solo-e2e-cluster + + kind delete cluster -n "${SOLO_CLUSTER_NAME}" || true + kind create cluster -n "${SOLO_CLUSTER_NAME}" || return + solo init --namespace "${SOLO_NAMESPACE}" -i node0,node1,node2 -s "${SOLO_CLUSTER_SETUP_NAMESPACE}" || return + solo node keys --gossip-keys --tls-keys --key-format pem || return + solo cluster setup || return + solo network deploy || return + solo node setup || return + solo node start || return + +# solo node add -i node3 --gossip-keys --tls-keys || return diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 838990f49..2cd66f195 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -21,17 +21,18 @@ import { Listr } from 'listr2' import path from 'path' import { FullstackTestingError, IllegalArgumentError } from '../core/errors.mjs' import * as helpers from '../core/helpers.mjs' -import { getNodeLogs, getTmpDir, sleep, validatePath } from '../core/helpers.mjs' +import {getNodeAccountMap, getNodeLogs, getTmpDir, sleep, validatePath} from '../core/helpers.mjs' import { constants, Templates } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as prompts from './prompts.mjs' import { - AccountId, + AccountBalanceQuery, + AccountId, AccountUpdateTransaction, FileContentsQuery, FileId, FreezeTransaction, - FreezeType, + FreezeType, PrivateKey, Timestamp } from '@hashgraph/sdk' import * as crypto from 'crypto' @@ -157,6 +158,49 @@ export class NodeCommand extends BaseCommand { this._portForwards = [] } + async addStake(namespace, accountId) { + try { + await this.accountManager.loadNodeClient(namespace) + const client = this.accountManager._nodeClient + + // get some initial balance + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 100) + + // check balance + const balance = await new AccountBalanceQuery() + .setAccountId(accountId) + .execute(client) + console.log(`Account ${accountId} balance: ${balance.hbars}`) + + + // Create the transaction + const transaction = await new AccountUpdateTransaction() + .setAccountId(accountId) + .setStakedAccountId(accountId) + .freezeWith(client) + + const genesisKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) + + // Sign the transaction with the account's private key + const signTx = await transaction.sign(genesisKey); + + // Submit the transaction to a Hedera network + const txResponse = await signTx.execute(client); + + // Request the receipt of the transaction + const receipt = await txResponse.getReceipt(client); + + // Get the transaction status + const transactionStatus = receipt.status; + console.log("The transaction consensus status is " + transactionStatus.toString()); + + const accountInfo = await this.accountManager.accountInfoQuery(accountId) + console.log(`Account info: ${accountInfo}`) + } catch (e) { + this.logger.error(`Error in adding stake: ${e.message}`) + } + } + async checkNetworkNodePod (namespace, nodeId, maxAttempts = 60, delay = 2000) { nodeId = nodeId.trim() const podName = Templates.renderNetworkPodName(nodeId) @@ -852,6 +896,28 @@ export class NodeCommand extends BaseCommand { }) }, skip: (ctx, _) => self.configManager.getFlag(flags.app) !== '' + }, + { + title: 'Add node stakes', + task: (ctx, task) => { + const subTasks = [] + const accountMap = getNodeAccountMap(ctx.config.nodeIds) + for (const nodeId of ctx.config.nodeIds) { + const accountId = accountMap.get(nodeId) + subTasks.push({ + title: `Adding stake for node: ${chalk.yellow(nodeId)}`, + task: () => self.addStake(ctx.config.namespace, accountId) + }) + } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } + }) + } }], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION @@ -869,7 +935,7 @@ export class NodeCommand extends BaseCommand { return true } - async stop (argv) { + async stop (argv) { const self = this const tasks = new Listr([ diff --git a/test/test_util.js b/test/test_util.js index e39dd2703..d430478fa 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -46,7 +46,7 @@ import { AccountBalanceQuery } from '@hashgraph/sdk' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.49.0-alpha.2' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.51.0' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index d7a48ab59..b6b71ba7f 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.28.2' -export const HEDERA_PLATFORM_VERSION = 'v0.49.0-alpha.2' +export const HEDERA_PLATFORM_VERSION = 'v0.51.0' From e03f2956a7e64d957232a48b81b74e558fdd240d Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Mon, 1 Jul 2024 09:31:54 -0500 Subject: [PATCH 06/56] build zip package Signed-off-by: Jeffrey Tang --- src/commands/node.mjs | 49 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 2cd66f195..21bc3f4cd 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -26,11 +26,13 @@ import { constants, Templates } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as prompts from './prompts.mjs' +import AdmZip from 'adm-zip' + import { AccountBalanceQuery, AccountId, AccountUpdateTransaction, FileContentsQuery, - FileId, + FileId, FileUpdateTransaction, FreezeTransaction, FreezeType, PrivateKey, Timestamp @@ -1773,14 +1775,41 @@ export class NodeCommand extends BaseCommand { try { // fetch special file const fileId = FileId.fromString('0.0.150') - const fileQuery = new FileContentsQuery().setFileId(fileId) - const addressBookBytes = await fileQuery.execute(client) - const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') + // const fileQuery = new FileContentsQuery().setFileId(fileId) + // const addressBookBytes = await fileQuery.execute(client) + // const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') + + // create a file VERSION with content + // VERSION=0.2 + // Thu Jun 27 11:07:20 UTC 2024 + const versionFile = `${config.stagingDir}/VERSION` + fs.writeFileSync(versionFile, '0.2\n') + fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) + + // bundle config.txt and VERSIO into a zip file + const zipFile = `${config.stagingDir}/freeze.zip` + const zip = AdmZip('', {}) + zip.addLocalFile(`${config.stagingDir}/VERSION`) + zip.addLocalFile(`${config.stagingDir}/config.txt`) + // get byte value of the zip file + const zipBytes = zip.toBuffer() + const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') + // save zip file to local disk + fs.writeFileSync(zipFile, zipBytes) + + this.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) + // create a file upload transaction to upload file to the network + const fileTransaction = new FileUpdateTransaction() + .setFileId(fileId) + .setContents(zipBytes) + + const fileTransactionReceipt = await fileTransaction.execute(client) + this.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) const prepareUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.PrepareUpgrade) .setFileId(fileId) - .setFileHash(fileHash) + .setFileHash(zipHash) .freezeWith(client) .execute(client) @@ -1791,6 +1820,14 @@ export class NodeCommand extends BaseCommand { prepareUpgradeReceipt.status.toString() ) + const fileQuery = new FileContentsQuery().setFileId(fileId) + const updateFileBinary = await fileQuery.execute(client) + // save updateFileBinary to a file + fs.writeFileSync(`${config.stagingDir}/update2.zip`, updateFileBinary) + + await sleep(50000000) + + const futureDate = new Date() this.logger.debug(`Current time: ${futureDate}`) @@ -1801,7 +1838,7 @@ export class NodeCommand extends BaseCommand { .setFreezeType(FreezeType.FreezeUpgrade) .setStartTimestamp(Timestamp.fromDate(futureDate)) .setFileId(fileId) - .setFileHash(fileHash) + .setFileHash(zipHash) .freezeWith(client) .execute(client) From 42dbe75a9b07b967c8cd0f44a9c746b3911c130d Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 18 Jul 2024 20:42:44 +0100 Subject: [PATCH 07/56] NodeCreateTransaction Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 131 +++++++++++++++++++++++++++----------- test/e2e/e2e_node_util.js | 2 +- test/test_util.js | 2 +- version.mjs | 2 +- 4 files changed, 96 insertions(+), 41 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 21bc3f4cd..d17091e99 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -21,7 +21,7 @@ import { Listr } from 'listr2' import path from 'path' import { FullstackTestingError, IllegalArgumentError } from '../core/errors.mjs' import * as helpers from '../core/helpers.mjs' -import {getNodeAccountMap, getNodeLogs, getTmpDir, sleep, validatePath} from '../core/helpers.mjs' +import { getNodeAccountMap, getNodeLogs, getTmpDir, sleep, validatePath } from '../core/helpers.mjs' import { constants, Templates } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' @@ -30,14 +30,20 @@ import AdmZip from 'adm-zip' import { AccountBalanceQuery, - AccountId, AccountUpdateTransaction, + AccountId, + AccountUpdateTransaction, FileContentsQuery, - FileId, FileUpdateTransaction, + FileId, + FileUpdateTransaction, FreezeTransaction, - FreezeType, PrivateKey, + FreezeType, + NodeCreateTransaction, + PrivateKey, + ServiceEndpoint, Timestamp } from '@hashgraph/sdk' import * as crypto from 'crypto' +import { GENESIS_KEY } from '../core/constants.mjs' /** * Defines the core functionalities of 'node' command @@ -160,7 +166,7 @@ export class NodeCommand extends BaseCommand { this._portForwards = [] } - async addStake(namespace, accountId) { + async addStake (namespace, accountId) { try { await this.accountManager.loadNodeClient(namespace) const client = this.accountManager._nodeClient @@ -174,7 +180,6 @@ export class NodeCommand extends BaseCommand { .execute(client) console.log(`Account ${accountId} balance: ${balance.hbars}`) - // Create the transaction const transaction = await new AccountUpdateTransaction() .setAccountId(accountId) @@ -184,17 +189,17 @@ export class NodeCommand extends BaseCommand { const genesisKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) // Sign the transaction with the account's private key - const signTx = await transaction.sign(genesisKey); + const signTx = await transaction.sign(genesisKey) // Submit the transaction to a Hedera network - const txResponse = await signTx.execute(client); + const txResponse = await signTx.execute(client) // Request the receipt of the transaction - const receipt = await txResponse.getReceipt(client); + const receipt = await txResponse.getReceipt(client) // Get the transaction status - const transactionStatus = receipt.status; - console.log("The transaction consensus status is " + transactionStatus.toString()); + const transactionStatus = receipt.status + console.log('The transaction consensus status is ' + transactionStatus.toString()) const accountInfo = await this.accountManager.accountInfoQuery(accountId) console.log(`Account info: ${accountInfo}`) @@ -937,7 +942,7 @@ export class NodeCommand extends BaseCommand { return true } - async stop (argv) { + async stop (argv) { const self = this const tasks = new Listr([ @@ -1608,6 +1613,7 @@ export class NodeCommand extends BaseCommand { } }, { + // TODO: not needed? title: 'Prepare config.txt for the network', task: async (ctx, _) => { const config = ctx.config @@ -1631,42 +1637,88 @@ export class NodeCommand extends BaseCommand { return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) } }, + // { + // // TODO: not needed? + // title: 'Freeze network nodes', + // task: + // async (ctx, task) => { + // await this.freezeNetworkNodes(ctx.config) + // } + // }, + // { + // // TODO: not needed? + // title: 'Check nodes are frozen', + // task: (ctx, task) => { + // const subTasks = [] + // for (const nodeId of ctx.config.existingNodeIds) { + // subTasks.push({ + // title: `Check node: ${chalk.yellow(nodeId)}`, + // task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') + // }) + // } + // + // // set up the sub-tasks + // return task.newListr(subTasks, { + // concurrent: false, + // rendererOptions: { + // collapseSubtasks: false + // } + // }) + // } + // }, { - title: 'Freeze network nodes', + title: 'Add new node(s) to network', task: async (ctx, task) => { - await this.freezeNetworkNodes(ctx.config) - } - }, - { - title: 'Check nodes are frozen', - task: (ctx, task) => { - const subTasks = [] - for (const nodeId of ctx.config.existingNodeIds) { - subTasks.push({ - title: `Check node: ${chalk.yellow(nodeId)}`, - task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') - }) - } - - // set up the sub-tasks - return task.newListr(subTasks, { - concurrent: false, - rendererOptions: { - collapseSubtasks: false + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const networkNodeServicesMap = await this.accountManager.getNodeServiceMap(config.namespace) + const client = this.accountManager._nodeClient + + for (const nodeId of config.nodeIds) { + const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) + const privateKeyFullPath = `${config.stagingKeysDir}/${privateKeyFile}` + const privateKeyAsBuffer = fs.readFileSync(privateKeyFullPath) + const privateKeyAsUint8Array = new Uint8Array(privateKeyAsBuffer) + + const networkNodeServices = networkNodeServicesMap.get(nodeId) + const gossipEndpoints = /** @type {ServiceEndpoint[]} **/ [] + gossipEndpoints.push(new ServiceEndpoint({ + port: networkNodeServices.nodeServiceGossipPort, + domainName: Templates.renderFullyQualifiedNetworkSvcName(config.namespace, nodeId) + })) + const serviceEndpoints = /** @type {ServiceEndpoint[]} **/ [] + serviceEndpoints.push(new ServiceEndpoint({ + port: networkNodeServices.nodeServiceGrpcPort, + domainName: Templates.renderFullyQualifiedNetworkSvcName(config.namespace, nodeId) + })) + try { + const nodeCreateTx = await new NodeCreateTransaction() + .setAccountId(networkNodeServices.accountId) + .setGossipEndpoints(gossipEndpoints) + .setServiceEndpoints(serviceEndpoints) + .setGossipCaCertificate(privateKeyAsUint8Array) + // TODO create Issue to add this later: .setCertificateHash() + .setAdminKey(PrivateKey.fromStringED25519(GENESIS_KEY)) + .execute(client) + const nodeCreateReceipt = await nodeCreateTx.getReceipt(client) + this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) + } catch (e) { + throw new FullstackTestingError(`Error adding node to network: ${e.message}`, e) + } + } } - }) - } }, { title: 'Setup network nodes', task: async (ctx, parentTask) => { const config = ctx.config - // modify application.properties to trick Hedera Services into receiving an updated address book - await self.bumpHederaConfigVersion(`${config.stagingDir}/templates/application.properties`) + // // TODO: not needed? + // // modify application.properties to trick Hedera Services into receiving an updated address book + // await self.bumpHederaConfigVersion(`${config.stagingDir}/templates/application.properties`) const subTasks = [] + // TODO: still needed for copying keys? for (const nodeId of config.allNodeIds) { const podName = config.podNames[nodeId] subTasks.push({ @@ -1687,7 +1739,11 @@ export class NodeCommand extends BaseCommand { title: 'Starting nodes', task: (ctx, task) => { const subTasks = [] - self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) + // TODO: only start new nodes? + // self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) + self.startNodes(ctx.config, ctx.config.nodeIds, subTasks) + + // TODO: stake new node? // set up the sub-tasks return task.newListr(subTasks, { @@ -1827,7 +1883,6 @@ export class NodeCommand extends BaseCommand { await sleep(50000000) - const futureDate = new Date() this.logger.debug(`Current time: ${futureDate}`) diff --git a/test/e2e/e2e_node_util.js b/test/e2e/e2e_node_util.js index 377e66d77..ea21f67e7 100644 --- a/test/e2e/e2e_node_util.js +++ b/test/e2e/e2e_node_util.js @@ -79,7 +79,7 @@ export function e2eNodeKeyRefreshAddTest (keyFormat, testName, mode, releaseTag afterAll(async () => { await getNodeLogs(k8, namespace) - await k8.deleteNamespace(namespace) + // await k8.deleteNamespace(namespace) // TODO revert }, 180000) describe(`Node should have started successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, () => { diff --git a/test/test_util.js b/test/test_util.js index d430478fa..031dd11b5 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -46,7 +46,7 @@ import { AccountBalanceQuery } from '@hashgraph/sdk' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.51.0' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.0-develop.xd2fbe98' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index b6b71ba7f..7287076e5 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.28.2' -export const HEDERA_PLATFORM_VERSION = 'v0.51.0' +export const HEDERA_PLATFORM_VERSION = 'v0.53.0-develop.xd2fbe98' From 342075a1a550b3ae60b35f4e8e29b71bfe37e4d6 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 18 Jul 2024 22:01:51 +0100 Subject: [PATCH 08/56] // scheduleNetworkUpdate is set to false, because the ports 50212/50211 are hardcoded in JS SDK that will not work when running locally or in a pipeline Signed-off-by: Jeromy Cannon --- src/core/account_manager.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index e3ac36c7f..16cd4fadb 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -232,7 +232,8 @@ export class AccountManager { } this.logger.debug(`creating client from network configuration: ${JSON.stringify(nodes)}`) - this._nodeClient = Client.fromConfig({ network: nodes }) + // scheduleNetworkUpdate is set to false, because the ports 50212/50211 are hardcoded in JS SDK that will not work when running locally or in a pipeline + this._nodeClient = Client.fromConfig({ network: nodes, scheduleNetworkUpdate: false }) this._nodeClient.setOperator(operatorId, operatorKey) this._nodeClient.setLogger(new Logger(LogLevel.Trace, `${constants.SOLO_LOGS_DIR}/hashgraph-sdk.log`)) this._nodeClient.setMaxAttempts(constants.NODE_CLIENT_MAX_ATTEMPTS) From 64f79817266805eee71c35311f2bbe8d562c27be Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 18 Jul 2024 22:08:38 +0100 Subject: [PATCH 09/56] increased default stake amount Signed-off-by: Jeromy Cannon --- src/core/constants.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/constants.mjs b/src/core/constants.mjs index 523517e9e..2ea7b94c7 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -46,7 +46,7 @@ export const HEDERA_BUILDS_URL = 'https://builds.hedera.com' export const HEDERA_NODE_ACCOUNT_ID_START = AccountId.fromString(process.env.SOLO_NODE_ACCOUNT_ID_START || '0.0.3') export const HEDERA_NODE_INTERNAL_GOSSIP_PORT = process.env.SOLO_NODE_INTERNAL_GOSSIP_PORT || '50111' export const HEDERA_NODE_EXTERNAL_GOSSIP_PORT = process.env.SOLO_NODE_EXTERNAL_GOSSIP_PORT || '50111' -export const HEDERA_NODE_DEFAULT_STAKE_AMOUNT = process.env.SOLO_NODE_DEFAULT_STAKE_AMOUNT || 1 +export const HEDERA_NODE_DEFAULT_STAKE_AMOUNT = process.env.SOLO_NODE_DEFAULT_STAKE_AMOUNT || 500 // --------------- Charts related constants ---------------------------------------------------------------------------- export const FULLSTACK_SETUP_NAMESPACE = 'fullstack-setup' From 1c7f216dc9f0a87c337a2677deb2e7057924dda0 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 18 Jul 2024 22:09:11 +0100 Subject: [PATCH 10/56] get the treasury key, it might be in k8s secret, else genesis key Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index d17091e99..075d2cc4a 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -43,7 +43,10 @@ import { Timestamp } from '@hashgraph/sdk' import * as crypto from 'crypto' -import { GENESIS_KEY } from '../core/constants.mjs' +import { + GENESIS_KEY, + HEDERA_NODE_DEFAULT_STAKE_AMOUNT +} from '../core/constants.mjs' /** * Defines the core functionalities of 'node' command @@ -172,7 +175,7 @@ export class NodeCommand extends BaseCommand { const client = this.accountManager._nodeClient // get some initial balance - await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 100) + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, HEDERA_NODE_DEFAULT_STAKE_AMOUNT) // check balance const balance = await new AccountBalanceQuery() @@ -186,10 +189,10 @@ export class NodeCommand extends BaseCommand { .setStakedAccountId(accountId) .freezeWith(client) - const genesisKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) + const treasuryKey = this.accountManager.getTreasuryAccountKeys(namespace) // Sign the transaction with the account's private key - const signTx = await transaction.sign(genesisKey) + const signTx = await transaction.sign(treasuryKey) // Submit the transaction to a Hedera network const txResponse = await signTx.execute(client) From 407ffa19c22533df2e0f7f2ae99467580fea87a9 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 18 Jul 2024 22:18:55 +0100 Subject: [PATCH 11/56] todo's after conversations in Slack Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 69 ++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 075d2cc4a..4df3b3a40 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -1617,6 +1617,8 @@ export class NodeCommand extends BaseCommand { }, { // TODO: not needed? + // TODO: might need to read this from an existing node, at the perfect time, which is? after freeze prepare upgrade + // the config.txt to get is in the same directory where the upgradeArtifactsPath located, such as data/upgrade/current title: 'Prepare config.txt for the network', task: async (ctx, _) => { const config = ctx.config @@ -1640,35 +1642,6 @@ export class NodeCommand extends BaseCommand { return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) } }, - // { - // // TODO: not needed? - // title: 'Freeze network nodes', - // task: - // async (ctx, task) => { - // await this.freezeNetworkNodes(ctx.config) - // } - // }, - // { - // // TODO: not needed? - // title: 'Check nodes are frozen', - // task: (ctx, task) => { - // const subTasks = [] - // for (const nodeId of ctx.config.existingNodeIds) { - // subTasks.push({ - // title: `Check node: ${chalk.yellow(nodeId)}`, - // task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') - // }) - // } - // - // // set up the sub-tasks - // return task.newListr(subTasks, { - // concurrent: false, - // rendererOptions: { - // collapseSubtasks: false - // } - // }) - // } - // }, { title: 'Add new node(s) to network', task: @@ -1701,6 +1674,8 @@ export class NodeCommand extends BaseCommand { .setServiceEndpoints(serviceEndpoints) .setGossipCaCertificate(privateKeyAsUint8Array) // TODO create Issue to add this later: .setCertificateHash() + // TODO if any k8s secrets exists for account IDs, we know that `solo account init` has been ran, + // and we need to generate a new key for the new account and store it in the k8s secrets .setAdminKey(PrivateKey.fromStringED25519(GENESIS_KEY)) .execute(client) const nodeCreateReceipt = await nodeCreateTx.getReceipt(client) @@ -1711,17 +1686,42 @@ export class NodeCommand extends BaseCommand { } } }, + { + title: 'Freeze network nodes', + task: + async (ctx, task) => { + await this.freezeNetworkNodes(ctx.config) + } + }, + { + title: 'Check nodes are frozen', + task: (ctx, task) => { + const subTasks = [] + for (const nodeId of ctx.config.existingNodeIds) { + subTasks.push({ + title: `Check node: ${chalk.yellow(nodeId)}`, + task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') + }) + } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } + }) + } + }, { title: 'Setup network nodes', task: async (ctx, parentTask) => { const config = ctx.config - // // TODO: not needed? - // // modify application.properties to trick Hedera Services into receiving an updated address book - // await self.bumpHederaConfigVersion(`${config.stagingDir}/templates/application.properties`) + // modify application.properties to trick Hedera Services into receiving an updated address book + await self.bumpHederaConfigVersion(`${config.stagingDir}/templates/application.properties`) const subTasks = [] - // TODO: still needed for copying keys? for (const nodeId of config.allNodeIds) { const podName = config.podNames[nodeId] subTasks.push({ @@ -1802,6 +1802,7 @@ export class NodeCommand extends BaseCommand { }) } }, + // TODO: add stake to the new node { title: 'Finalize', task: (ctx, _) => { @@ -1879,6 +1880,8 @@ export class NodeCommand extends BaseCommand { prepareUpgradeReceipt.status.toString() ) + // TODO: split? now we need to get the config.txt? + const fileQuery = new FileContentsQuery().setFileId(fileId) const updateFileBinary = await fileQuery.execute(client) // save updateFileBinary to a file From da231bb5cb891d7ca6b97f94ace16d176587798c Mon Sep 17 00:00:00 2001 From: Jeffrey Tang Date: Wed, 24 Jul 2024 16:59:31 -0500 Subject: [PATCH 12/56] update Signed-off-by: Jeffrey Tang --- add_node.sh | 17 ---- package.json | 1 - test/e2e/commands/file.test.mjs | 140 -------------------------------- test/test_util.js | 1 + 4 files changed, 1 insertion(+), 158 deletions(-) delete mode 100755 add_node.sh delete mode 100644 test/e2e/commands/file.test.mjs diff --git a/add_node.sh b/add_node.sh deleted file mode 100755 index 124a8d283..000000000 --- a/add_node.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - - SOLO_CLUSTER_NAME=solo-e2e - SOLO_NAMESPACE=solo-e2e - SOLO_CLUSTER_SETUP_NAMESPACE=solo-e2e-cluster - - kind delete cluster -n "${SOLO_CLUSTER_NAME}" || true - kind create cluster -n "${SOLO_CLUSTER_NAME}" || return - solo init --namespace "${SOLO_NAMESPACE}" -i node0,node1,node2 -s "${SOLO_CLUSTER_SETUP_NAMESPACE}" || return - solo node keys --gossip-keys --tls-keys --key-format pem || return - solo cluster setup || return - solo network deploy || return - solo node setup --local-build-path /Users/jeffrey/hedera-services/hedera-node/data/ || return - - solo node start || return - solo node add -i node3 --gossip-keys --tls-keys || return - solo node logs diff --git a/package.json b/package.json index dd6d84e69..7058a86b5 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "test-e2e-node-pfx-kill-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PFX Kill Add Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pfx-kill-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pfx-kill-add' --testRegex=\".*\\/e2e\\/commands\\/node_pfx_kill_add\\.test\\.mjs\"", "test-e2e-node-local-build": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Local Custom Build' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-local-build.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-local-build' --testRegex=\".*\\/e2e\\/commands\\/node-local.*\\.test\\.mjs\"", "test-e2e-relay": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Relay Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-relay.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-relay' --testRegex=\".*\\/e2e\\/commands\\/relay\\.test\\.mjs\"", - "test-file": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand --detectOpenHandles --forceExit --testRegex=\".*\\/e2e\\/commands\\/file\\.test\\.mjs\"", "merge-clean": "rm -rf .nyc_output && mkdir .nyc_output && rm -rf coverage/lcov-report && rm -rf coverage/solo && rm coverage/*.*", "merge-e2e": "nyc merge ./coverage/e2e/ .nyc_output/coverage.json", "merge-unit": "nyc merge ./coverage/unit/ .nyc_output/coverage.json", diff --git a/test/e2e/commands/file.test.mjs b/test/e2e/commands/file.test.mjs deleted file mode 100644 index 7df5402e8..000000000 --- a/test/e2e/commands/file.test.mjs +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the ""License""); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an ""AS IS"" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @jest-environment steps - */ - -import { - AccountId, - FileContentsQuery, - FileId, - FileUpdateTransaction, - FreezeTransaction, - FreezeType, - PrivateKey -} from '@hashgraph/sdk' -import { - afterAll, - beforeAll, - describe, - expect, - it -} from '@jest/globals' -import { - constants -} from '../../../src/core/index.mjs' -import * as version from '../../../version.mjs' -import { - bootstrapNetwork, bootstrapTestVariables, - getDefaultArgv, - HEDERA_PLATFORM_VERSION_TAG, - TEST_CLUSTER, - testLogger -} from '../../test_util.js' -import { AccountCommand } from '../../../src/commands/account.mjs' -import { flags } from '../../../src/commands/index.mjs' -import {getNodeLogs, sleep} from '../../../src/core/helpers.mjs' -import fs from "fs"; -import AdmZip from "adm-zip"; -import crypto from "crypto"; - -describe('AccountCommand', () => { - const testName = 'account-cmd-e2e' - const namespace = testName - const defaultTimeout = 20000 - const testSystemAccounts = [[3, 5]] - const argv = getDefaultArgv() - argv[flags.namespace.name] = namespace - argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG - argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM - argv[flags.nodeIDs.name] = 'node0' - argv[flags.generateGossipKeys.name] = true - argv[flags.generateTlsKeys.name] = true - argv[flags.clusterName.name] = TEST_CLUSTER - argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION - // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts - argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - const bootstrapResp = bootstrapTestVariables(testName, argv) - const k8 = bootstrapResp.opts.k8 - const accountManager = bootstrapResp.opts.accountManager - const configManager = bootstrapResp.opts.configManager - const nodeCmd = bootstrapResp.cmd.nodeCmd - const accountCmd = new AccountCommand(bootstrapResp.opts, testSystemAccounts) - - - describe('test update file 0.0.150', () => { - let accountId1, accountId2 - - it('should update file 0.0.150', async () => { - await accountManager.loadNodeClient(namespace) - const client = accountManager._nodeClient - - - // fetch special file - // const fileQuery = new FileContentsQuery().setFileId(fileId) - // const addressBookBytes = await fileQuery.execute(client) - // const fileHash = crypto.createHash('sha384').update(addressBookBytes).digest('hex') - - // create a file VERSION with content - // VERSION=0.2 - // Thu Jun 27 11:07:20 UTC 2024 - const versionFile = `/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/VERSION` - fs.writeFileSync(versionFile, '0.2\n') - fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) - - // bundle config.txt and VERSIO into a zip file - const zipFile = `/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/freeze.zip` - const zip = AdmZip('', {}) - zip.addLocalFile(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/VERSION`) - zip.addLocalFile(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0/config.txt`) - // get byte value of the zip file - const zipBytes = zip.toBuffer() - const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') - - accountManager.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) - // create a file upload transaction to upload file to the network - const fileId = FileId.fromString('0.0.150') - const fileTransaction = new FileUpdateTransaction() - .setFileId(fileId) - .setContents(zipBytes) - - - const fileTransactionReceipt = await fileTransaction.execute(client) - accountManager.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) - - - const prepareUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.PrepareUpgrade) - .setFileId(fileId) - .setFileHash(zipHash) - .freezeWith(client) - .execute(client) - - const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) - - accountManager.logger.debug( - `Upgrade prepared with transaction id: ${prepareUpgradeTx.transactionId.toString()}`, - prepareUpgradeReceipt.status.toString() - ) - - const fileQuery = new FileContentsQuery().setFileId(fileId) - const updateFileBinary = await fileQuery.execute(client) - // save updateFileBinary to a file - fs.writeFileSync(`/Users/jeffrey//.solo/cache/v0.51/staging/v0.51.0//update.zip`, updateFileBinary) - - }, defaultTimeout) - - }) -}) diff --git a/test/test_util.js b/test/test_util.js index 031dd11b5..77bbc2916 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -229,6 +229,7 @@ export function bootstrapNetwork (testName, argv, flags.applicationProperties.constName, flags.bootstrapProperties.constName, flags.devMode.constName, + flags.localBuildPath.constName, flags.log4j2Xml.constName, flags.settingTxt.constName ]) From febbaf0ec5c9f16ceb520308244cec73235f3384 Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Tue, 30 Jul 2024 16:11:03 +1000 Subject: [PATCH 13/56] feat: implement solo node add with dynamic address-book support Signed-off-by: Lenin Mehedy --- resources/templates/application.properties | 11 + src/commands/flags.mjs | 49 +- src/commands/node.mjs | 833 ++++++++++++--------- src/commands/prompts.mjs | 36 + src/core/account_manager.mjs | 7 +- src/core/constants.mjs | 12 +- src/core/helpers.mjs | 30 +- test/e2e/commands/account.test.mjs | 1 + test/e2e/commands/node-add.test.mjs | 52 ++ 9 files changed, 673 insertions(+), 358 deletions(-) create mode 100644 test/e2e/commands/node-add.test.mjs diff --git a/resources/templates/application.properties b/resources/templates/application.properties index fd3777b1d..d47caec3f 100644 --- a/resources/templates/application.properties +++ b/resources/templates/application.properties @@ -1,2 +1,13 @@ +ledger.id=0x01 +contracts.chainId=298 +hedera.recordStream.logPeriod=1 +balances.exportPeriodSecs=400 +files.maxSizeKb=2048 +hedera.recordStream.compressFilesOnCreation=true +balances.compressOnCreation=true +contracts.maxNumWithHapiSigsAccess=0 autoRenew.targetTypes= hedera.config.version=0 +nodes.gossipFqdnRestricted=false +netty.mode=TEST +hedera.profiles.active=TEST diff --git a/src/commands/flags.mjs b/src/commands/flags.mjs index 6955076b4..a65b8659d 100644 --- a/src/commands/flags.mjs +++ b/src/commands/flags.mjs @@ -614,6 +614,47 @@ export const amount = { } } +/** @type {CommandFlag} **/ +export const nodeID = { + constName: 'nodeId', + name: 'node-id', + definition: { + describe: 'Node id (e.g. node99)', + type: 'string' + } +} + +/** @type {CommandFlag} **/ +export const gossipEndpoints = { + constName: 'gossipEndpoints', + name: 'gossip-endpoints', + definition: { + describe: 'Comma separated gossip endpoints of the node(e.g. first one is internal, second one is external)', + type: 'string' + } +} + +/** @type {CommandFlag} **/ +export const grpcEndpoints = { + constName: 'grpcEndpoints', + name: 'grpc-endpoints', + definition: { + describe: 'Comma separated gRPC endpoints of the node (at most 8)', + type: 'string' + } +} + +/** @type {CommandFlag} **/ +export const endpointType= { + constName: 'endpointType', + name: 'endpoint-type', + definition: { + describe: 'Endpoint type (IP or FQDN)', + defaultValue: constants.ENDPOINT_TYPE_FQDN, + type: 'string' + } +} + /** @type {CommandFlag[]} **/ export const allFlags = [ accountId, @@ -664,7 +705,11 @@ export const allFlags = [ settingTxt, tlsClusterIssuerType, updateAccountKeys, - valuesFile + valuesFile, + nodeID, + gossipEndpoints, + grpcEndpoints, + endpointType, ] export const allFlagsMap = new Map(allFlags.map(f => [f.name, f])) @@ -674,7 +719,7 @@ export const nodeConfigFileFlags = new Map([ applicationProperties, bootstrapProperties, log4j2Xml, - settingTxt + settingTxt, ].map(f => [f.name, f])) export const integerFlags = new Map([replicaCount].map(f => [f.name, f])) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index a1121799b..1d4d44ed7 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -14,6 +14,7 @@ * limitations under the License. * */ +import * as x509 from '@peculiar/x509' import chalk from 'chalk' import * as fs from 'fs' import { readFile, writeFile } from 'fs/promises' @@ -22,29 +23,26 @@ import path from 'path' import { FullstackTestingError, IllegalArgumentError } from '../core/errors.mjs' import * as helpers from '../core/helpers.mjs' import { getNodeAccountMap, getNodeLogs, getTmpDir, sleep, validatePath } from '../core/helpers.mjs' -import { constants, Templates } from '../core/index.mjs' +import { constants, Templates, Zippy } from '../core/index.mjs' import { BaseCommand } from './base.mjs' import * as flags from './flags.mjs' import * as prompts from './prompts.mjs' -import AdmZip from 'adm-zip' import { AccountBalanceQuery, - AccountId, AccountUpdateTransaction, - FileContentsQuery, - FileId, FileUpdateTransaction, + FileAppendTransaction, FreezeTransaction, FreezeType, - NodeCreateTransaction, - PrivateKey, ServiceEndpoint, - Timestamp + Timestamp, + PrivateKey, + AccountId, + NodeCreateTransaction, } from '@hashgraph/sdk' import * as crypto from 'crypto' import { - GENESIS_KEY, HEDERA_NODE_DEFAULT_STAKE_AMOUNT } from '../core/constants.mjs' @@ -148,9 +146,12 @@ export class NodeCommand extends BaseCommand { flags.keyFormat, flags.log4j2Xml, flags.namespace, - flags.nodeIDs, flags.releaseTag, - flags.settingTxt + flags.settingTxt, + flags.nodeID, + flags.gossipEndpoints, + flags.grpcEndpoints, + flags.endpointType ] } @@ -258,7 +259,7 @@ export class NodeCommand extends BaseCommand { try { const output = await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['tail', '-100', logfilePath]) if (output && output.indexOf('Terminating Netty') < 0 && // make sure we are not at the beginning of a restart - (output.indexOf(`Now current platform status = ${status}`) > 0 || + (output.indexOf(`Now current platform status = ${status}`) > 0 || output.indexOf(`Platform Status Change ${status}`) > 0 || output.indexOf(`is ${status}`) > 0)) { // 'is ACTIVE' is for newer versions, first seen in v0.49.0 this.logger.debug(`Node ${nodeId} is ${status} [ attempt: ${attempt}/${maxAttempt}]`) @@ -391,9 +392,9 @@ export class NodeCommand extends BaseCommand { case constants.KEY_FORMAT_PEM: { subTasks.push({ - title: 'Backup old files', - task: () => helpers.backupOldPemKeys(nodeIds, keysDir, curDate) - } + title: 'Backup old files', + task: () => helpers.backupOldPemKeys(nodeIds, keysDir, curDate) + } ) for (const nodeId of nodeIds) { @@ -442,9 +443,9 @@ export class NodeCommand extends BaseCommand { const subTasks = [] subTasks.push({ - title: 'Backup old files', - task: () => helpers.backupOldTlsKeys(nodeIds, keysDir, curDate) - } + title: 'Backup old files', + task: () => helpers.backupOldTlsKeys(nodeIds, keysDir, curDate) + } ) for (const nodeId of nodeIds) { @@ -566,7 +567,7 @@ export class NodeCommand extends BaseCommand { for (const nodeId of ctx.config.nodeIds) { const podName = ctx.config.podNames[nodeId] subTasks.push({ - title: `Update node: ${chalk.yellow(nodeId)}`, + title: `Update node: ${chalk.yellow(nodeId)} [ platformVersion = ${ctx.config.releaseTag} ]`, task: () => platformInstaller.fetchPlatform(podName, config.releaseTag) }) @@ -581,6 +582,211 @@ export class NodeCommand extends BaseCommand { }) } + async prepareUpgradeZip (config) { + // we build a mock upgrade.zip file as we really don't need to upgrade the network + // also the platform zip file is ~80Mb in size requiring a lot of transactions since the max + // transaction size is 6Kb and in practice we need to send the file as 4Kb chunks. + // Note however that in DAB phase-2, we won't need to trigger this fake upgrade process + const zipper = new Zippy(this.logger) + const upgradeConfigDir = `${config.stagingDir}/mock-upgrade/data/config` + if (!fs.existsSync(upgradeConfigDir)) { + fs.mkdirSync(upgradeConfigDir, {recursive: true}) + } + // const fileList = ['templates/application.properties'] + // fileList.forEach(filePath => { + // const fileName = path.basename(filePath) + // fs.copyFileSync(path.join(ctx.config.stagingDir, filePath), path.join(upgradeConfigDir, fileName)) + // }) + + // bump field hedera.config.version + const fileBytes = fs.readFileSync(`${config.stagingDir}/templates/application.properties`) + const lines = fileBytes.toString().split('\n') + const newLines = [] + for (let line of lines) { + line = line.trim() + const parts = line.split('=') + if (parts.length === 2) { + if (parts[0] === 'hedera.config.version') { + let version = parseInt(parts[1]) + line = `hedera.config.version=${++version}` + } + newLines.push(line) + } + } + fs.writeFileSync(`${upgradeConfigDir}/application.properties`, newLines.join('\n')) + + return await zipper.zip(`${config.stagingDir}/mock-upgrade`, `${config.stagingDir}/mock-upgrade.zip`) + } + + async uploadUpgradeZip (config, upgradeZipFile, nodeClient) { + // get byte value of the zip file + const zipBytes = fs.readFileSync(upgradeZipFile) + const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') + this.logger.debug(`loaded upgrade zip file [ zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}, zipPath = ${upgradeZipFile}]`) + + // create a file upload transaction to upload file to the network + try { + let start = 0 + + while (start < zipBytes.length) { + const zipBytesChunk = new Uint8Array(zipBytes.subarray(start, constants.UPGRADE_FILE_CHUNK_SIZE)) + let fileTransaction = null + + if (start === 0) { + fileTransaction = new FileUpdateTransaction() + .setFileId(constants.UPGRADE_FILE_ID) + .setContents(zipBytesChunk) + } else { + fileTransaction = new FileAppendTransaction() + .setFileId(constants.UPGRADE_FILE_ID) + .setContents(zipBytesChunk) + } + const resp = await fileTransaction.execute(nodeClient) + const receipt = await resp.getReceipt(nodeClient) + this.logger.debug(`updated file ${constants.UPGRADE_FILE_ID} [chunkSize= ${zipBytesChunk.length}, txReceipt = ${receipt.toString()}]`) + + start += constants.UPGRADE_FILE_CHUNK_SIZE + } + + return zipHash + } catch (e) { + throw new FullstackTestingError(`failed to upload build.zip file: ${e.message}`, e) + } + } + + async prepareUpgradeNetworkNodes (config, upgradeZipHash, client) { + try { + const prepareUpgradeTx = await new FreezeTransaction() + .setFreezeType(FreezeType.PrepareUpgrade) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(client) + .execute(client) + + const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) + + this.logger.debug( + `sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, + prepareUpgradeReceipt.status.toString(), + ) + } catch (e) { + this.logger.error(`Error in prepare upgrade: ${e.message}`, e) + throw new FullstackTestingError(`Error in prepare upgrade: ${e.message}`, e) + } + } + + async freezeUpgradeNetworkNodes (config, upgradeZipHash, client) { + try { + const futureDate = new Date() + this.logger.debug(`Current time: ${futureDate}`) + + futureDate.setTime(futureDate.getTime() + 5000) // 5 seconds in the future + this.logger.debug(`Freeze time: ${futureDate}`) + + const freezeUpgradeTx = await new FreezeTransaction() + .setFreezeType(FreezeType.FreezeUpgrade) + .setStartTimestamp(Timestamp.fromDate(futureDate)) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(client) + .execute(client) + + this.logger.debug('send freeze-upgrade transaction') + } catch (e) { + this.logger.error(`Error in freeze upgrade: ${e.message}`, e) + throw new FullstackTestingError(`Error in freeze upgrade: ${e.message}`, e) + } + } + + startNodes (config, nodeIds, subTasks) { + for (const nodeId of nodeIds) { + const podName = config.podNames[nodeId] + subTasks.push({ + title: `Start node: ${chalk.yellow(nodeId)}`, + task: async () => { + await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/output/*`]) + await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) + } + }) + } + } + + async copyGossipKeysToStaging (config, nodeIds) { + // copy gossip keys to the staging + for (const nodeId of nodeIds) { + switch (config.keyFormat) { + case constants.KEY_FORMAT_PEM: { + const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.SIGNING_KEY_PREFIX) + await this._copyNodeKeys(signingKeyFiles, config.stagingKeysDir) + + // generate missing agreement keys + const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.AGREEMENT_KEY_PREFIX) + await this._copyNodeKeys(agreementKeyFiles, config.stagingKeysDir) + break + } + + case constants.KEY_FORMAT_PFX: { + const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) + fs.cpSync(`${config.keysDir}/${privateKeyFile}`, `${config.stagingKeysDir}/${privateKeyFile}`) + fs.cpSync(`${config.keysDir}/${constants.PUBLIC_PFX}`, `${config.stagingKeysDir}/${constants.PUBLIC_PFX}`) + break + } + + default: + throw new FullstackTestingError(`Unsupported key-format ${config.keyFormat}`) + } + } + } + + async bumpHederaConfigVersion (configTxtPath) { + const lines = (await readFile(configTxtPath, 'utf-8')).split('\n') + + for (const line of lines) { + if (line.startsWith('hedera.config.version=')) { + const version = parseInt(line.split('=')[1]) + 1 + lines[lines.indexOf(line)] = `hedera.config.version=${version}` + break + } + } + + await writeFile(configTxtPath, lines.join('\n')) + } + + prepareEndpoints (config, endpoints, defaultPort) { + const ret = /** @typedef ServiceEndpoint **/[] + for (const endpoint of endpoints) { + const parts = endpoint.split(':') + + let url = '' + let port = defaultPort + + if (parts.length === 2) { + url = parts[0].trim() + port = parts[1].trim() + } else if (parts.length === 1) { + url = parts[0] + } else { + throw new FullstackTestingError(`incorrect endpoint format. expected url:port, found ${endpoint}`) + } + + if (config.endpointType.toUpperCase() === constants.ENDPOINT_TYPE_IP) { + ret.push(new ServiceEndpoint({ + port: port, + ipAddressV4: helpers.parseIpAddressToUint8Array(url), + })) + } else { + ret.push(new ServiceEndpoint({ + port: port, + domainName: url, + })) + } + } + + return ret + } + + // List of Commands + async setup (argv) { const self = this @@ -643,17 +849,17 @@ export class NodeCommand extends BaseCommand { * @returns {string[]} */ - // create a config object for subsequent steps + // create a config object for subsequent steps const config = /** @type {NodeSetupConfigClass} **/ this.getConfig(NodeCommand.SETUP_CONFIGS_NAME, NodeCommand.SETUP_FLAGS_LIST, - [ - 'buildZipFile', - 'curDate', - 'keysDir', - 'nodeIds', - 'releasePrefix', - 'stagingDir', - 'stagingKeysDir' - ]) + [ + 'buildZipFile', + 'curDate', + 'keysDir', + 'nodeIds', + 'releasePrefix', + 'stagingDir', + 'stagingKeysDir' + ]) config.nodeIds = helpers.parseNodeIds(config.nodeIDs) config.curDate = new Date() @@ -1050,13 +1256,13 @@ export class NodeCommand extends BaseCommand { * @returns {string[]} */ - // create a config object for subsequent steps + // create a config object for subsequent steps const config = /** @type {NodeKeysConfigClass} **/ this.getConfig(NodeCommand.SETUP_CONFIGS_NAME, NodeCommand.SETUP_FLAGS_LIST, - [ - 'curDate', - 'keysDir', - 'nodeIds' - ]) + [ + 'curDate', + 'keysDir', + 'nodeIds' + ]) config.curDate = new Date() config.nodeIds = helpers.parseNodeIds(config.nodeIDs) @@ -1191,32 +1397,32 @@ export class NodeCommand extends BaseCommand { { title: 'Dump network nodes saved state', task: - async (ctx, task) => { - const subTasks = [] - for (const nodeId of ctx.config.nodeIds) { - const podName = ctx.config.podNames[nodeId] - subTasks.push({ - title: `Node: ${chalk.yellow(nodeId)}`, - task: async () => - await self.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/*`]) - }) - } - - // set up the sub-tasks - return task.newListr(subTasks, { - concurrent: true, - rendererOptions: { - collapseSubtasks: false - } + async (ctx, task) => { + const subTasks = [] + for (const nodeId of ctx.config.nodeIds) { + const podName = ctx.config.podNames[nodeId] + subTasks.push({ + title: `Node: ${chalk.yellow(nodeId)}`, + task: async () => + await self.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/*`]) }) } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: true, + rendererOptions: { + collapseSubtasks: false + } + }) + } }, { title: 'Fetch platform software into network nodes', task: - async (ctx, task) => { - return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) - } + async (ctx, task) => { + return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) + } }, { title: 'Setup network nodes', @@ -1396,7 +1602,10 @@ export class NodeCommand extends BaseCommand { flags.force, flags.fstChartVersion, flags.log4j2Xml, - flags.settingTxt + flags.settingTxt, + flags.gossipEndpoints, + flags.grpcEndpoints, + flags.endpointType, ]) await prompts.execute(task, self.configManager, NodeCommand.ADD_FLAGS_LIST) @@ -1418,9 +1627,12 @@ export class NodeCommand extends BaseCommand { * @property {string} keyFormat * @property {string} log4j2Xml * @property {string} namespace - * @property {string} nodeIDs * @property {string} releaseTag * @property {string} settingTxt + * @property {string} nodeId + * @property {string} gossipEndpoints + * @property {string} grpcEndpoints + * @property {string} endpointType * -- extra args -- * @property {string[]} allNodeIds * @property {string} buildZipFile @@ -1441,26 +1653,29 @@ export class NodeCommand extends BaseCommand { * @returns {string[]} */ - // create a config object for subsequent steps + // create a config object for subsequent steps const config = /** @type {NodeAddConfigClass} **/ this.getConfig(NodeCommand.ADD_CONFIGS_NAME, NodeCommand.ADD_FLAGS_LIST, - [ - 'allNodeIds', - 'buildZipFile', - 'curDate', - 'existingNodeIds', - 'keysDir', - 'nodeIds', - 'podNames', - 'releasePrefix', - 'serviceMap', - 'stagingDir', - 'stagingKeysDir' - ]) + [ + 'allNodeIds', + 'buildZipFile', + 'curDate', + 'existingNodeIds', + 'keysDir', + 'nodeIds', + 'podNames', + 'releasePrefix', + 'serviceMap', + 'stagingDir', + 'stagingKeysDir' + ]) - config.nodeIds = helpers.parseNodeIds(config.nodeIDs) config.curDate = new Date() config.existingNodeIds = [] + if (config.keyFormat !== constants.KEY_FORMAT_PEM) { + throw new FullstackTestingError('key type cannot be PFX') + } + await self.initializeSetup(config, self.configManager, self.k8) // set config in the context for later tasks to use @@ -1488,10 +1703,11 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Deploy new network node', - task: async (ctx, task) => { + title: 'Determine new node account number', + task: (ctx, task) => { const values = { hedera: { nodes: [] } } let maxNum + for (/** @type {NetworkNodeServices} **/ const networkNodeServices of ctx.config.serviceMap.values()) { values.hedera.nodes.push({ accountId: networkNodeServices.accountId, @@ -1501,22 +1717,198 @@ export class NodeCommand extends BaseCommand { ? maxNum : AccountId.fromString(networkNodeServices.accountId).num } - for (const nodeId of ctx.config.nodeIds) { - const accountId = AccountId.fromString(values.hedera.nodes[0].accountId) - accountId.num = ++maxNum - values.hedera.nodes.push({ - accountId: accountId.toString(), - name: nodeId - }) + + ctx.maxNum = maxNum + ctx.newNode = { + accountId: `${constants.HEDERA_NODE_ACCOUNT_ID_START.realm}.${constants.HEDERA_NODE_ACCOUNT_ID_START.shard}.${++maxNum}`, + name: ctx.config.nodeId } + } + }, + { + title: 'Generate Gossip key', + task: async (ctx, parentTask) => { + const config = ctx.config + const subTasks = self._nodeGossipKeysTaskList(config.keyFormat, [config.nodeId], config.keysDir, config.curDate, config.allNodeIds) + // set up the sub-tasks + return parentTask.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false, + timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION + } + }) + }, + skip: (ctx, _) => !ctx.config.generateGossipKeys + }, + { + title: 'Generate gRPC TLS key', + task: async (ctx, parentTask) => { + const config = ctx.config + const subTasks = self._nodeTlsKeyTaskList([config.nodeId], config.keysDir, config.curDate) + // set up the sub-tasks + return parentTask.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false, + timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION + } + }) + }, + skip: (ctx, _) => !ctx.config.generateTlsKeys + }, + { + title: 'Load signing key certificate', + task: async (ctx, task) => { + const signingCertFile = Templates.renderGossipPemPublicKeyFile(constants.SIGNING_KEY_PREFIX, ctx.config.nodeId) + const signingCertFullPath = `${ctx.config.keysDir}/${signingCertFile}` + const signingCertPem = fs.readFileSync(signingCertFullPath).toString() + const decodedDers = x509.PemConverter.decode(signingCertPem) + if (!decodedDers || decodedDers.length === 0) { + throw new FullstackTestingError('unable to decode public key: ' + signingCertFile) + } + ctx.signingCertDer = new Uint8Array(decodedDers[0]) + } + }, + { + title: 'Compute mTLS certificate hash', + task: async (ctx, task) => { + const tlsCertFile = Templates.renderTLSPemPublicKeyFile(ctx.config.nodeId) + const tlsCertFullPath = `${ctx.config.keysDir}/${tlsCertFile}` + const tlsCertPem = fs.readFileSync(tlsCertFullPath).toString() + const tlsCertDers = x509.PemConverter.decode(tlsCertPem) + if (!tlsCertDers || tlsCertDers.length === 0) { + throw new FullstackTestingError('unable to decode tls cert: ' + tlsCertFullPath) + } + const tlsCertDer = new Uint8Array(tlsCertDers[0]) + ctx.tlsCertHash = crypto.createHash('sha384').update(tlsCertDer).digest() + } + }, + { + title: 'Prepare gossip endpoints', + task: (ctx, task) => { + let endpoints = [] + if (!ctx.config.gossipEndpoints) { + if (ctx.config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { + throw new FullstackTestingError(`--gossip-endpoints must be set if --endpoint-type is: ${constants.ENDPOINT_TYPE_IP}`) + } - let valuesArg = '' - let index = 0 - for (const node of values.hedera.nodes) { - valuesArg += ` --set "hedera.nodes[${index}].accountId=${node.accountId}" --set "hedera.nodes[${index}].name=${node.name}"` - index++ + endpoints = [ + `${Templates.renderFullyQualifiedNetworkPodName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT}`, + `${Templates.renderFullyQualifiedNetworkSvcName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` + ] + } else { + endpoints = helpers.splitFlagInput(ctx.config.gossipEndpoints) } + ctx.gossipEndpoints = this.prepareEndpoints(ctx.config, endpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT) + } + }, + { + title: 'Prepare grpc service endpoints', + task: (ctx, task) => { + let endpoints = [] + + if (!ctx.config.grpcServiceEndpoints) { + if (ctx.config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { + throw new FullstackTestingError(`--grpc-endpoints must be set if --endpoint-type is: ${constants.ENDPOINT_TYPE_IP}`) + } + + endpoints = [ + `${Templates.renderFullyQualifiedNetworkSvcName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` + ] + } else { + endpoints = helpers.splitFlagInput(ctx.config.grpcServiceEndpoints) + } + + ctx.grpcServiceEndpoints = this.prepareEndpoints(ctx.config, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) + } + }, + { + title: 'Load node admin key', + task: async (ctx, task) => { + ctx.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) + // await accountManager.loadNodeClient(config.namespace) + // const keys = await accountManager.getAccountKeys(constants.COUNCIL_ACCOUNT_ID) + // if (keys && keys.length > 0 && keys[0].toString() !== constants.GENESIS_KEY) { + // adminKey = PrivateKey.fromStringED25519(keys[0].toString()) + // } + } + }, + { + title: 'Send node create transaction', + task: async (ctx, task) => { + try { + const nodeClient = this.accountManager._nodeClient + const nodeCreateTx = await new NodeCreateTransaction() + .setAccountId(ctx.newNode.accountId) + .setGossipEndpoints(ctx.gossipEndpoints) + .setServiceEndpoints(ctx.grpcServiceEndpoints) + .setGossipCaCertificate(ctx.signingCertDer) + .setCertificateHash(ctx.tlsCertHash) + .setAdminKey(ctx.adminKey.publicKey) + .freezeWith(nodeClient) + const signedTx = await nodeCreateTx.sign(ctx.adminKey) + const txResp = await signedTx.execute(nodeClient) + const nodeCreateReceipt = await txResp.getReceipt(nodeClient) + this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) + } catch (e) { + throw new FullstackTestingError(`Error adding node to network: ${e.message}`, e) + } + } + }, + { + title: 'Prepare upgrade zip file for node upgrade process', + task: async (ctx, task) => { + ctx.nodeClient = await this.accountManager.loadNodeClient(ctx.config.namespace) + ctx.upgradeZipFile = await this.prepareUpgradeZip(ctx.config) + ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.config, ctx.upgradeZipFile, ctx.nodeClient) + } + }, + { + title: 'Send prepare upgrade transaction', + task: async (ctx, task) => { + await this.prepareUpgradeNetworkNodes(ctx.config, ctx.upgradeZipHash, ctx.nodeClient) + } + }, + { + title: 'Send freeze upgrade transaction', + task: async (ctx, task) => { + await this.freezeUpgradeNetworkNodes(ctx.config, ctx.upgradeZipHash, ctx.nodeClient) + } + }, + { + title: 'Check network nodes are frozen', + task: (ctx, task) => { + const subTasks = [] + for (const nodeId of ctx.config.existingNodeIds) { + subTasks.push({ + title: `Check node: ${chalk.yellow(nodeId)}`, + task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') + }) + } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } + }) + } + }, + { + title: 'Prepare latest state for new node with updated config.txt', + task: async (ctx, task) => { + + } + }, + { + title: 'Deploy new network node', + task: async (ctx, task) => { + let index = ctx.existingNodeIds.length + const valuesArg = ` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}" --set "hedera.nodes[${index}].name=${ctx.newNode.name}"` + await self.chartManager.upgrade( ctx.config.namespace, constants.FULLSTACK_DEPLOYMENT_CHART, @@ -1524,7 +1916,8 @@ export class NodeCommand extends BaseCommand { valuesArg, ctx.config.fstChartVersion ) - ctx.config.allNodeIds = [...ctx.config.existingNodeIds, ...ctx.config.nodeIds] + + ctx.config.allNodeIds = [...ctx.config.existingNodeIds, ctx.config.nodeId] } }, { @@ -1549,38 +1942,6 @@ export class NodeCommand extends BaseCommand { }) } }, - { - title: 'Generate Gossip keys', - task: async (ctx, parentTask) => { - const config = ctx.config - const subTasks = self._nodeGossipKeysTaskList(config.keyFormat, config.nodeIds, config.keysDir, config.curDate, config.allNodeIds) - // set up the sub-tasks - return parentTask.newListr(subTasks, { - concurrent: false, - rendererOptions: { - collapseSubtasks: false, - timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION - } - }) - }, - skip: (ctx, _) => !ctx.config.generateGossipKeys - }, - { - title: 'Generate gRPC TLS keys', - task: async (ctx, parentTask) => { - const config = ctx.config - const subTasks = self._nodeTlsKeyTaskList(config.nodeIds, config.keysDir, config.curDate) - // set up the sub-tasks - return parentTask.newListr(subTasks, { - concurrent: false, - rendererOptions: { - collapseSubtasks: false, - timer: constants.LISTR_DEFAULT_RENDERER_TIMER_OPTION - } - }) - }, - skip: (ctx, _) => !ctx.config.generateTlsKeys - }, { title: 'Prepare staging directory', task: async (ctx, parentTask) => { @@ -1642,86 +2003,14 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Fetch platform software into network nodes', + title: 'Fetch platform software into new network node', task: - async (ctx, task) => { - return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) - } - }, - { - title: 'Add new node(s) to network', - task: - async (ctx, task) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config - const networkNodeServicesMap = await this.accountManager.getNodeServiceMap(config.namespace) - const client = this.accountManager._nodeClient - - for (const nodeId of config.nodeIds) { - const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) - const privateKeyFullPath = `${config.stagingKeysDir}/${privateKeyFile}` - const privateKeyAsBuffer = fs.readFileSync(privateKeyFullPath) - const privateKeyAsUint8Array = new Uint8Array(privateKeyAsBuffer) - - const networkNodeServices = networkNodeServicesMap.get(nodeId) - const gossipEndpoints = /** @type {ServiceEndpoint[]} **/ [] - gossipEndpoints.push(new ServiceEndpoint({ - port: networkNodeServices.nodeServiceGossipPort, - domainName: Templates.renderFullyQualifiedNetworkSvcName(config.namespace, nodeId) - })) - const serviceEndpoints = /** @type {ServiceEndpoint[]} **/ [] - serviceEndpoints.push(new ServiceEndpoint({ - port: networkNodeServices.nodeServiceGrpcPort, - domainName: Templates.renderFullyQualifiedNetworkSvcName(config.namespace, nodeId) - })) - try { - const nodeCreateTx = await new NodeCreateTransaction() - .setAccountId(networkNodeServices.accountId) - .setGossipEndpoints(gossipEndpoints) - .setServiceEndpoints(serviceEndpoints) - .setGossipCaCertificate(privateKeyAsUint8Array) - // TODO create Issue to add this later: .setCertificateHash() - // TODO if any k8s secrets exists for account IDs, we know that `solo account init` has been ran, - // and we need to generate a new key for the new account and store it in the k8s secrets - .setAdminKey(PrivateKey.fromStringED25519(GENESIS_KEY)) - .execute(client) - const nodeCreateReceipt = await nodeCreateTx.getReceipt(client) - this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) - } catch (e) { - throw new FullstackTestingError(`Error adding node to network: ${e.message}`, e) - } - } - } - }, - { - title: 'Freeze network nodes', - task: - async (ctx, task) => { - await this.freezeNetworkNodes(ctx.config) - await this.logs(argv) - } - }, - { - title: 'Check nodes are frozen', - task: (ctx, task) => { - const subTasks = [] - for (const nodeId of ctx.config.existingNodeIds) { - subTasks.push({ - title: `Check node: ${chalk.yellow(nodeId)}`, - task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') - }) + async (ctx, task) => { + return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) } - - // set up the sub-tasks - return task.newListr(subTasks, { - concurrent: false, - rendererOptions: { - collapseSubtasks: false - } - }) - } }, { - title: 'Setup network nodes', + title: 'Setup new network node', task: async (ctx, parentTask) => { const config = ctx.config @@ -1746,12 +2035,12 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Starting nodes', + title: 'Start network nodes', task: (ctx, task) => { const subTasks = [] // TODO: only start new nodes? // self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) - self.startNodes(ctx.config, ctx.config.nodeIds, subTasks) + self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) // TODO: stake new node? @@ -1766,7 +2055,7 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Check nodes are ACTIVE', + title: 'Check all nodes are ACTIVE', task: (ctx, task) => { const subTasks = [] for (const nodeId of ctx.config.allNodeIds) { @@ -1786,7 +2075,7 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Check node proxies are ACTIVE', + title: 'Check all node proxies are ACTIVE', // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind task: async (ctx, task) => { @@ -1809,7 +2098,6 @@ export class NodeCommand extends BaseCommand { }) } }, - // TODO: add stake to the new node { title: 'Finalize', task: (ctx, _) => { @@ -1835,136 +2123,7 @@ export class NodeCommand extends BaseCommand { return true } - async freezeNetworkNodes (config) { - await this.accountManager.loadNodeClient(config.namespace) - const client = this.accountManager._nodeClient - - try { - const fileId = FileId.fromString('0.0.150') - - // bundle config.txt and VERSION into a zip file - const versionFile = `${config.stagingDir}/VERSION` - fs.writeFileSync(versionFile, '0.2\n') - fs.appendFileSync(versionFile, `${new Date().toUTCString()}\n`) - - // bundle config.txt and VERSIO into a zip file - const zipFile = `${config.stagingDir}/freeze.zip` - const zip = AdmZip('', {}) - zip.addLocalFile(`${config.stagingDir}/VERSION`) - zip.addLocalFile(`${config.stagingDir}/config.txt`) - // get byte value of the zip file - const zipBytes = zip.toBuffer() - const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') - // save zip file to local disk - fs.writeFileSync(zipFile, zipBytes) - - this.logger.debug(`zipHash = ${zipHash} zipBytes.length = ${zipBytes.length}`) - // create a file upload transaction to upload file to the network - const fileTransaction = new FileUpdateTransaction() - .setFileId(fileId) - .setContents(zipBytes) - - const fileTransactionReceipt = await fileTransaction.execute(client) - this.logger.debug(`fileTransactionReceipt = ${fileTransactionReceipt.toString()}`) - - const prepareUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.PrepareUpgrade) - .setFileId(fileId) - .setFileHash(zipHash) - .freezeWith(client) - .execute(client) - - const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) - - this.logger.debug( - `Upgrade prepared with transaction id: ${prepareUpgradeTx.transactionId.toString()}`, - prepareUpgradeReceipt.status.toString() - ) - - // TODO: split? now we need to get the config.txt? - - const fileQuery = new FileContentsQuery().setFileId(fileId) - const updateFileBinary = await fileQuery.execute(client) - // save updateFileBinary to a file - fs.writeFileSync(`${config.stagingDir}/update2.zip`, updateFileBinary) - - await sleep(5000) - - const futureDate = new Date() - this.logger.debug(`Current time: ${futureDate}`) - - futureDate.setTime(futureDate.getTime() + 20000) // 20 seconds in the future - this.logger.debug(`Freeze time: ${futureDate}`) - - const freezeUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.FreezeUpgrade) - .setStartTimestamp(Timestamp.fromDate(futureDate)) - .setFileId(fileId) - .setFileHash(zipHash) - .freezeWith(client) - .execute(client) - - await sleep(5000) - - const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(client) - - this.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, - freezeUpgradeReceipt.status.toString()) - - // overwrite config.text fro all nodes except ctx.config.nodeIds - for (const nodeId of config.allNodeIds) { - if (!config.nodeIds.includes(nodeId)) { - const podName = config.podNames[nodeId] - this.logger.info(`copy config.txt for node = ${nodeId}`) - await this.k8.copyTo(podName, constants.ROOT_CONTAINER, `${config.stagingDir}/config.txt`, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/`) - } - } - } catch (e) { - this.logger.error(`Error in freeze upgrade: ${e.message}`, e) - throw new FullstackTestingError(`Error in freeze upgrade: ${e.message}`, e) - } - } - - startNodes (config, nodeIds, subTasks) { - for (const nodeId of nodeIds) { - const podName = config.podNames[nodeId] - subTasks.push({ - title: `Start node: ${chalk.yellow(nodeId)}`, - task: async () => { - await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/output/*`]) - await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) - } - }) - } - } - - async copyGossipKeysToStaging (config, nodeIds) { - // copy gossip keys to the staging - for (const nodeId of nodeIds) { - switch (config.keyFormat) { - case constants.KEY_FORMAT_PEM: { - const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.SIGNING_KEY_PREFIX) - await this._copyNodeKeys(signingKeyFiles, config.stagingKeysDir) - - // generate missing agreement keys - const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.AGREEMENT_KEY_PREFIX) - await this._copyNodeKeys(agreementKeyFiles, config.stagingKeysDir) - break - } - - case constants.KEY_FORMAT_PFX: { - const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) - fs.cpSync(`${config.keysDir}/${privateKeyFile}`, `${config.stagingKeysDir}/${privateKeyFile}`) - fs.cpSync(`${config.keysDir}/${constants.PUBLIC_PFX}`, `${config.stagingKeysDir}/${constants.PUBLIC_PFX}`) - break - } - - default: - throw new FullstackTestingError(`Unsupported key-format ${config.keyFormat}`) - } - } - } - + // Command Definition /** * Return Yargs command definition for 'node' command * @param nodeCmd an instance of NodeCommand @@ -2109,18 +2268,4 @@ export class NodeCommand extends BaseCommand { } } } - - async bumpHederaConfigVersion (configTxtPath) { - const lines = (await readFile(configTxtPath, 'utf-8')).split('\n') - - for (const line of lines) { - if (line.startsWith('hedera.config.version=')) { - const version = parseInt(line.split('=')[1]) + 1 - lines[lines.indexOf(line)] = `hedera.config.version=${version}` - break - } - } - - await writeFile(configTxtPath, lines.join('\n')) - } } diff --git a/src/commands/prompts.mjs b/src/commands/prompts.mjs index bbc6ade13..096dc5d5c 100644 --- a/src/commands/prompts.mjs +++ b/src/commands/prompts.mjs @@ -412,6 +412,38 @@ export async function promptAmount (task, input) { flags.amount.name) } +export async function promptNewNodeId(task, input) { + return await promptText(task, input, + flags.nodeID.definition.defaultValue, + 'Enter the new node id: ', + null, + flags.nodeID.name) +} + +export async function promptGossipEndpoints(task, input) { + return await promptText(task, input, + flags.gossipEndpoints.definition.defaultValue, + 'Enter the gossip endpoints(comma separated): ', + null, + flags.gossipEndpoints.name) +} + +export async function promptGrpcEndpoints(task, input) { + return await promptText(task, input, + flags.grpcEndpoints.definition.defaultValue, + 'Enter the gRPC endpoints(comma separated): ', + null, + flags.grpcEndpoints.name) +} + +export async function promptEndpointType(task, input) { + return await promptText(task, input, + flags.endpointType.definition.defaultValue, + 'Enter the endpoint type(IP or FQDN): ', + null, + flags.endpointType.name) +} + export function getPromptMap () { return new Map() .set(flags.accountId.name, promptAccountId) @@ -449,6 +481,10 @@ export function getPromptMap () { .set(flags.tlsClusterIssuerType.name, promptTlsClusterIssuerType) .set(flags.updateAccountKeys.name, promptUpdateAccountKeys) .set(flags.valuesFile.name, promptValuesFile) + .set(flags.nodeID.name, promptNewNodeId) + .set(flags.gossipEndpoints.name, promptGossipEndpoints) + .set(flags.grpcEndpoints.name, promptGrpcEndpoints) + .set(flags.endpointType.name, promptEndpointType) } // build the prompt registry diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index 16cd4fadb..6d02c5edc 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -167,6 +167,8 @@ export class AccountManager { this._nodeClient = await this._getNodeClient(namespace, networkNodeServicesMap, treasuryAccountInfo.accountId, treasuryAccountInfo.privateKey) } + + return this._nodeClient } /** @@ -478,12 +480,9 @@ export class AccountManager { async getAccountKeys (accountId) { const accountInfo = await this.accountInfoQuery(accountId) - let keys + let keys = [] if (accountInfo.key instanceof KeyList) { keys = accountInfo.key.toArray() - } else { - keys = [] - keys.push(accountInfo.key) } return keys diff --git a/src/core/constants.mjs b/src/core/constants.mjs index 2ea7b94c7..b36dfef35 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { AccountId } from '@hashgraph/sdk' +import { AccountId, FileId } from '@hashgraph/sdk' import { color, PRESET_TIMER } from 'listr2' import { dirname, normalize } from 'path' import { fileURLToPath } from 'url' @@ -68,6 +68,7 @@ export const OPERATOR_ID = process.env.SOLO_OPERATOR_ID || '0.0.2' export const OPERATOR_KEY = process.env.SOLO_OPERATOR_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' export const OPERATOR_PUBLIC_KEY = process.env.SOLO_OPERATOR_PUBLIC_KEY || '302a300506032b65700321000aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92' export const TREASURY_ACCOUNT_ID = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.2` +export const COUNCIL_ACCOUNT_ID = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.55` export const GENESIS_KEY = process.env.GENESIS_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' export const SYSTEM_ACCOUNTS = [[3, 100], [200, 349], [400, 750], [900, 1000]] // do account 0.0.2 last and outside the loop export const TREASURY_ACCOUNT = 2 @@ -135,3 +136,12 @@ export const NODE_CLIENT_MAX_ATTEMPTS = process.env.NODE_CLIENT_MAX_ATTEMPTS || export const NODE_CLIENT_MIN_BACKOFF = process.env.NODE_CLIENT_MIN_BACKOFF || 1000 export const NODE_CLIENT_MAX_BACKOFF = process.env.NODE_CLIENT_MAX_BACKOFF || 1000 export const NODE_CLIENT_REQUEST_TIMEOUT = process.env.NODE_CLIENT_REQUEST_TIMEOUT || 120000 + +// ---- New Node Related ---- +export const ENDPOINT_TYPE_IP = 'IP' +export const ENDPOINT_TYPE_FQDN = 'FQDN' + +// file-id must be between 0.0.150 and 0.0.159 +// file must be uploaded using FileUpdateTransaction in maximum of 5Kb chunks +export const UPGRADE_FILE_ID = FileId.fromString('0.0.150') +export const UPGRADE_FILE_CHUNK_SIZE = 1024 * 5 // 5Kb diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 13aa0ad24..8db28f037 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -36,19 +36,23 @@ export function sleep (ms) { } export function parseNodeIds (input) { + return splitFlagInput(input, ',') +} + +export function splitFlagInput(input, separator = ',') { if (typeof input === 'string') { - const nodeIds = [] - input.split(',').forEach(item => { - const nodeId = item.trim() - if (nodeId) { - nodeIds.push(nodeId) + const items = [] + input.split(separator).forEach(s => { + const item = s.trim() + if (s) { + items.push(item) } }) - return nodeIds + return items } - throw new FullstackTestingError('node IDs is not a comma separated string') + throw new FullstackTestingError('input is not a comma separated string') } export function cloneArray (arr) { @@ -236,3 +240,15 @@ export function getNodeAccountMap (nodeIDs) { }) return accountMap } + +export function parseIpAddressToUint8Array(ipAddress) { + const parts = ipAddress.split('.'); + const uint8Array = new Uint8Array(4); + + for (let i = 0; i < 4; i++) { + uint8Array[i] = parseInt(parts[i], 10); + } + + return uint8Array; +} + diff --git a/test/e2e/commands/account.test.mjs b/test/e2e/commands/account.test.mjs index 20b92fdd4..972a6491e 100644 --- a/test/e2e/commands/account.test.mjs +++ b/test/e2e/commands/account.test.mjs @@ -106,6 +106,7 @@ describe('AccountCommand', () => { nodeCmd.logger.info(`Fetching account keys: accountId ${accountId}`) const keys = await accountManager.getAccountKeys(accountId) nodeCmd.logger.info(`Fetched account keys: accountId ${accountId}`) + expect(keys.length).not.toEqual(0) expect(keys[0].toString()).not.toEqual(genesisKey.toString()) }, 20000) } diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs new file mode 100644 index 000000000..659432353 --- /dev/null +++ b/test/e2e/commands/node-add.test.mjs @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { describe, it } from '@jest/globals' +import { bootstrapProperties } from '../../../src/commands/flags.mjs' +import { flags } from '../../../src/commands/index.mjs' +import { constants } from '../../../src/core/index.mjs' +import { bootstrapNetwork, bootstrapTestVariables, getDefaultArgv } from '../../test_util.js' + +describe('Node add', () => { + const TEST_NAMESPACE = 'node-add' + const argv = getDefaultArgv() + argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM + argv[flags.nodeIDs.name] = 'node0,node1,node2' + argv[flags.generateGossipKeys.name] = true + argv[flags.generateTlsKeys.name] = true + // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts + argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined + argv[flags.releaseTag] = 'v0.53.0-develop.xd2fbe98' + + argv[flags.namespace.name] = TEST_NAMESPACE + // const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) + const bootstrapResp = bootstrapTestVariables(TEST_NAMESPACE, argv) + const nodeCmd = bootstrapResp.cmd.nodeCmd + + // afterAll(async () => { + // await getNodeLogs(hederaK8, TEST_NAMESPACE) + // await hederaK8.deleteNamespace(TEST_NAMESPACE) + // }, 120000) + + it('should add a new node to the network successfully', async () => { + argv[flags.nodeID.name] = 'lenin-1' + argv[flags.generateGossipKeys.name] = true + argv[flags.generateTlsKeys.name] = true + argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM + + await nodeCmd.add(argv) + }) +}) From 2df92a9cad537e6489b916407cd68381c4c06fc4 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 30 Jul 2024 15:40:17 +0100 Subject: [PATCH 14/56] lint fixes Signed-off-by: Jeromy Cannon --- src/commands/flags.mjs | 14 +++++++------- src/commands/prompts.mjs | 8 ++++---- src/core/helpers.mjs | 13 ++++++------- test/e2e/commands/node-add.test.mjs | 3 +-- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/commands/flags.mjs b/src/commands/flags.mjs index a65b8659d..8c8c6b361 100644 --- a/src/commands/flags.mjs +++ b/src/commands/flags.mjs @@ -645,7 +645,7 @@ export const grpcEndpoints = { } /** @type {CommandFlag} **/ -export const endpointType= { +export const endpointType = { constName: 'endpointType', name: 'endpoint-type', definition: { @@ -683,15 +683,19 @@ export const allFlags = [ ecdsaPrivateKey, enableHederaExplorerTls, enablePrometheusSvcMonitor, + endpointType, fstChartVersion, generateGossipKeys, generateTlsKeys, + gossipEndpoints, + grpcEndpoints, hederaExplorerTlsHostName, hederaExplorerTlsLoadBalancerIp, keyFormat, localBuildPath, log4j2Xml, namespace, + nodeID, nodeIDs, operatorId, operatorKey, @@ -705,11 +709,7 @@ export const allFlags = [ settingTxt, tlsClusterIssuerType, updateAccountKeys, - valuesFile, - nodeID, - gossipEndpoints, - grpcEndpoints, - endpointType, + valuesFile ] export const allFlagsMap = new Map(allFlags.map(f => [f.name, f])) @@ -719,7 +719,7 @@ export const nodeConfigFileFlags = new Map([ applicationProperties, bootstrapProperties, log4j2Xml, - settingTxt, + settingTxt ].map(f => [f.name, f])) export const integerFlags = new Map([replicaCount].map(f => [f.name, f])) diff --git a/src/commands/prompts.mjs b/src/commands/prompts.mjs index 096dc5d5c..81c8f4a8e 100644 --- a/src/commands/prompts.mjs +++ b/src/commands/prompts.mjs @@ -412,7 +412,7 @@ export async function promptAmount (task, input) { flags.amount.name) } -export async function promptNewNodeId(task, input) { +export async function promptNewNodeId (task, input) { return await promptText(task, input, flags.nodeID.definition.defaultValue, 'Enter the new node id: ', @@ -420,7 +420,7 @@ export async function promptNewNodeId(task, input) { flags.nodeID.name) } -export async function promptGossipEndpoints(task, input) { +export async function promptGossipEndpoints (task, input) { return await promptText(task, input, flags.gossipEndpoints.definition.defaultValue, 'Enter the gossip endpoints(comma separated): ', @@ -428,7 +428,7 @@ export async function promptGossipEndpoints(task, input) { flags.gossipEndpoints.name) } -export async function promptGrpcEndpoints(task, input) { +export async function promptGrpcEndpoints (task, input) { return await promptText(task, input, flags.grpcEndpoints.definition.defaultValue, 'Enter the gRPC endpoints(comma separated): ', @@ -436,7 +436,7 @@ export async function promptGrpcEndpoints(task, input) { flags.grpcEndpoints.name) } -export async function promptEndpointType(task, input) { +export async function promptEndpointType (task, input) { return await promptText(task, input, flags.endpointType.definition.defaultValue, 'Enter the endpoint type(IP or FQDN): ', diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 8db28f037..02a160635 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -39,7 +39,7 @@ export function parseNodeIds (input) { return splitFlagInput(input, ',') } -export function splitFlagInput(input, separator = ',') { +export function splitFlagInput (input, separator = ',') { if (typeof input === 'string') { const items = [] input.split(separator).forEach(s => { @@ -241,14 +241,13 @@ export function getNodeAccountMap (nodeIDs) { return accountMap } -export function parseIpAddressToUint8Array(ipAddress) { - const parts = ipAddress.split('.'); - const uint8Array = new Uint8Array(4); +export function parseIpAddressToUint8Array (ipAddress) { + const parts = ipAddress.split('.') + const uint8Array = new Uint8Array(4) for (let i = 0; i < 4; i++) { - uint8Array[i] = parseInt(parts[i], 10); + uint8Array[i] = parseInt(parts[i], 10) } - return uint8Array; + return uint8Array } - diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 659432353..5a0bd3f37 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -15,10 +15,9 @@ * */ import { describe, it } from '@jest/globals' -import { bootstrapProperties } from '../../../src/commands/flags.mjs' import { flags } from '../../../src/commands/index.mjs' import { constants } from '../../../src/core/index.mjs' -import { bootstrapNetwork, bootstrapTestVariables, getDefaultArgv } from '../../test_util.js' +import { bootstrapTestVariables, getDefaultArgv } from '../../test_util.js' describe('Node add', () => { const TEST_NAMESPACE = 'node-add' From e5522fa3757b5c779d6299d29d8511f948b5af4c Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 30 Jul 2024 23:21:00 +0100 Subject: [PATCH 15/56] bumped version Signed-off-by: Jeromy Cannon --- test/test_util.js | 2 +- version.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_util.js b/test/test_util.js index 77bbc2916..f985bc631 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -46,7 +46,7 @@ import { AccountBalanceQuery } from '@hashgraph/sdk' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.0-develop.xd2fbe98' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.0-release-0.53.x7be05aa' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index 7287076e5..c28fc6317 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.28.2' -export const HEDERA_PLATFORM_VERSION = 'v0.53.0-develop.xd2fbe98' +export const HEDERA_PLATFORM_VERSION = 'v0.53.0-release-0.53.x7be05aa' From b417e9bbab555f8f44e805fb45e0b0054150a0ff Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 30 Jul 2024 23:21:41 +0100 Subject: [PATCH 16/56] FREEZE_ADMIN_ACCOUNT Signed-off-by: Jeromy Cannon --- src/core/constants.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/constants.mjs b/src/core/constants.mjs index 02656a8ff..6bd919376 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -67,7 +67,7 @@ export const DEFAULT_CHART_REPO = new Map() export const OPERATOR_ID = process.env.SOLO_OPERATOR_ID || '0.0.2' export const OPERATOR_KEY = process.env.SOLO_OPERATOR_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' export const OPERATOR_PUBLIC_KEY = process.env.SOLO_OPERATOR_PUBLIC_KEY || '302a300506032b65700321000aa8e21064c61eab86e2a9c164565b4e7a9a4146106e0a6cd03a8c395a110e92' -export const FREEZE_ADMIN_ACCOUNT = process.env.FREEZE_ADMIN_ACCOUNT || '0.0.58' +export const FREEZE_ADMIN_ACCOUNT = process.env.FREEZE_ADMIN_ACCOUNT || `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.58` export const TREASURY_ACCOUNT_ID = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.2` export const COUNCIL_ACCOUNT_ID = `${HEDERA_NODE_ACCOUNT_ID_START.realm}.${HEDERA_NODE_ACCOUNT_ID_START.shard}.55` export const GENESIS_KEY = process.env.GENESIS_KEY || '302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137' From b46d459e1775568d5a0970f14172f92f5b9b2714 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 30 Jul 2024 23:22:09 +0100 Subject: [PATCH 17/56] saving progress, network doesn't come back up, yet Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 152 +++++++++++++++------------- src/core/account_manager.mjs | 23 ++--- src/core/helpers.mjs | 5 + src/core/k8.mjs | 11 +- test/e2e/commands/node-add.test.mjs | 25 +++-- 5 files changed, 120 insertions(+), 96 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 4bad179ec..10a3dba35 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -44,7 +44,7 @@ import { import * as crypto from 'crypto' import { FREEZE_ADMIN_ACCOUNT, - HEDERA_NODE_DEFAULT_STAKE_AMOUNT + HEDERA_NODE_DEFAULT_STAKE_AMOUNT, TREASURY_ACCOUNT_ID } from '../core/constants.mjs' /** @@ -655,9 +655,10 @@ export class NodeCommand extends BaseCommand { } } - async prepareUpgradeNetworkNodes (config, upgradeZipHash, client) { + async prepareUpgradeNetworkNodes (/** @type {NodeAddConfigClass} **/ config, upgradeZipHash, client) { try { // transfer some tiny amount to the freeze admin account + // TODO: if we do multiple adds, then it will keep adding more hbar to the freeze account instead of just checking to see if it has enough first await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000) // query the balance @@ -668,8 +669,8 @@ export class NodeCommand extends BaseCommand { // set operator of freeze transaction as freeze admin account const accountKeys = await this.accountManager.getAccountKeysFromSecret(FREEZE_ADMIN_ACCOUNT, config.namespace) - const freezeAdminPrivateKey = accountKeys.privateKey - client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) + config.freezeAdminPrivateKey = accountKeys.privateKey + client.setOperator(FREEZE_ADMIN_ACCOUNT, config.freezeAdminPrivateKey) const prepareUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.PrepareUpgrade) @@ -690,7 +691,7 @@ export class NodeCommand extends BaseCommand { } } - async freezeUpgradeNetworkNodes (config, upgradeZipHash, client) { + async freezeUpgradeNetworkNodes (/** @type {NodeAddConfigClass} **/ config, upgradeZipHash, client) { try { const futureDate = new Date() this.logger.debug(`Current time: ${futureDate}`) @@ -698,6 +699,7 @@ export class NodeCommand extends BaseCommand { futureDate.setTime(futureDate.getTime() + 5000) // 5 seconds in the future this.logger.debug(`Freeze time: ${futureDate}`) + client.setOperator(FREEZE_ADMIN_ACCOUNT, config.freezeAdminPrivateKey) const freezeUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.FreezeUpgrade) .setStartTimestamp(Timestamp.fromDate(futureDate)) @@ -1651,11 +1653,16 @@ export class NodeCommand extends BaseCommand { * @property {string} releaseTag * @property {string} settingTxt * -- extra args -- + * @property {PrivateKey} adminKey * @property {string[]} allNodeIds * @property {string} buildZipFile + * @property {string} chartPath * @property {Date} curDate * @property {string[]} existingNodeIds + * @property {string} freezeAdminPrivateKey + * @property {ServiceEndpoint[]} grpcServiceEndpoints * @property {string} keysDir + * @property {string[]} nodeIds * @property {Object} podNames * @property {string} releasePrefix * @property {Map} serviceMap @@ -1672,11 +1679,16 @@ export class NodeCommand extends BaseCommand { // create a config object for subsequent steps const config = /** @type {NodeAddConfigClass} **/ this.getConfig(NodeCommand.ADD_CONFIGS_NAME, NodeCommand.ADD_FLAGS_LIST, [ + 'adminKey', 'allNodeIds', 'buildZipFile', + 'chartPath', 'curDate', 'existingNodeIds', + 'freezeAdminPrivateKey', + 'grpcServiceEndpoints', 'keysDir', + 'nodeIds', 'podNames', 'releasePrefix', 'serviceMap', @@ -1686,6 +1698,7 @@ export class NodeCommand extends BaseCommand { config.curDate = new Date() config.existingNodeIds = [] + config.nodeIds = [config.nodeId] if (config.keyFormat !== constants.KEY_FORMAT_PEM) { throw new FullstackTestingError('key type cannot be PFX') @@ -1721,7 +1734,7 @@ export class NodeCommand extends BaseCommand { title: 'Determine new node account number', task: (ctx, task) => { const values = { hedera: { nodes: [] } } - let maxNum + let maxNum = 0 for (/** @type {NetworkNodeServices} **/ const networkNodeServices of ctx.config.serviceMap.values()) { values.hedera.nodes.push({ @@ -1802,47 +1815,50 @@ export class NodeCommand extends BaseCommand { { title: 'Prepare gossip endpoints', task: (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config let endpoints = [] - if (!ctx.config.gossipEndpoints) { - if (ctx.config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { + if (!config.gossipEndpoints) { + if (config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { throw new FullstackTestingError(`--gossip-endpoints must be set if --endpoint-type is: ${constants.ENDPOINT_TYPE_IP}`) } endpoints = [ - `${Templates.renderFullyQualifiedNetworkPodName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT}`, - `${Templates.renderFullyQualifiedNetworkSvcName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` + `${Templates.renderFullyQualifiedNetworkPodName(config.namespace, config.nodeId)}:${constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT}`, + `${Templates.renderFullyQualifiedNetworkSvcName(config.namespace, config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` ] } else { - endpoints = helpers.splitFlagInput(ctx.config.gossipEndpoints) + endpoints = helpers.splitFlagInput(config.gossipEndpoints) } - ctx.gossipEndpoints = this.prepareEndpoints(ctx.config, endpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT) + ctx.gossipEndpoints = this.prepareEndpoints(config, endpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT) } }, { title: 'Prepare grpc service endpoints', task: (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config let endpoints = [] - if (!ctx.config.grpcServiceEndpoints) { - if (ctx.config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { + if (!config.grpcServiceEndpoints) { + if (config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { throw new FullstackTestingError(`--grpc-endpoints must be set if --endpoint-type is: ${constants.ENDPOINT_TYPE_IP}`) } endpoints = [ - `${Templates.renderFullyQualifiedNetworkSvcName(ctx.config.namespace, ctx.config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` + `${Templates.renderFullyQualifiedNetworkSvcName(config.namespace, config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` ] } else { - endpoints = helpers.splitFlagInput(ctx.config.grpcServiceEndpoints) + endpoints = helpers.splitFlagInput(config.grpcServiceEndpoints) } - ctx.grpcServiceEndpoints = this.prepareEndpoints(ctx.config, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) + ctx.grpcServiceEndpoints = this.prepareEndpoints(config, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) } }, { title: 'Load node admin key', task: async (ctx, task) => { - ctx.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) + const config = /** @type {NodeAddConfigClass} **/ ctx.config + config.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) // await accountManager.loadNodeClient(config.namespace) // const keys = await accountManager.getAccountKeys(constants.COUNCIL_ACCOUNT_ID) // if (keys && keys.length > 0 && keys[0].toString() !== constants.GENESIS_KEY) { @@ -1850,20 +1866,40 @@ export class NodeCommand extends BaseCommand { // } } }, + { + title: 'Prepare upgrade zip file for node upgrade process', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + ctx.nodeClient = await this.accountManager.loadNodeClient(config.namespace) + ctx.upgradeZipFile = await this.prepareUpgradeZip(config) + ctx.upgradeZipHash = await this.uploadUpgradeZip(config, ctx.upgradeZipFile, ctx.nodeClient) + } + }, + { + title: 'Send prepare upgrade transaction', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + await this.prepareUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) + } + }, { title: 'Send node create transaction', task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + try { const nodeClient = this.accountManager._nodeClient + nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.adminKey.toString()) + const nodeCreateTx = await new NodeCreateTransaction() .setAccountId(ctx.newNode.accountId) .setGossipEndpoints(ctx.gossipEndpoints) .setServiceEndpoints(ctx.grpcServiceEndpoints) .setGossipCaCertificate(ctx.signingCertDer) .setCertificateHash(ctx.tlsCertHash) - .setAdminKey(ctx.adminKey.publicKey) + .setAdminKey(config.adminKey.publicKey) .freezeWith(nodeClient) - const signedTx = await nodeCreateTx.sign(ctx.adminKey) + const signedTx = await nodeCreateTx.sign(config.adminKey) const txResp = await signedTx.execute(nodeClient) const nodeCreateReceipt = await txResp.getReceipt(nodeClient) this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) @@ -1872,31 +1908,19 @@ export class NodeCommand extends BaseCommand { } } }, - { - title: 'Prepare upgrade zip file for node upgrade process', - task: async (ctx, task) => { - ctx.nodeClient = await this.accountManager.loadNodeClient(ctx.config.namespace) - ctx.upgradeZipFile = await this.prepareUpgradeZip(ctx.config) - ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.config, ctx.upgradeZipFile, ctx.nodeClient) - } - }, - { - title: 'Send prepare upgrade transaction', - task: async (ctx, task) => { - await this.prepareUpgradeNetworkNodes(ctx.config, ctx.upgradeZipHash, ctx.nodeClient) - } - }, { title: 'Send freeze upgrade transaction', task: async (ctx, task) => { - await this.freezeUpgradeNetworkNodes(ctx.config, ctx.upgradeZipHash, ctx.nodeClient) + const config = /** @type {NodeAddConfigClass} **/ ctx.config + await this.freezeUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) } }, { title: 'Check network nodes are frozen', task: (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [] - for (const nodeId of ctx.config.existingNodeIds) { + for (const nodeId of config.existingNodeIds) { subTasks.push({ title: `Check node: ${chalk.yellow(nodeId)}`, task: () => self.checkNetworkNodeState(nodeId, 100, 'FREEZE_COMPLETE') @@ -1921,46 +1945,36 @@ export class NodeCommand extends BaseCommand { { title: 'Deploy new network node', task: async (ctx, task) => { - const index = ctx.existingNodeIds.length - const valuesArg = ` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}" --set "hedera.nodes[${index}].name=${ctx.newNode.name}"` + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const index = config.existingNodeIds.length + let valuesArg = '' + for (let i = 0; i < index; i++) { + valuesArg += ` --set "hedera.nodes.${i}.accountId=${config.serviceMap.get(config.existingNodeIds[i]).accountId}" --set "hedera.nodes.${i}.name=${config.existingNodeIds[i]}"` + } + valuesArg += ` --set "hedera.nodes.${index}.accountId=${ctx.newNode.accountId}" --set "hedera.nodes.${index}.name=${ctx.newNode.name}"` await self.chartManager.upgrade( - ctx.config.namespace, + config.namespace, constants.FULLSTACK_DEPLOYMENT_CHART, - ctx.config.chartPath, + config.chartPath, valuesArg, - ctx.config.fstChartVersion + config.fstChartVersion ) - ctx.config.allNodeIds = [...ctx.config.existingNodeIds, ctx.config.nodeId] + config.allNodeIds = [...config.existingNodeIds, config.nodeId] } }, { title: 'Check new network node pod is running', task: async (ctx, task) => { - const subTasks = [] - for (const nodeId of ctx.config.nodeIds) { - subTasks.push({ - title: `Check new network pod: ${chalk.yellow(nodeId)}`, - task: async (ctx) => { - ctx.config.podNames[nodeId] = await this.checkNetworkNodePod(ctx.config.namespace, nodeId) - } - }) - } - - // setup the sub-tasks - return task.newListr(subTasks, { - concurrent: true, - rendererOptions: { - collapseSubtasks: false - } - }) + const config = /** @type {NodeAddConfigClass} **/ ctx.config + config.podNames[config.nodeId] = await this.checkNetworkNodePod(config.namespace, config.nodeId) } }, { title: 'Prepare staging directory', task: async (ctx, parentTask) => { - const config = ctx.config + const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [ { title: 'Copy configuration files', @@ -1982,16 +1996,16 @@ export class NodeCommand extends BaseCommand { { title: 'Copy Gossip keys to staging', task: async (ctx, _) => { - const config = ctx.config + const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.copyGossipKeysToStaging(config, ctx.config.allNodeIds) + await this.copyGossipKeysToStaging(config, config.allNodeIds) } }, { title: 'Copy gRPC TLS keys to staging', task: async (ctx, _) => { - const config = ctx.config - for (const nodeId of ctx.config.allNodeIds) { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + for (const nodeId of config.allNodeIds) { const tlsKeyFiles = self.keyManager.prepareTLSKeyFilePaths(nodeId, config.keysDir) await self._copyNodeKeys(tlsKeyFiles, config.stagingKeysDir) } @@ -2003,7 +2017,7 @@ export class NodeCommand extends BaseCommand { // the config.txt to get is in the same directory where the upgradeArtifactsPath located, such as data/upgrade/current title: 'Prepare config.txt for the network', task: async (ctx, _) => { - const config = ctx.config + const config = /** @type {NodeAddConfigClass} **/ ctx.config const configTxtPath = `${config.stagingDir}/config.txt` const template = `${constants.RESOURCES_DIR}/templates/config.template` await self.platformInstaller.prepareConfigTxt(config.allNodeIds, configTxtPath, config.releaseTag, config.chainId, template) @@ -2027,7 +2041,7 @@ export class NodeCommand extends BaseCommand { { title: 'Setup new network node', task: async (ctx, parentTask) => { - const config = ctx.config + const config = /** @type {NodeAddConfigClass} **/ ctx.config // modify application.properties to trick Hedera Services into receiving an updated address book await self.bumpHederaConfigVersion(`${config.stagingDir}/templates/application.properties`) @@ -2052,10 +2066,11 @@ export class NodeCommand extends BaseCommand { { title: 'Start network nodes', task: (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [] // TODO: only start new nodes? // self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) - self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) + self.startNodes(config, config.allNodeIds, subTasks) // TODO: stake new node? @@ -2094,8 +2109,9 @@ export class NodeCommand extends BaseCommand { // this is more reliable than checking the nodes logs for ACTIVE, as the // logs will have a lot of white noise from being behind task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [] - for (const nodeId of ctx.config.nodeIds) { + for (const nodeId of config.allNodeIds) { subTasks.push({ title: `Check proxy for node: ${chalk.yellow(nodeId)}`, task: async () => await self.k8.waitForPodReady( diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index 6d02c5edc..ee59bfb4b 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -79,7 +79,12 @@ export class AccountManager { publicKey: Base64.decode(secret.data.publicKey) } } else { - return null + // if it isn't in the secrets we can load genesis key + return { + accountId, + privateKey: constants.GENESIS_KEY, + publicKey: PrivateKey.fromStringED25519(constants.GENESIS_KEY).publicKey.toString() + } } } @@ -92,18 +97,7 @@ export class AccountManager { */ async getTreasuryAccountKeys (namespace) { // check to see if the treasure account is in the secrets - let accountInfo = await this.getAccountKeysFromSecret(constants.TREASURY_ACCOUNT_ID, namespace) - - // if it isn't in the secrets we can load genesis key - if (!accountInfo) { - accountInfo = { - accountId: constants.TREASURY_ACCOUNT_ID, - privateKey: constants.GENESIS_KEY, - publicKey: PrivateKey.fromStringED25519(constants.GENESIS_KEY).publicKey.toString() - } - } - - return accountInfo + return await this.getAccountKeysFromSecret(constants.TREASURY_ACCOUNT_ID, namespace) } /** @@ -256,8 +250,7 @@ export class AccountManager { async getNodeServiceMap (namespace) { const labelSelector = 'fullstack.hedera.com/node-name' - /** @type {Map} **/ - const serviceBuilderMap = new Map() + const serviceBuilderMap = /** @type {Map} **/ new Map() const serviceList = await this.k8.kubeClient.listNamespacedService( namespace, undefined, undefined, undefined, undefined, labelSelector) diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 02a160635..57dcacf7d 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -213,6 +213,11 @@ export async function getNodeLogs (k8, namespace) { await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/config.txt`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settings.txt`, targetDir) + // get the saved address books + const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book` + const output = await this.execContainer(podName, ROOT_CONTAINER, ['grep', '.', `${addressBookPath}/*`]) + fs.writeFileSync(`${targetDir}/address_book.txt`, output) + // rename all files with timeString as prefix to avoid overwrite fs.readdirSync(targetDir).forEach(file => { const oldPath = path.join(targetDir, file) diff --git a/src/core/k8.mjs b/src/core/k8.mjs index 026c28b13..2b4e27574 100644 --- a/src/core/k8.mjs +++ b/src/core/k8.mjs @@ -331,7 +331,7 @@ export class K8 { * @param containerName container name * @param destPath path inside the container * @param timeout timeout in ms - * @return {Promise<{}>} + * @return {Promise<[]>} */ async listDir (podName, containerName, destPath, timeout = 5000) { try { @@ -344,8 +344,13 @@ export class K8 { for (let line of lines) { line = line.replace(/\s+/g, '|') const parts = line.split('|') - if (parts.length === 9) { - const name = parts[parts.length - 1] + if (parts.length >= 9) { + let name = parts[parts.length - 1] + // handle unique file format (without single quotes): 'usedAddressBook_vHederaSoftwareVersion{hapiVersion=v0.53.0, servicesVersion=v0.53.0}_2024-07-30-20-39-06_node_0.txt.debug' + for (let i = parts.length - 1; i > 8; i--) { + name = `${parts[i - 1]} ${name}` + } + if (name !== '.' && name !== '..') { const permission = parts[0] const item = { diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 5a0bd3f37..afbdd3dd3 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -14,10 +14,15 @@ * limitations under the License. * */ -import { describe, it } from '@jest/globals' +import { afterAll, describe, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' import { constants } from '../../../src/core/index.mjs' -import { bootstrapTestVariables, getDefaultArgv } from '../../test_util.js' +import { + bootstrapNetwork, + getDefaultArgv, + HEDERA_PLATFORM_VERSION_TAG +} from '../../test_util.js' +import { getNodeLogs } from '../../../src/core/helpers.mjs' describe('Node add', () => { const TEST_NAMESPACE = 'node-add' @@ -28,24 +33,24 @@ describe('Node add', () => { argv[flags.generateTlsKeys.name] = true // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - argv[flags.releaseTag] = 'v0.53.0-develop.xd2fbe98' + argv[flags.releaseTag] = HEDERA_PLATFORM_VERSION_TAG argv[flags.namespace.name] = TEST_NAMESPACE - // const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) - const bootstrapResp = bootstrapTestVariables(TEST_NAMESPACE, argv) + const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) + // const bootstrapResp = bootstrapTestVariables(TEST_NAMESPACE, argv) const nodeCmd = bootstrapResp.cmd.nodeCmd - // afterAll(async () => { - // await getNodeLogs(hederaK8, TEST_NAMESPACE) + afterAll(async () => { + await getNodeLogs(nodeCmd.k8, TEST_NAMESPACE) // await hederaK8.deleteNamespace(TEST_NAMESPACE) - // }, 120000) + }, 120000) it('should add a new node to the network successfully', async () => { - argv[flags.nodeID.name] = 'lenin-1' + argv[flags.nodeID.name] = 'uniquenodename' // TODO: open an issue: node ID cannot have a hyphen, platform strips it out, also, can't have capital letters argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM await nodeCmd.add(argv) - }) + }, 120000) }) From 637d7a01018933c01401b849246b168d11cb51a2 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 31 Jul 2024 14:35:30 +0100 Subject: [PATCH 18/56] latest Signed-off-by: Jeromy Cannon --- src/core/helpers.mjs | 5 +++-- test/e2e/commands/node-add.test.mjs | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 57dcacf7d..03ff50bc2 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -214,8 +214,9 @@ export async function getNodeLogs (k8, namespace) { await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settings.txt`, targetDir) // get the saved address books - const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book` - const output = await this.execContainer(podName, ROOT_CONTAINER, ['grep', '.', `${addressBookPath}/*`]) + const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` + const output = await k8.execContainer(podName, ROOT_CONTAINER, + ['bash', '-c', `for file in ${addressBookPath}* ; do echo ; echo File: $file ; echo ; cat "$file" ; done`]) fs.writeFileSync(`${targetDir}/address_book.txt`, output) // rename all files with timeString as prefix to avoid overwrite diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index afbdd3dd3..3be17e901 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -53,4 +53,11 @@ describe('Node add', () => { await nodeCmd.add(argv) }, 120000) + + // it('test', async () => { + // const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` + // const output = await nodeCmd.k8.execContainer('network-node0-0', ROOT_CONTAINER, + // ['bash', '-c', `for file in ${addressBookPath}* ; do echo ; echo File: $file ; echo ; cat "$file" ; done`]) + // console.log(output) + // }, 120000) }) From e1057be7be87c02a5dee64ed7ebd36d24b15ae15 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 31 Jul 2024 19:04:37 +0100 Subject: [PATCH 19/56] latest after talking with Iris and Lev Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 42 ++++++++++++++++++++--------- test/e2e/commands/node-add.test.mjs | 6 ++--- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 10a3dba35..c69a303c7 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -190,6 +190,7 @@ export class NodeCommand extends BaseCommand { .setAccountId(accountId) .setStakedAccountId(accountId) .freezeWith(client) + // TODO fix the staking const treasuryKey = this.accountManager.getTreasuryAccountKeys(namespace) @@ -210,6 +211,8 @@ export class NodeCommand extends BaseCommand { console.log(`Account info: ${accountInfo}`) } catch (e) { this.logger.error(`Error in adding stake: ${e.message}`) + + // throw new FullstackTestingError(`Error in adding stake: ${e.message}`, e) } } @@ -1795,6 +1798,7 @@ export class NodeCommand extends BaseCommand { if (!decodedDers || decodedDers.length === 0) { throw new FullstackTestingError('unable to decode public key: ' + signingCertFile) } + // TODO validate this is right?? ctx.signingCertDer = new Uint8Array(decodedDers[0]) } }, @@ -1875,13 +1879,6 @@ export class NodeCommand extends BaseCommand { ctx.upgradeZipHash = await this.uploadUpgradeZip(config, ctx.upgradeZipFile, ctx.nodeClient) } }, - { - title: 'Send prepare upgrade transaction', - task: async (ctx, task) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.prepareUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) - } - }, { title: 'Send node create transaction', task: async (ctx, task) => { @@ -1908,6 +1905,29 @@ export class NodeCommand extends BaseCommand { } } }, + { + title: 'Send prepare upgrade transaction', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + await this.prepareUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) + } + }, + // { + // title: 'Prepare latest state for new node with updated config.txt', + // task: async (ctx, task) => { + // const config = /** @type {NodeAddConfigClass} **/ ctx.config + // const node1FullyQualifiedPodName = Templates.renderNetworkPodName(config.existingNodeIds[0]) + // const configTxtPath = `${config.stagingDir}/config.txt` + // if (!fs.existsSync(config.stagingDir)) { + // fs.mkdirSync(config.stagingDir, { recursive: true }) + // } + // + // await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/config.txt`, configTxtPath) + // } + // }, + // TODO copy the node 4 pem file into the Hapi directory on all of the old nodes, also the file size and hash are different from the old ones + // the java code is using a pemparser utility + // TODO copy the new config.txt into the Hapi directory on all of the old nodes { title: 'Send freeze upgrade transaction', task: async (ctx, task) => { @@ -1936,12 +1956,6 @@ export class NodeCommand extends BaseCommand { }) } }, - { - title: 'Prepare latest state for new node with updated config.txt', - task: async (ctx, task) => { - - } - }, { title: 'Deploy new network node', task: async (ctx, task) => { @@ -2063,6 +2077,8 @@ export class NodeCommand extends BaseCommand { }) } }, + // TODO new node needs to get the freeze state from the data/saved folder, the latest state to disk and copy to new node, genesis state is not supported, needs to be primed with last state + // /opt/hgcapp/services-hedera/HapiApp2.0/data/saved/com.hedera.services.ServicesMain/0/123 (the 0 is the network node number), then ls -1 to get the last/greatest i.e.: 452, copy the entire directory and put it on the new machine { title: 'Start network nodes', task: (ctx, task) => { diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 3be17e901..aae271b48 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -28,7 +28,7 @@ describe('Node add', () => { const TEST_NAMESPACE = 'node-add' const argv = getDefaultArgv() argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM - argv[flags.nodeIDs.name] = 'node0,node1,node2' + argv[flags.nodeIDs.name] = 'node1,node2,node3' argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts @@ -46,13 +46,13 @@ describe('Node add', () => { }, 120000) it('should add a new node to the network successfully', async () => { - argv[flags.nodeID.name] = 'uniquenodename' // TODO: open an issue: node ID cannot have a hyphen, platform strips it out, also, can't have capital letters + argv[flags.nodeID.name] = 'node4' // TODO: open an issue: node ID cannot have a hyphen, platform strips it out, also, can't have capital letters argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM await nodeCmd.add(argv) - }, 120000) + }, 900000) // it('test', async () => { // const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` From d8a391ef29e36b7062e61809a8d6a511f485c820 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 6 Aug 2024 16:25:48 +0100 Subject: [PATCH 20/56] upgrade @hasgraph/sdk to 2.49.2 to pick up bug fix for connection dropping when only one node Signed-off-by: Jeromy Cannon --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b843bef2..6c6e06ce4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ ], "dependencies": { "@hashgraph/proto": "^2.15.0", - "@hashgraph/sdk": "^2.48.1", + "@hashgraph/sdk": "^2.49.2", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.10", "@peculiar/x509": "^1.11.0", @@ -1238,9 +1238,9 @@ } }, "node_modules/@hashgraph/sdk": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.48.1.tgz", - "integrity": "sha512-ZizLBcgu7bBIx+EuwRWFkBQugAQAkH5ZvKe+dTf53TCVoUw3LPIta+u/hpVIhB9Xm9PJmA9F23DSo+Bmken46Q==", + "version": "2.49.2", + "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.49.2.tgz", + "integrity": "sha512-HqESeH6gF/QEm69qmEyPZ40i9w2jBXsyXFqT/kRsrb7yEyCdVrG1Wnt88HxOSP9XyOsm3hj4OBTEiy2sI+kl+A==", "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", @@ -1248,7 +1248,7 @@ "@ethersproject/rlp": "^5.7.0", "@grpc/grpc-js": "1.8.2", "@hashgraph/cryptography": "1.4.8-beta.5", - "@hashgraph/proto": "2.15.0-beta.2", + "@hashgraph/proto": "2.15.0-beta.3", "axios": "^1.6.4", "bignumber.js": "^9.1.1", "bn.js": "^5.1.1", @@ -1274,9 +1274,9 @@ } }, "node_modules/@hashgraph/sdk/node_modules/@hashgraph/proto": { - "version": "2.15.0-beta.2", - "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0-beta.2.tgz", - "integrity": "sha512-vtkW8rZccnch3FKmNj1bLRZsS0YO3w7PuCj8LNq9HgMP6KzzA7scRk8yLm/XyiD7UHJS/l1cFg0Cj/DDRSwGxQ==", + "version": "2.15.0-beta.3", + "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0-beta.3.tgz", + "integrity": "sha512-/95cydqBQRaO1gagBOenNpcfJIcnRx+vzefhuzSFNp4pfl0AM3oXau39hM2raKLhFHoKZysSjkXkMp24AaCE5w==", "dependencies": { "long": "^4.0.0", "protobufjs": "^7.2.5" diff --git a/package.json b/package.json index 9fdbe3da5..8ac5f47d7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "license": "Apache2.0", "dependencies": { "@hashgraph/proto": "^2.15.0", - "@hashgraph/sdk": "^2.48.1", + "@hashgraph/sdk": "^2.49.2", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.10", "@peculiar/x509": "^1.11.0", From 6ce82c2ec6f7024f8dedeba2554afbb16d0179d7 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 7 Aug 2024 18:54:26 +0100 Subject: [PATCH 21/56] application.properties: set stake weight calculations to once per minute Signed-off-by: Jeromy Cannon --- resources/templates/application.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/templates/application.properties b/resources/templates/application.properties index d47caec3f..baa214200 100644 --- a/resources/templates/application.properties +++ b/resources/templates/application.properties @@ -11,3 +11,5 @@ hedera.config.version=0 nodes.gossipFqdnRestricted=false netty.mode=TEST hedera.profiles.active=TEST +# TODO: this is a workaround until prepareUpgrade freeze will recalculate the weight prior to writing the config.txt +staking.periodMins=1 From e37cb3ab765deb09ad6b0c5c2e5a697d063d8798 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 7 Aug 2024 18:54:53 +0100 Subject: [PATCH 22/56] log4j2.xml: trigger new file for swirlds.log and hgcaa.log on startup Signed-off-by: Jeromy Cannon --- resources/templates/log4j2.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/templates/log4j2.xml b/resources/templates/log4j2.xml index cb70b0a88..7cb57e7ad 100644 --- a/resources/templates/log4j2.xml +++ b/resources/templates/log4j2.xml @@ -12,6 +12,7 @@ %d{yyyy-MM-dd HH:mm:ss.SSS} %-5p %-4L %c{1} - %m{nolookups}%n + @@ -35,6 +36,7 @@ %d{yyyy-MM-dd HH:mm:ss.SSS} %-8sn %-5p %-16marker <%t> %c{1}: %msg{nolookups}%n + From 21f4c9f10ef9f0bad7796d746261f5626b42245d Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 8 Aug 2024 14:36:57 +0100 Subject: [PATCH 23/56] current version updates Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 134 ++++++++++++++++++---------- src/core/templates.mjs | 8 ++ test/e2e/commands/node-add.test.mjs | 14 +-- test/test_util.js | 2 +- version.mjs | 2 +- 5 files changed, 104 insertions(+), 56 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index c69a303c7..5f72885de 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -35,6 +35,8 @@ import { FileAppendTransaction, FreezeTransaction, FreezeType, + Hbar, + HbarUnit, ServiceEndpoint, Timestamp, PrivateKey, @@ -171,13 +173,13 @@ export class NodeCommand extends BaseCommand { this._portForwards = [] } - async addStake (namespace, accountId) { + async addStake (namespace, accountId, nodeId) { try { await this.accountManager.loadNodeClient(namespace) const client = this.accountManager._nodeClient // get some initial balance - await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, HEDERA_NODE_DEFAULT_STAKE_AMOUNT) + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, HEDERA_NODE_DEFAULT_STAKE_AMOUNT + 1) // check balance const balance = await new AccountBalanceQuery() @@ -188,14 +190,14 @@ export class NodeCommand extends BaseCommand { // Create the transaction const transaction = await new AccountUpdateTransaction() .setAccountId(accountId) - .setStakedAccountId(accountId) + .setStakedNodeId(Templates.nodeNumberFromNodeId(nodeId) - 1) .freezeWith(client) - // TODO fix the staking - const treasuryKey = this.accountManager.getTreasuryAccountKeys(namespace) + const treasuryKey = await this.accountManager.getTreasuryAccountKeys(namespace) + const treasuryPrivateKey = PrivateKey.fromStringED25519(treasuryKey.privateKey) // Sign the transaction with the account's private key - const signTx = await transaction.sign(treasuryKey) + const signTx = await transaction.sign(treasuryPrivateKey) // Submit the transaction to a Hedera network const txResponse = await signTx.execute(client) @@ -205,14 +207,12 @@ export class NodeCommand extends BaseCommand { // Get the transaction status const transactionStatus = receipt.status - console.log('The transaction consensus status is ' + transactionStatus.toString()) + this.logger.debug(`The transaction consensus status is ${transactionStatus.toString()}`) const accountInfo = await this.accountManager.accountInfoQuery(accountId) - console.log(`Account info: ${accountInfo}`) + this.logger.info(`Account ID: ${accountId}, nodeId: ${nodeId - 1}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) } catch (e) { - this.logger.error(`Error in adding stake: ${e.message}`) - - // throw new FullstackTestingError(`Error in adding stake: ${e.message}`, e) + throw new FullstackTestingError(`Error in adding stake: ${e.message}`, e) } } @@ -726,7 +726,7 @@ export class NodeCommand extends BaseCommand { subTasks.push({ title: `Start node: ${chalk.yellow(nodeId)}`, task: async () => { - await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/output/*`]) + // await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/output/*`]) await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) } }) @@ -1150,7 +1150,7 @@ export class NodeCommand extends BaseCommand { const accountId = accountMap.get(nodeId) subTasks.push({ title: `Adding stake for node: ${chalk.yellow(nodeId)}`, - task: () => self.addStake(ctx.config.namespace, accountId) + task: () => self.addStake(ctx.config.namespace, accountId, nodeId) }) } @@ -1665,6 +1665,7 @@ export class NodeCommand extends BaseCommand { * @property {string} freezeAdminPrivateKey * @property {ServiceEndpoint[]} grpcServiceEndpoints * @property {string} keysDir + * @property {string} lastStateZipPath * @property {string[]} nodeIds * @property {Object} podNames * @property {string} releasePrefix @@ -1691,6 +1692,7 @@ export class NodeCommand extends BaseCommand { 'freezeAdminPrivateKey', 'grpcServiceEndpoints', 'keysDir', + 'lastStateZipPath', 'nodeIds', 'podNames', 'releasePrefix', @@ -1879,6 +1881,21 @@ export class NodeCommand extends BaseCommand { ctx.upgradeZipHash = await this.uploadUpgradeZip(config, ctx.upgradeZipFile, ctx.nodeClient) } }, + { + title: 'Check existing nodes staked amount', + task: async (ctx, task) => { + // const config = /** @type {NodeAddConfigClass} **/ ctx.config + await sleep(60000) + const accountMap = getNodeAccountMap(ctx.config.existingNodeIds) + for (const nodeId of ctx.config.existingNodeIds) { + const accountId = accountMap.get(nodeId) + const accountInfo = await this.accountManager.accountInfoQuery(accountId) + this.logger.info(`Account ID: ${accountId}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1) + } + } + }, + { title: 'Send node create transaction', task: async (ctx, task) => { @@ -1912,22 +1929,23 @@ export class NodeCommand extends BaseCommand { await this.prepareUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) } }, - // { - // title: 'Prepare latest state for new node with updated config.txt', - // task: async (ctx, task) => { - // const config = /** @type {NodeAddConfigClass} **/ ctx.config - // const node1FullyQualifiedPodName = Templates.renderNetworkPodName(config.existingNodeIds[0]) - // const configTxtPath = `${config.stagingDir}/config.txt` - // if (!fs.existsSync(config.stagingDir)) { - // fs.mkdirSync(config.stagingDir, { recursive: true }) - // } - // - // await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/config.txt`, configTxtPath) - // } - // }, - // TODO copy the node 4 pem file into the Hapi directory on all of the old nodes, also the file size and hash are different from the old ones - // the java code is using a pemparser utility - // TODO copy the new config.txt into the Hapi directory on all of the old nodes + { + title: 'Download generated files from an existing node', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const node1FullyQualifiedPodName = Templates.renderNetworkPodName(config.existingNodeIds[0]) + + // copy the config.txt file from the node1 upgrade directory + // const configTxtPath = `${config.stagingDir}/config.txt` + await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/config.txt`, config.stagingDir) + + const signedKeyFiles = (await self.k8.listDir(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current`)).filter(file => file.name.startsWith(constants.SIGNING_KEY_PREFIX)) + for (const signedKeyFile of signedKeyFiles) { + await self.k8.execContainer(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `[[ ! -f "${constants.HEDERA_HAPI_PATH}/data/keys/${signedKeyFile.name}" ]] || cp ${constants.HEDERA_HAPI_PATH}/data/keys/${signedKeyFile.name} ${constants.HEDERA_HAPI_PATH}/data/keys/${signedKeyFile.name}.old`]) + await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/${signedKeyFile.name}`, `${config.keysDir}`) + } + } + }, { title: 'Send freeze upgrade transaction', task: async (ctx, task) => { @@ -1956,6 +1974,16 @@ export class NodeCommand extends BaseCommand { }) } }, + { + title: 'Purge pces files from existing nodes', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + for (const nodeId of config.existingNodeIds) { + const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeId) + await self.k8.execContainer(nodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/preconsensus-events/*`]) + } + } + }, { title: 'Deploy new network node', task: async (ctx, task) => { @@ -2024,18 +2052,6 @@ export class NodeCommand extends BaseCommand { await self._copyNodeKeys(tlsKeyFiles, config.stagingKeysDir) } } - }, - { - // TODO: not needed? - // TODO: might need to read this from an existing node, at the perfect time, which is? after freeze prepare upgrade - // the config.txt to get is in the same directory where the upgradeArtifactsPath located, such as data/upgrade/current - title: 'Prepare config.txt for the network', - task: async (ctx, _) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config - const configTxtPath = `${config.stagingDir}/config.txt` - const template = `${constants.RESOURCES_DIR}/templates/config.template` - await self.platformInstaller.prepareConfigTxt(config.allNodeIds, configTxtPath, config.releaseTag, config.chainId, template) - } } ] @@ -2052,6 +2068,34 @@ export class NodeCommand extends BaseCommand { return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) } }, + { + title: 'Download last state from an existing node', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const node1FullyQualifiedPodName = Templates.renderNetworkPodName(config.existingNodeIds[0]) + const upgradeDirectory = `${constants.HEDERA_HAPI_PATH}/data/saved/com.hedera.services.ServicesMain/0/123` + // zip the contents of the newest folder on node1 within /opt/hgcapp/services-hedera/HapiApp2.0/data/saved/com.hedera.services.ServicesMain/0/123/ + const zipFileName = await self.k8.execContainer(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `cd ${upgradeDirectory} && mapfile -t states < <(ls -1t .) && jar cf "\${states[0]}.zip" -C "\${states[0]}" . && echo -n \${states[0]}.zip`]) + await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${upgradeDirectory}/${zipFileName}`, config.stagingDir) + config.lastStateZipPath = `${config.stagingDir}/${zipFileName}` + } + }, + // TODO: update all nodeIds used in tests to start with node1 instead of node0 + { + title: 'Upload last saved state to new network node', + task: + async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const newNodeFullyQualifiedPodName = Templates.renderNetworkPodName(config.nodeId) + const nodeNumber = Templates.nodeNumberFromNodeId(config.nodeId) + const savedStateDir = (config.lastStateZipPath.match(/\/(\d+)\.zip$/))[1] + const savedStatePath = `${constants.HEDERA_HAPI_PATH}/data/saved/com.hedera.services.ServicesMain/${nodeNumber}/123/${savedStateDir}` + await self.k8.execContainer(newNodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `mkdir -p ${savedStatePath}`]) + await self.k8.copyTo(newNodeFullyQualifiedPodName, constants.ROOT_CONTAINER, config.lastStateZipPath, savedStatePath) + await self.platformInstaller.setPathPermission(newNodeFullyQualifiedPodName, constants.HEDERA_HAPI_PATH) + await self.k8.execContainer(newNodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `cd ${savedStatePath} && jar xf ${path.basename(config.lastStateZipPath)} && rm -f ${path.basename(config.lastStateZipPath)}`]) + } + }, { title: 'Setup new network node', task: async (ctx, parentTask) => { @@ -2077,19 +2121,14 @@ export class NodeCommand extends BaseCommand { }) } }, - // TODO new node needs to get the freeze state from the data/saved folder, the latest state to disk and copy to new node, genesis state is not supported, needs to be primed with last state - // /opt/hgcapp/services-hedera/HapiApp2.0/data/saved/com.hedera.services.ServicesMain/0/123 (the 0 is the network node number), then ls -1 to get the last/greatest i.e.: 452, copy the entire directory and put it on the new machine + // TODO: get a copy of the previous hgcaa.log/swirlds.log before restart { title: 'Start network nodes', task: (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [] - // TODO: only start new nodes? - // self.startNodes(ctx.config, ctx.config.allNodeIds, subTasks) self.startNodes(config, config.allNodeIds, subTasks) - // TODO: stake new node? - // set up the sub-tasks return task.newListr(subTasks, { concurrent: true, @@ -2145,6 +2184,7 @@ export class NodeCommand extends BaseCommand { }) } }, + // TODO: stake new node? { title: 'Finalize', task: (ctx, _) => { diff --git a/src/core/templates.mjs b/src/core/templates.mjs index c7f8e20c7..b587fc768 100644 --- a/src/core/templates.mjs +++ b/src/core/templates.mjs @@ -180,4 +180,12 @@ export class Templates { const parts = svcName.split('.') return this.nodeIdFromNetworkSvcName(parts[0]) } + + static nodeNumberFromNodeId (nodeId) { + for (let i = nodeId.length - 1; i > 0; i--) { + if (isNaN(nodeId[i])) { + return parseInt(nodeId.substring(i + 1, nodeId.length)) + } + } + } } diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index aae271b48..46503b73d 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -16,13 +16,14 @@ */ import { afterAll, describe, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' -import { constants } from '../../../src/core/index.mjs' +import { constants, Templates } from '../../../src/core/index.mjs' import { - bootstrapNetwork, + bootstrapNetwork, bootstrapTestVariables, getDefaultArgv, HEDERA_PLATFORM_VERSION_TAG } from '../../test_util.js' import { getNodeLogs } from '../../../src/core/helpers.mjs' +import fs from 'fs' describe('Node add', () => { const TEST_NAMESPACE = 'node-add' @@ -41,7 +42,7 @@ describe('Node add', () => { const nodeCmd = bootstrapResp.cmd.nodeCmd afterAll(async () => { - await getNodeLogs(nodeCmd.k8, TEST_NAMESPACE) + // await getNodeLogs(nodeCmd.k8, TEST_NAMESPACE) // await hederaK8.deleteNamespace(TEST_NAMESPACE) }, 120000) @@ -55,9 +56,8 @@ describe('Node add', () => { }, 900000) // it('test', async () => { - // const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` - // const output = await nodeCmd.k8.execContainer('network-node0-0', ROOT_CONTAINER, - // ['bash', '-c', `for file in ${addressBookPath}* ; do echo ; echo File: $file ; echo ; cat "$file" ; done`]) - // console.log(output) + // const node1FullyQualifiedPodName = Templates.renderNetworkPodName('node1') + // const zipFileName = await nodeCmd.k8.execContainer(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `cd ${constants.HEDERA_HAPI_PATH}/data/saved/com.hedera.services.ServicesMain/0/123 && mapfile -t states < <(ls -1t .) && jar cf "\${states[0]}.zip" -C "\${states[0]}" . && echo -n \${states[0]}.zip`]) + // console.log(zipFileName) // }, 120000) }) diff --git a/test/test_util.js b/test/test_util.js index f985bc631..6472804b5 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -46,7 +46,7 @@ import { AccountBalanceQuery } from '@hashgraph/sdk' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.0-release-0.53.x7be05aa' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.0-release-0.53.xff7c43d' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index c28fc6317..9e8f123f1 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.28.2' -export const HEDERA_PLATFORM_VERSION = 'v0.53.0-release-0.53.x7be05aa' +export const HEDERA_PLATFORM_VERSION = 'v0.53.0-release-0.53.xff7c43d' From b7ac9e3524ae586b223c46b2dca35d0c50ec2b5a Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 8 Aug 2024 19:47:59 +0100 Subject: [PATCH 24/56] working version Signed-off-by: Jeromy Cannon --- resources/templates/log4j2.xml | 2 +- resources/templates/settings.txt | 3 ++ src/commands/node.mjs | 51 +++++++++++++++++++++-------- src/core/profile_manager.mjs | 4 +++ test/e2e/commands/node-add.test.mjs | 5 +-- test/test_util.js | 2 +- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/resources/templates/log4j2.xml b/resources/templates/log4j2.xml index 7cb57e7ad..1234fc7e0 100644 --- a/resources/templates/log4j2.xml +++ b/resources/templates/log4j2.xml @@ -93,7 +93,7 @@ - + diff --git a/resources/templates/settings.txt b/resources/templates/settings.txt index 7ecdc3ccf..3b34a834b 100644 --- a/resources/templates/settings.txt +++ b/resources/templates/settings.txt @@ -9,3 +9,6 @@ state.mainClassNameOverride, com.hedera.services.ServicesMain ############################# crypto.enableNewKeyStoreModel, true + +# TODO: remove this? only defaults to true when going from 0.52 to 0.53 +event.migrateEventHashing, false diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 5f72885de..a7ee42f4c 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -1895,7 +1895,6 @@ export class NodeCommand extends BaseCommand { } } }, - { title: 'Send node create transaction', task: async (ctx, task) => { @@ -1974,16 +1973,16 @@ export class NodeCommand extends BaseCommand { }) } }, - { - title: 'Purge pces files from existing nodes', - task: async (ctx, task) => { - const config = /** @type {NodeAddConfigClass} **/ ctx.config - for (const nodeId of config.existingNodeIds) { - const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeId) - await self.k8.execContainer(nodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/preconsensus-events/*`]) - } - } - }, + // { + // title: 'Purge pces files from existing nodes', + // task: async (ctx, task) => { + // const config = /** @type {NodeAddConfigClass} **/ ctx.config + // for (const nodeId of config.existingNodeIds) { + // const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeId) + // await self.k8.execContainer(nodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/preconsensus-events/*`]) + // } + // } + // }, { title: 'Deploy new network node', task: async (ctx, task) => { @@ -1991,9 +1990,13 @@ export class NodeCommand extends BaseCommand { const index = config.existingNodeIds.length let valuesArg = '' for (let i = 0; i < index; i++) { - valuesArg += ` --set "hedera.nodes.${i}.accountId=${config.serviceMap.get(config.existingNodeIds[i]).accountId}" --set "hedera.nodes.${i}.name=${config.existingNodeIds[i]}"` + valuesArg += ` --set "hedera.nodes[${i}].accountId=${config.serviceMap.get(config.existingNodeIds[i]).accountId}" --set "hedera.nodes[${i}].name=${config.existingNodeIds[i]}"` + // if (i === 0) { + // valuesArg += ` --set "hedera.nodes[${i}].root.extraEnv[0].name=JAVA_OPTS"` + // valuesArg += ` --set "hedera.nodes[${i}].root.extraEnv[0].value=-agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=y\\,address=*:5005"` + // } } - valuesArg += ` --set "hedera.nodes.${index}.accountId=${ctx.newNode.accountId}" --set "hedera.nodes.${index}.name=${ctx.newNode.name}"` + valuesArg += ` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}" --set "hedera.nodes[${index}].name=${ctx.newNode.name}"` await self.chartManager.upgrade( config.namespace, @@ -2184,7 +2187,27 @@ export class NodeCommand extends BaseCommand { }) } }, - // TODO: stake new node? + { + title: 'Stake new node', + task: (ctx, _) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + self.addStake(config.namespace, ctx.newNode.accountId, config.nodeId) + } + }, + { + title: 'Check existing nodes staked amount', + task: async (ctx, task) => { + const config = /** @type {NodeAddConfigClass} **/ ctx.config + await sleep(60000) + const accountMap = getNodeAccountMap(config.allNodeIds) + for (const nodeId of config.allNodeIds) { + const accountId = accountMap.get(nodeId) + const accountInfo = await this.accountManager.accountInfoQuery(accountId) + this.logger.info(`Account ID: ${accountId}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1) + } + } + }, { title: 'Finalize', task: (ctx, _) => { diff --git a/src/core/profile_manager.mjs b/src/core/profile_manager.mjs index 3931ff18a..5a36afe3d 100644 --- a/src/core/profile_manager.mjs +++ b/src/core/profile_manager.mjs @@ -156,6 +156,10 @@ export class ProfileManager { this._setValue(`hedera.nodes.${nodeIndex}.name`, nodeIds[nodeIndex], yamlRoot) this._setValue(`hedera.nodes.${nodeIndex}.accountId`, accountMap.get(nodeIds[nodeIndex]), yamlRoot) this._setChartItems(`hedera.nodes.${nodeIndex}`, profile.consensus, yamlRoot) + // if (nodeIndex === 0) { + // this._setValue(`hedera.nodes.${nodeIndex}.root.extraEnv.0.name`, 'JAVA_OPTS', yamlRoot) + // this._setValue(`hedera.nodes.${nodeIndex}.root.extraEnv.0.value`, '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005', yamlRoot) + // } } if (profile.consensus) { diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 46503b73d..6c8eb89f4 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -34,7 +34,7 @@ describe('Node add', () => { argv[flags.generateTlsKeys.name] = true // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - argv[flags.releaseTag] = HEDERA_PLATFORM_VERSION_TAG + argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG argv[flags.namespace.name] = TEST_NAMESPACE const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) @@ -42,6 +42,7 @@ describe('Node add', () => { const nodeCmd = bootstrapResp.cmd.nodeCmd afterAll(async () => { + // TODO: enhance getNodeLogs to get rolled over logs and other files that are important // await getNodeLogs(nodeCmd.k8, TEST_NAMESPACE) // await hederaK8.deleteNamespace(TEST_NAMESPACE) }, 120000) @@ -53,7 +54,7 @@ describe('Node add', () => { argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM await nodeCmd.add(argv) - }, 900000) + }, 99999999) // it('test', async () => { // const node1FullyQualifiedPodName = Templates.renderNetworkPodName('node1') diff --git a/test/test_util.js b/test/test_util.js index 6472804b5..860196db2 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -247,7 +247,7 @@ export function bootstrapNetwork (testName, argv, nodeCmd.logger.showUserError(e) expect(e).toBeNull() } - }, 1800000) + }, 99999999) }) return bootstrapResp From d481fd5bd90ae234bdcbb8108d5153ce1d8d56e6 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Thu, 8 Aug 2024 19:58:39 +0100 Subject: [PATCH 25/56] add todo Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index a7ee42f4c..15f2e53ac 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -2148,6 +2148,7 @@ export class NodeCommand extends BaseCommand { const subTasks = [] for (const nodeId of ctx.config.allNodeIds) { subTasks.push({ + // TODO: during node add, this seems to trip accidentally as true when message said that node isn't ACTIVE title: `Check node: ${chalk.yellow(nodeId)}`, task: () => self.checkNetworkNodeState(nodeId, 200) }) From 229c171b04bf89efaae5da31718e878db5df62e6 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 9 Aug 2024 15:08:42 +0100 Subject: [PATCH 26/56] revert Signed-off-by: Jeromy Cannon --- test/e2e/e2e_node_util.js | 2 +- test/test_util.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/e2e_node_util.js b/test/e2e/e2e_node_util.js index 2e2671510..ee39884dd 100644 --- a/test/e2e/e2e_node_util.js +++ b/test/e2e/e2e_node_util.js @@ -81,7 +81,7 @@ export function e2eNodeKeyRefreshAddTest (keyFormat, testName, mode, releaseTag afterAll(async () => { await getNodeLogs(k8, namespace) - // await k8.deleteNamespace(namespace) // TODO revert + await k8.deleteNamespace(namespace) }, 180000) describe(`Node should have started successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, () => { diff --git a/test/test_util.js b/test/test_util.js index 860196db2..6472804b5 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -247,7 +247,7 @@ export function bootstrapNetwork (testName, argv, nodeCmd.logger.showUserError(e) expect(e).toBeNull() } - }, 99999999) + }, 1800000) }) return bootstrapResp From 39c979294ad07d429c6f72ed10eee7d57d4a91da Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 9 Aug 2024 16:18:14 +0100 Subject: [PATCH 27/56] updates for PR reviews Signed-off-by: Jeromy Cannon --- src/core/helpers.mjs | 2 ++ test/e2e/commands/node-add.test.mjs | 20 ++++++-------------- version.mjs | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 03ff50bc2..4e620c9b2 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -212,6 +212,7 @@ export async function getNodeLogs (k8, namespace) { await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/output/hgcaa.log`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/config.txt`, targetDir) await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settings.txt`, targetDir) + await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settingsUsed.txt`, targetDir) // get the saved address books const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` @@ -219,6 +220,7 @@ export async function getNodeLogs (k8, namespace) { ['bash', '-c', `for file in ${addressBookPath}* ; do echo ; echo File: $file ; echo ; cat "$file" ; done`]) fs.writeFileSync(`${targetDir}/address_book.txt`, output) + // TODO: open issue, this will cause it to prefix the prefix if old files are already in the directory running locally // rename all files with timeString as prefix to avoid overwrite fs.readdirSync(targetDir).forEach(file => { const oldPath = path.join(targetDir, file) diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 6c8eb89f4..64dcf0675 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -16,14 +16,13 @@ */ import { afterAll, describe, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' -import { constants, Templates } from '../../../src/core/index.mjs' +import { constants } from '../../../src/core/index.mjs' import { - bootstrapNetwork, bootstrapTestVariables, + bootstrapNetwork, getDefaultArgv, HEDERA_PLATFORM_VERSION_TAG } from '../../test_util.js' import { getNodeLogs } from '../../../src/core/helpers.mjs' -import fs from 'fs' describe('Node add', () => { const TEST_NAMESPACE = 'node-add' @@ -38,13 +37,12 @@ describe('Node add', () => { argv[flags.namespace.name] = TEST_NAMESPACE const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) - // const bootstrapResp = bootstrapTestVariables(TEST_NAMESPACE, argv) const nodeCmd = bootstrapResp.cmd.nodeCmd + const k8 = bootstrapResp.opts.k8 afterAll(async () => { - // TODO: enhance getNodeLogs to get rolled over logs and other files that are important - // await getNodeLogs(nodeCmd.k8, TEST_NAMESPACE) - // await hederaK8.deleteNamespace(TEST_NAMESPACE) + await getNodeLogs(k8, TEST_NAMESPACE) + await k8.deleteNamespace(TEST_NAMESPACE) }, 120000) it('should add a new node to the network successfully', async () => { @@ -54,11 +52,5 @@ describe('Node add', () => { argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM await nodeCmd.add(argv) - }, 99999999) - - // it('test', async () => { - // const node1FullyQualifiedPodName = Templates.renderNetworkPodName('node1') - // const zipFileName = await nodeCmd.k8.execContainer(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `cd ${constants.HEDERA_HAPI_PATH}/data/saved/com.hedera.services.ServicesMain/0/123 && mapfile -t states < <(ls -1t .) && jar cf "\${states[0]}.zip" -C "\${states[0]}" . && echo -n \${states[0]}.zip`]) - // console.log(zipFileName) - // }, 120000) + }, 600000) }) diff --git a/version.mjs b/version.mjs index 9e8f123f1..75b7c406f 100644 --- a/version.mjs +++ b/version.mjs @@ -21,5 +21,5 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' -export const FST_CHART_VERSION = 'v0.28.2' +export const FST_CHART_VERSION = 'v0.29.1' export const HEDERA_PLATFORM_VERSION = 'v0.53.0-release-0.53.xff7c43d' From d8f8072e83d64b56b06040b7c3ab75961508d79e Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 9 Aug 2024 20:04:11 +0100 Subject: [PATCH 28/56] cleanup Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 31 ++----------------- test/e2e/commands/node-add.test.mjs | 12 ++++++- test/e2e/core/platform_installer_e2e.test.mjs | 2 +- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 15f2e53ac..5ebd30dc3 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -661,7 +661,6 @@ export class NodeCommand extends BaseCommand { async prepareUpgradeNetworkNodes (/** @type {NodeAddConfigClass} **/ config, upgradeZipHash, client) { try { // transfer some tiny amount to the freeze admin account - // TODO: if we do multiple adds, then it will keep adding more hbar to the freeze account instead of just checking to see if it has enough first await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000) // query the balance @@ -1663,7 +1662,6 @@ export class NodeCommand extends BaseCommand { * @property {Date} curDate * @property {string[]} existingNodeIds * @property {string} freezeAdminPrivateKey - * @property {ServiceEndpoint[]} grpcServiceEndpoints * @property {string} keysDir * @property {string} lastStateZipPath * @property {string[]} nodeIds @@ -1690,7 +1688,6 @@ export class NodeCommand extends BaseCommand { 'curDate', 'existingNodeIds', 'freezeAdminPrivateKey', - 'grpcServiceEndpoints', 'keysDir', 'lastStateZipPath', 'nodeIds', @@ -1800,7 +1797,6 @@ export class NodeCommand extends BaseCommand { if (!decodedDers || decodedDers.length === 0) { throw new FullstackTestingError('unable to decode public key: ' + signingCertFile) } - // TODO validate this is right?? ctx.signingCertDer = new Uint8Array(decodedDers[0]) } }, @@ -1845,7 +1841,7 @@ export class NodeCommand extends BaseCommand { const config = /** @type {NodeAddConfigClass} **/ ctx.config let endpoints = [] - if (!config.grpcServiceEndpoints) { + if (!config.grpcEndpoints) { if (config.endpointType !== constants.ENDPOINT_TYPE_FQDN) { throw new FullstackTestingError(`--grpc-endpoints must be set if --endpoint-type is: ${constants.ENDPOINT_TYPE_IP}`) } @@ -1854,7 +1850,7 @@ export class NodeCommand extends BaseCommand { `${Templates.renderFullyQualifiedNetworkSvcName(config.namespace, config.nodeId)}:${constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT}` ] } else { - endpoints = helpers.splitFlagInput(config.grpcServiceEndpoints) + endpoints = helpers.splitFlagInput(config.grpcEndpoints) } ctx.grpcServiceEndpoints = this.prepareEndpoints(config, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) @@ -1865,11 +1861,6 @@ export class NodeCommand extends BaseCommand { task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config config.adminKey = PrivateKey.fromStringED25519(constants.GENESIS_KEY) - // await accountManager.loadNodeClient(config.namespace) - // const keys = await accountManager.getAccountKeys(constants.COUNCIL_ACCOUNT_ID) - // if (keys && keys.length > 0 && keys[0].toString() !== constants.GENESIS_KEY) { - // adminKey = PrivateKey.fromStringED25519(keys[0].toString()) - // } } }, { @@ -1935,7 +1926,6 @@ export class NodeCommand extends BaseCommand { const node1FullyQualifiedPodName = Templates.renderNetworkPodName(config.existingNodeIds[0]) // copy the config.txt file from the node1 upgrade directory - // const configTxtPath = `${config.stagingDir}/config.txt` await self.k8.copyFrom(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current/config.txt`, config.stagingDir) const signedKeyFiles = (await self.k8.listDir(node1FullyQualifiedPodName, constants.ROOT_CONTAINER, `${constants.HEDERA_HAPI_PATH}/data/upgrade/current`)).filter(file => file.name.startsWith(constants.SIGNING_KEY_PREFIX)) @@ -1973,16 +1963,6 @@ export class NodeCommand extends BaseCommand { }) } }, - // { - // title: 'Purge pces files from existing nodes', - // task: async (ctx, task) => { - // const config = /** @type {NodeAddConfigClass} **/ ctx.config - // for (const nodeId of config.existingNodeIds) { - // const nodeFullyQualifiedPodName = Templates.renderNetworkPodName(nodeId) - // await self.k8.execContainer(nodeFullyQualifiedPodName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/data/saved/preconsensus-events/*`]) - // } - // } - // }, { title: 'Deploy new network node', task: async (ctx, task) => { @@ -1991,10 +1971,6 @@ export class NodeCommand extends BaseCommand { let valuesArg = '' for (let i = 0; i < index; i++) { valuesArg += ` --set "hedera.nodes[${i}].accountId=${config.serviceMap.get(config.existingNodeIds[i]).accountId}" --set "hedera.nodes[${i}].name=${config.existingNodeIds[i]}"` - // if (i === 0) { - // valuesArg += ` --set "hedera.nodes[${i}].root.extraEnv[0].name=JAVA_OPTS"` - // valuesArg += ` --set "hedera.nodes[${i}].root.extraEnv[0].value=-agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=y\\,address=*:5005"` - // } } valuesArg += ` --set "hedera.nodes[${index}].accountId=${ctx.newNode.accountId}" --set "hedera.nodes[${index}].name=${ctx.newNode.name}"` @@ -2083,7 +2059,6 @@ export class NodeCommand extends BaseCommand { config.lastStateZipPath = `${config.stagingDir}/${zipFileName}` } }, - // TODO: update all nodeIds used in tests to start with node1 instead of node0 { title: 'Upload last saved state to new network node', task: @@ -2124,7 +2099,6 @@ export class NodeCommand extends BaseCommand { }) } }, - // TODO: get a copy of the previous hgcaa.log/swirlds.log before restart { title: 'Start network nodes', task: (ctx, task) => { @@ -2148,7 +2122,6 @@ export class NodeCommand extends BaseCommand { const subTasks = [] for (const nodeId of ctx.config.allNodeIds) { subTasks.push({ - // TODO: during node add, this seems to trip accidentally as true when message said that node isn't ACTIVE title: `Check node: ${chalk.yellow(nodeId)}`, task: () => self.checkNetworkNodeState(nodeId, 200) }) diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 64dcf0675..0eebce9a4 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { afterAll, describe, it } from '@jest/globals' +import { afterAll, describe, expect, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' import { constants } from '../../../src/core/index.mjs' import { @@ -23,6 +23,7 @@ import { HEDERA_PLATFORM_VERSION_TAG } from '../../test_util.js' import { getNodeLogs } from '../../../src/core/helpers.mjs' +import { NodeCommand } from '../../../src/commands/node.mjs' describe('Node add', () => { const TEST_NAMESPACE = 'node-add' @@ -52,5 +53,14 @@ describe('Node add', () => { argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM await nodeCmd.add(argv) + expect(nodeCmd.getUnusedConfigs(NodeCommand.ADD_CONFIGS_NAME)).toEqual([ + flags.apiPermissionProperties.constName, + flags.applicationProperties.constName, + flags.bootstrapProperties.constName, + flags.chainId.constName, + flags.devMode.constName, + flags.log4j2Xml.constName, + flags.settingTxt.constName + ]) }, 600000) }) diff --git a/test/e2e/core/platform_installer_e2e.test.mjs b/test/e2e/core/platform_installer_e2e.test.mjs index 97458a9c8..40769ff81 100644 --- a/test/e2e/core/platform_installer_e2e.test.mjs +++ b/test/e2e/core/platform_installer_e2e.test.mjs @@ -93,7 +93,7 @@ describe('PackageInstallerE2E', () => { expect(configLines.length).toBe(4) expect(configLines[0]).toBe(`swirld, ${chainId}`) expect(configLines[1]).toBe(`app, ${constants.HEDERA_APP_NAME}`) - expect(configLines[2]).toContain('address, 0, node0, node0, 1') + expect(configLines[2]).toContain(`address, 0, node0, node0, ${constants.HEDERA_NODE_DEFAULT_STAKE_AMOUNT}`) expect(configLines[3]).toBe('nextNodeId, 1') // verify the file exists From 36af0e137ebd7326e036e128c6736d40bd3122bf Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Fri, 9 Aug 2024 20:04:54 +0100 Subject: [PATCH 29/56] handle account info query returning key instead of array of keys Signed-off-by: Jeromy Cannon --- src/core/account_manager.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index ee59bfb4b..fde11c70d 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -476,6 +476,8 @@ export class AccountManager { let keys = [] if (accountInfo.key instanceof KeyList) { keys = accountInfo.key.toArray() + } else { + keys.push(accountInfo.key) } return keys From d8333a46adbbb4d0d300e253d2e777b13437daea Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:04:01 +0100 Subject: [PATCH 30/56] sleep for 30 seconds after starting nodes so that logs can roll over Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 5ebd30dc3..8c123cb3f 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -596,11 +596,6 @@ export class NodeCommand extends BaseCommand { if (!fs.existsSync(upgradeConfigDir)) { fs.mkdirSync(upgradeConfigDir, { recursive: true }) } - // const fileList = ['templates/application.properties'] - // fileList.forEach(filePath => { - // const fileName = path.basename(filePath) - // fs.copyFileSync(path.join(ctx.config.stagingDir, filePath), path.join(upgradeConfigDir, fileName)) - // }) // bump field hedera.config.version const fileBytes = fs.readFileSync(`${config.stagingDir}/templates/application.properties`) @@ -725,7 +720,6 @@ export class NodeCommand extends BaseCommand { subTasks.push({ title: `Start node: ${chalk.yellow(nodeId)}`, task: async () => { - // await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['bash', '-c', `rm -rf ${constants.HEDERA_HAPI_PATH}/output/*`]) await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) } }) @@ -2118,8 +2112,10 @@ export class NodeCommand extends BaseCommand { }, { title: 'Check all nodes are ACTIVE', - task: (ctx, task) => { + task: async (ctx, task) => { const subTasks = [] + // sleep for 30 seconds to give time for the logs to roll over to prevent capturing an invalid "ACTIVE" string + await sleep(30000) for (const nodeId of ctx.config.allNodeIds) { subTasks.push({ title: `Check node: ${chalk.yellow(nodeId)}`, From 79bc289364b5447b6f1801de71376e89abde74ed Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:11:01 +0100 Subject: [PATCH 31/56] removed account balance info, as it is now staked to node id instead of account Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 8c123cb3f..d05a02288 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -208,9 +208,6 @@ export class NodeCommand extends BaseCommand { // Get the transaction status const transactionStatus = receipt.status this.logger.debug(`The transaction consensus status is ${transactionStatus.toString()}`) - - const accountInfo = await this.accountManager.accountInfoQuery(accountId) - this.logger.info(`Account ID: ${accountId}, nodeId: ${nodeId - 1}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) } catch (e) { throw new FullstackTestingError(`Error in adding stake: ${e.message}`, e) } From 325ffb8c8816a7fbfab4361686f60ab84307bded Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:12:59 +0100 Subject: [PATCH 32/56] removed console.log Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index d05a02288..e2c875911 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -185,7 +185,7 @@ export class NodeCommand extends BaseCommand { const balance = await new AccountBalanceQuery() .setAccountId(accountId) .execute(client) - console.log(`Account ${accountId} balance: ${balance.hbars}`) + this.logger.debug(`Account ${accountId} balance: ${balance.hbars}`) // Create the transaction const transaction = await new AccountUpdateTransaction() From 5d1386c8952369ee98d386fecee08ff5ab67d7c7 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:14:17 +0100 Subject: [PATCH 33/56] remove unused import Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index e2c875911..9a35520e0 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -35,7 +35,6 @@ import { FileAppendTransaction, FreezeTransaction, FreezeType, - Hbar, HbarUnit, ServiceEndpoint, Timestamp, From d3b2160b20382eee946cbb17db541bcc4ae7ea9c Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:25:00 +0100 Subject: [PATCH 34/56] cleanup use of ctx.config Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 9a35520e0..9248582dc 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -567,7 +567,7 @@ export class NodeCommand extends BaseCommand { for (const nodeId of ctx.config.nodeIds) { const podName = ctx.config.podNames[nodeId] subTasks.push({ - title: `Update node: ${chalk.yellow(nodeId)} [ platformVersion = ${ctx.config.releaseTag} ]`, + title: `Update node: ${chalk.yellow(nodeId)} [ platformVersion = ${config.releaseTag} ]`, task: () => platformInstaller.fetchPlatform(podName, config.releaseTag) }) @@ -582,7 +582,7 @@ export class NodeCommand extends BaseCommand { }) } - async prepareUpgradeZip (config) { + async prepareUpgradeZip (/** @type {NodeAddConfigClass} **/ config) { // we build a mock upgrade.zip file as we really don't need to upgrade the network // also the platform zip file is ~80Mb in size requiring a lot of transactions since the max // transaction size is 6Kb and in practice we need to send the file as 4Kb chunks. @@ -613,7 +613,7 @@ export class NodeCommand extends BaseCommand { return await zipper.zip(`${config.stagingDir}/mock-upgrade`, `${config.stagingDir}/mock-upgrade.zip`) } - async uploadUpgradeZip (config, upgradeZipFile, nodeClient) { + async uploadUpgradeZip (upgradeZipFile, nodeClient) { // get byte value of the zip file const zipBytes = fs.readFileSync(upgradeZipFile) const zipHash = crypto.createHash('sha384').update(zipBytes).digest('hex') @@ -684,7 +684,7 @@ export class NodeCommand extends BaseCommand { } } - async freezeUpgradeNetworkNodes (/** @type {NodeAddConfigClass} **/ config, upgradeZipHash, client) { + async freezeUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { try { const futureDate = new Date() this.logger.debug(`Current time: ${futureDate}`) @@ -692,7 +692,7 @@ export class NodeCommand extends BaseCommand { futureDate.setTime(futureDate.getTime() + 5000) // 5 seconds in the future this.logger.debug(`Freeze time: ${futureDate}`) - client.setOperator(FREEZE_ADMIN_ACCOUNT, config.freezeAdminPrivateKey) + client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) const freezeUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.FreezeUpgrade) .setStartTimestamp(Timestamp.fromDate(futureDate)) @@ -710,9 +710,9 @@ export class NodeCommand extends BaseCommand { } } - startNodes (config, nodeIds, subTasks) { + startNodes (podNames, nodeIds, subTasks) { for (const nodeId of nodeIds) { - const podName = config.podNames[nodeId] + const podName = podNames[nodeId] subTasks.push({ title: `Start node: ${chalk.yellow(nodeId)}`, task: async () => { @@ -722,29 +722,29 @@ export class NodeCommand extends BaseCommand { } } - async copyGossipKeysToStaging (config, nodeIds) { + async copyGossipKeysToStaging (keyFormat, keysDir, stagingKeysDir, nodeIds) { // copy gossip keys to the staging for (const nodeId of nodeIds) { - switch (config.keyFormat) { + switch (keyFormat) { case constants.KEY_FORMAT_PEM: { - const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.SIGNING_KEY_PREFIX) - await this._copyNodeKeys(signingKeyFiles, config.stagingKeysDir) + const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.SIGNING_KEY_PREFIX) + await this._copyNodeKeys(signingKeyFiles, stagingKeysDir) // generate missing agreement keys - const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, config.keysDir, constants.AGREEMENT_KEY_PREFIX) - await this._copyNodeKeys(agreementKeyFiles, config.stagingKeysDir) + const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.AGREEMENT_KEY_PREFIX) + await this._copyNodeKeys(agreementKeyFiles, stagingKeysDir) break } case constants.KEY_FORMAT_PFX: { const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) - fs.cpSync(`${config.keysDir}/${privateKeyFile}`, `${config.stagingKeysDir}/${privateKeyFile}`) - fs.cpSync(`${config.keysDir}/${constants.PUBLIC_PFX}`, `${config.stagingKeysDir}/${constants.PUBLIC_PFX}`) + fs.cpSync(`${keysDir}/${privateKeyFile}`, `${stagingKeysDir}/${privateKeyFile}`) + fs.cpSync(`${keysDir}/${constants.PUBLIC_PFX}`, `${stagingKeysDir}/${constants.PUBLIC_PFX}`) break } default: - throw new FullstackTestingError(`Unsupported key-format ${config.keyFormat}`) + throw new FullstackTestingError(`Unsupported key-format ${keyFormat}`) } } } @@ -944,7 +944,7 @@ export class NodeCommand extends BaseCommand { { title: 'Copy Gossip keys to staging', task: async (ctx, _) => { - await this.copyGossipKeysToStaging(ctx.config, ctx.config.nodeIds) + await this.copyGossipKeysToStaging(ctx.config.keyFormat, ctx.config.keysDir, ctx.config.stagingKeysDir, ctx.config.nodeIds) } }, { @@ -1068,7 +1068,7 @@ export class NodeCommand extends BaseCommand { title: 'Starting nodes', task: (ctx, task) => { const subTasks = [] - self.startNodes(ctx.config, ctx.config.nodeIds, subTasks) + self.startNodes(ctx.config.podNames, ctx.config.nodeIds, subTasks) // set up the sub-tasks return task.newListr(subTasks, { @@ -1477,7 +1477,7 @@ export class NodeCommand extends BaseCommand { title: 'Starting nodes', task: (ctx, task) => { const subTasks = [] - self.startNodes(ctx.config, ctx.config.nodeIds, subTasks) + self.startNodes(ctx.config.podNames, ctx.config.nodeIds, subTasks) // set up the sub-tasks return task.newListr(subTasks, { @@ -1859,7 +1859,7 @@ export class NodeCommand extends BaseCommand { const config = /** @type {NodeAddConfigClass} **/ ctx.config ctx.nodeClient = await this.accountManager.loadNodeClient(config.namespace) ctx.upgradeZipFile = await this.prepareUpgradeZip(config) - ctx.upgradeZipHash = await this.uploadUpgradeZip(config, ctx.upgradeZipFile, ctx.nodeClient) + ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.upgradeZipFile, ctx.nodeClient) } }, { @@ -1929,7 +1929,7 @@ export class NodeCommand extends BaseCommand { title: 'Send freeze upgrade transaction', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.freezeUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) + await this.freezeUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, ctx.nodeClient) } }, { @@ -2009,7 +2009,7 @@ export class NodeCommand extends BaseCommand { task: async (ctx, _) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.copyGossipKeysToStaging(config, config.allNodeIds) + await this.copyGossipKeysToStaging(config.keyFormat, config.keysDir, config.stagingKeysDir, config.allNodeIds) } }, { @@ -2094,7 +2094,7 @@ export class NodeCommand extends BaseCommand { task: (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config const subTasks = [] - self.startNodes(config, config.allNodeIds, subTasks) + self.startNodes(config.podNames, config.allNodeIds, subTasks) // set up the sub-tasks return task.newListr(subTasks, { From 544aa2da468fd6552d53cd7ce3d16b78f9c08c96 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 13:42:42 +0100 Subject: [PATCH 35/56] cleanup use of ctx.config Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 63 ++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 9248582dc..5ffee33ff 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -498,7 +498,7 @@ export class NodeCommand extends BaseCommand { } } - uploadPlatformSoftware (ctx, task, localBuildPath) { + uploadPlatformSoftware (nodeIds, podNames, task, localBuildPath) { const self = this const subTasks = [] @@ -517,8 +517,8 @@ export class NodeCommand extends BaseCommand { } let localDataLibBuildPath - for (const nodeId of ctx.config.nodeIds) { - const podName = ctx.config.podNames[nodeId] + for (const nodeId of nodeIds) { + const podName = podNames[nodeId] if (buildPathMap.has(nodeId)) { localDataLibBuildPath = buildPathMap.get(nodeId) } else { @@ -550,26 +550,24 @@ export class NodeCommand extends BaseCommand { }) } - fetchLocalOrReleasedPlatformSoftware (ctx, task) { + fetchLocalOrReleasedPlatformSoftware (nodeIds, podNames, releaseTag, task) { const self = this const localBuildPath = self.configManager.getFlag(flags.localBuildPath) if (localBuildPath !== '') { - return self.uploadPlatformSoftware(ctx, task, localBuildPath) + return self.uploadPlatformSoftware(nodeIds, podNames, task, localBuildPath) } else { - return self.fetchPlatformSoftware(ctx, task, self.platformInstaller) + return self.fetchPlatformSoftware(nodeIds, podNames, releaseTag, task, self.platformInstaller) } } - fetchPlatformSoftware (ctx, task, platformInstaller) { - const config = ctx.config - + fetchPlatformSoftware (nodeIds, podNames, releaseTag, task, platformInstaller) { const subTasks = [] - for (const nodeId of ctx.config.nodeIds) { - const podName = ctx.config.podNames[nodeId] + for (const nodeId of nodeIds) { + const podName = podNames[nodeId] subTasks.push({ - title: `Update node: ${chalk.yellow(nodeId)} [ platformVersion = ${config.releaseTag} ]`, + title: `Update node: ${chalk.yellow(nodeId)} [ platformVersion = ${releaseTag} ]`, task: () => - platformInstaller.fetchPlatform(podName, config.releaseTag) + platformInstaller.fetchPlatform(podName, releaseTag) }) } @@ -582,19 +580,19 @@ export class NodeCommand extends BaseCommand { }) } - async prepareUpgradeZip (/** @type {NodeAddConfigClass} **/ config) { + async prepareUpgradeZip (stagingDir) { // we build a mock upgrade.zip file as we really don't need to upgrade the network // also the platform zip file is ~80Mb in size requiring a lot of transactions since the max // transaction size is 6Kb and in practice we need to send the file as 4Kb chunks. // Note however that in DAB phase-2, we won't need to trigger this fake upgrade process const zipper = new Zippy(this.logger) - const upgradeConfigDir = `${config.stagingDir}/mock-upgrade/data/config` + const upgradeConfigDir = `${stagingDir}/mock-upgrade/data/config` if (!fs.existsSync(upgradeConfigDir)) { fs.mkdirSync(upgradeConfigDir, { recursive: true }) } // bump field hedera.config.version - const fileBytes = fs.readFileSync(`${config.stagingDir}/templates/application.properties`) + const fileBytes = fs.readFileSync(`${stagingDir}/templates/application.properties`) const lines = fileBytes.toString().split('\n') const newLines = [] for (let line of lines) { @@ -610,7 +608,7 @@ export class NodeCommand extends BaseCommand { } fs.writeFileSync(`${upgradeConfigDir}/application.properties`, newLines.join('\n')) - return await zipper.zip(`${config.stagingDir}/mock-upgrade`, `${config.stagingDir}/mock-upgrade.zip`) + return await zipper.zip(`${stagingDir}/mock-upgrade`, `${stagingDir}/mock-upgrade.zip`) } async uploadUpgradeZip (upgradeZipFile, nodeClient) { @@ -649,7 +647,7 @@ export class NodeCommand extends BaseCommand { } } - async prepareUpgradeNetworkNodes (/** @type {NodeAddConfigClass} **/ config, upgradeZipHash, client) { + async prepareUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { try { // transfer some tiny amount to the freeze admin account await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000) @@ -661,9 +659,7 @@ export class NodeCommand extends BaseCommand { this.logger.debug(`Freeze admin account balance: ${balance.hbars}`) // set operator of freeze transaction as freeze admin account - const accountKeys = await this.accountManager.getAccountKeysFromSecret(FREEZE_ADMIN_ACCOUNT, config.namespace) - config.freezeAdminPrivateKey = accountKeys.privateKey - client.setOperator(FREEZE_ADMIN_ACCOUNT, config.freezeAdminPrivateKey) + client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) const prepareUpgradeTx = await new FreezeTransaction() .setFreezeType(FreezeType.PrepareUpgrade) @@ -763,7 +759,7 @@ export class NodeCommand extends BaseCommand { await writeFile(configTxtPath, lines.join('\n')) } - prepareEndpoints (config, endpoints, defaultPort) { + prepareEndpoints (endpointType, endpoints, defaultPort) { const ret = /** @typedef ServiceEndpoint **/[] for (const endpoint of endpoints) { const parts = endpoint.split(':') @@ -780,7 +776,7 @@ export class NodeCommand extends BaseCommand { throw new FullstackTestingError(`incorrect endpoint format. expected url:port, found ${endpoint}`) } - if (config.endpointType.toUpperCase() === constants.ENDPOINT_TYPE_IP) { + if (endpointType.toUpperCase() === constants.ENDPOINT_TYPE_IP) { ret.push(new ServiceEndpoint({ port, ipAddressV4: helpers.parseIpAddressToUint8Array(url) @@ -849,6 +845,7 @@ export class NodeCommand extends BaseCommand { * @property {Date} curDate * @property {string} keysDir * @property {string[]} nodeIds + * @property {Object} podNames * @property {string} releasePrefix * @property {string} stagingDir * @property {string} stagingKeysDir @@ -867,6 +864,7 @@ export class NodeCommand extends BaseCommand { 'curDate', 'keysDir', 'nodeIds', + 'podNames', 'releasePrefix', 'stagingDir', 'stagingKeysDir' @@ -982,7 +980,8 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into network nodes', task: async (ctx, task) => { - return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) + const config = /** @type {NodeSetupConfigClass} **/ ctx.config + return self.fetchLocalOrReleasedPlatformSoftware(config.nodeIds, config.podNames, config.releaseTag, task) } }, { @@ -1432,7 +1431,7 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into network nodes', task: async (ctx, task) => { - return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) + return self.fetchLocalOrReleasedPlatformSoftware(ctx.config.nodeIds, ctx.config.podNames, ctx.config.releaseTag, task) } }, { @@ -1707,6 +1706,9 @@ export class NodeCommand extends BaseCommand { // initialize Node Client with existing network nodes prior to adding the new node which isn't functioning, yet await this.accountManager.loadNodeClient(ctx.config.namespace) + const accountKeys = await this.accountManager.getAccountKeysFromSecret(FREEZE_ADMIN_ACCOUNT, config.namespace) + config.freezeAdminPrivateKey = accountKeys.privateKey + self.logger.debug('Initialized config', { config }) } }, @@ -1822,7 +1824,7 @@ export class NodeCommand extends BaseCommand { endpoints = helpers.splitFlagInput(config.gossipEndpoints) } - ctx.gossipEndpoints = this.prepareEndpoints(config, endpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT) + ctx.gossipEndpoints = this.prepareEndpoints(config.endpointType, endpoints, constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT) } }, { @@ -1843,7 +1845,7 @@ export class NodeCommand extends BaseCommand { endpoints = helpers.splitFlagInput(config.grpcEndpoints) } - ctx.grpcServiceEndpoints = this.prepareEndpoints(config, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) + ctx.grpcServiceEndpoints = this.prepareEndpoints(config.endpointType, endpoints, constants.HEDERA_NODE_EXTERNAL_GOSSIP_PORT) } }, { @@ -1858,7 +1860,7 @@ export class NodeCommand extends BaseCommand { task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config ctx.nodeClient = await this.accountManager.loadNodeClient(config.namespace) - ctx.upgradeZipFile = await this.prepareUpgradeZip(config) + ctx.upgradeZipFile = await this.prepareUpgradeZip(config.stagingDir) ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.upgradeZipFile, ctx.nodeClient) } }, @@ -1906,7 +1908,7 @@ export class NodeCommand extends BaseCommand { title: 'Send prepare upgrade transaction', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.prepareUpgradeNetworkNodes(config, ctx.upgradeZipHash, ctx.nodeClient) + await this.prepareUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, ctx.nodeClient) } }, { @@ -2034,7 +2036,8 @@ export class NodeCommand extends BaseCommand { title: 'Fetch platform software into new network node', task: async (ctx, task) => { - return self.fetchLocalOrReleasedPlatformSoftware(ctx, task) + const config = /** @type {NodeAddConfigClass} **/ ctx.config + return self.fetchLocalOrReleasedPlatformSoftware(config.nodeIds, config.podNames, config.releaseTag, task) } }, { From ed43861ebc90b70854481623834703f0e7fe324d Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 14:00:13 +0100 Subject: [PATCH 36/56] cleanup use of ctx.config Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 5ffee33ff..659d9d5e8 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -1782,8 +1782,9 @@ export class NodeCommand extends BaseCommand { { title: 'Load signing key certificate', task: async (ctx, task) => { - const signingCertFile = Templates.renderGossipPemPublicKeyFile(constants.SIGNING_KEY_PREFIX, ctx.config.nodeId) - const signingCertFullPath = `${ctx.config.keysDir}/${signingCertFile}` + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const signingCertFile = Templates.renderGossipPemPublicKeyFile(constants.SIGNING_KEY_PREFIX, config.nodeId) + const signingCertFullPath = `${config.keysDir}/${signingCertFile}` const signingCertPem = fs.readFileSync(signingCertFullPath).toString() const decodedDers = x509.PemConverter.decode(signingCertPem) if (!decodedDers || decodedDers.length === 0) { @@ -1795,8 +1796,9 @@ export class NodeCommand extends BaseCommand { { title: 'Compute mTLS certificate hash', task: async (ctx, task) => { - const tlsCertFile = Templates.renderTLSPemPublicKeyFile(ctx.config.nodeId) - const tlsCertFullPath = `${ctx.config.keysDir}/${tlsCertFile}` + const config = /** @type {NodeAddConfigClass} **/ ctx.config + const tlsCertFile = Templates.renderTLSPemPublicKeyFile(config.nodeId) + const tlsCertFullPath = `${config.keysDir}/${tlsCertFile}` const tlsCertPem = fs.readFileSync(tlsCertFullPath).toString() const tlsCertDers = x509.PemConverter.decode(tlsCertPem) if (!tlsCertDers || tlsCertDers.length === 0) { @@ -1867,13 +1869,11 @@ export class NodeCommand extends BaseCommand { { title: 'Check existing nodes staked amount', task: async (ctx, task) => { - // const config = /** @type {NodeAddConfigClass} **/ ctx.config + const config = /** @type {NodeAddConfigClass} **/ ctx.config await sleep(60000) - const accountMap = getNodeAccountMap(ctx.config.existingNodeIds) - for (const nodeId of ctx.config.existingNodeIds) { + const accountMap = getNodeAccountMap(config.existingNodeIds) + for (const nodeId of config.existingNodeIds) { const accountId = accountMap.get(nodeId) - const accountInfo = await this.accountManager.accountInfoQuery(accountId) - this.logger.info(`Account ID: ${accountId}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1) } } @@ -2164,15 +2164,15 @@ export class NodeCommand extends BaseCommand { } }, { - title: 'Check existing nodes staked amount', + title: 'Trigger stake weight calculate', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config + // sleep 60 seconds for the handler to be able to trigger the network node stake weight recalculate await sleep(60000) const accountMap = getNodeAccountMap(config.allNodeIds) + // send some write transactions to invoke the handler that will trigger the stake weight recalculate for (const nodeId of config.allNodeIds) { const accountId = accountMap.get(nodeId) - const accountInfo = await this.accountManager.accountInfoQuery(accountId) - this.logger.info(`Account ID: ${accountId}, amount staked: ${accountInfo.stakingInfo.stakedToMe.toString(HbarUnit.Hbar)}`) await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1) } } From f8fff769970f9ed41027f8d56d7bc7ebc6959feb Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 14:05:21 +0100 Subject: [PATCH 37/56] revert location of methods to prevent merge conflict issues with other inflight PRs Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 224 +++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 112 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 659d9d5e8..e9b5f9dbf 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -647,118 +647,6 @@ export class NodeCommand extends BaseCommand { } } - async prepareUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { - try { - // transfer some tiny amount to the freeze admin account - await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000) - - // query the balance - const balance = await new AccountBalanceQuery() - .setAccountId(FREEZE_ADMIN_ACCOUNT) - .execute(this.accountManager._nodeClient) - this.logger.debug(`Freeze admin account balance: ${balance.hbars}`) - - // set operator of freeze transaction as freeze admin account - client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) - - const prepareUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.PrepareUpgrade) - .setFileId(constants.UPGRADE_FILE_ID) - .setFileHash(upgradeZipHash) - .freezeWith(client) - .execute(client) - - const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) - - this.logger.debug( - `sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, - prepareUpgradeReceipt.status.toString() - ) - } catch (e) { - this.logger.error(`Error in prepare upgrade: ${e.message}`, e) - throw new FullstackTestingError(`Error in prepare upgrade: ${e.message}`, e) - } - } - - async freezeUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { - try { - const futureDate = new Date() - this.logger.debug(`Current time: ${futureDate}`) - - futureDate.setTime(futureDate.getTime() + 5000) // 5 seconds in the future - this.logger.debug(`Freeze time: ${futureDate}`) - - client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) - const freezeUpgradeTx = await new FreezeTransaction() - .setFreezeType(FreezeType.FreezeUpgrade) - .setStartTimestamp(Timestamp.fromDate(futureDate)) - .setFileId(constants.UPGRADE_FILE_ID) - .setFileHash(upgradeZipHash) - .freezeWith(client) - .execute(client) - - const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(client) - this.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, - freezeUpgradeReceipt.status.toString()) - } catch (e) { - this.logger.error(`Error in freeze upgrade: ${e.message}`, e) - throw new FullstackTestingError(`Error in freeze upgrade: ${e.message}`, e) - } - } - - startNodes (podNames, nodeIds, subTasks) { - for (const nodeId of nodeIds) { - const podName = podNames[nodeId] - subTasks.push({ - title: `Start node: ${chalk.yellow(nodeId)}`, - task: async () => { - await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) - } - }) - } - } - - async copyGossipKeysToStaging (keyFormat, keysDir, stagingKeysDir, nodeIds) { - // copy gossip keys to the staging - for (const nodeId of nodeIds) { - switch (keyFormat) { - case constants.KEY_FORMAT_PEM: { - const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.SIGNING_KEY_PREFIX) - await this._copyNodeKeys(signingKeyFiles, stagingKeysDir) - - // generate missing agreement keys - const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.AGREEMENT_KEY_PREFIX) - await this._copyNodeKeys(agreementKeyFiles, stagingKeysDir) - break - } - - case constants.KEY_FORMAT_PFX: { - const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) - fs.cpSync(`${keysDir}/${privateKeyFile}`, `${stagingKeysDir}/${privateKeyFile}`) - fs.cpSync(`${keysDir}/${constants.PUBLIC_PFX}`, `${stagingKeysDir}/${constants.PUBLIC_PFX}`) - break - } - - default: - throw new FullstackTestingError(`Unsupported key-format ${keyFormat}`) - } - } - } - - async bumpHederaConfigVersion (configTxtPath) { - const lines = (await readFile(configTxtPath, 'utf-8')).split('\n') - - for (const line of lines) { - if (line.startsWith('hedera.config.version=')) { - const version = parseInt(line.split('=')[1]) + 1 - lines[lines.indexOf(line)] = `hedera.config.version=${version}` - break - } - } - - await writeFile(configTxtPath, lines.join('\n')) - } - prepareEndpoints (endpointType, endpoints, defaultPort) { const ret = /** @typedef ServiceEndpoint **/[] for (const endpoint of endpoints) { @@ -2202,6 +2090,104 @@ export class NodeCommand extends BaseCommand { return true } + async prepareUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { + try { + // transfer some tiny amount to the freeze admin account + await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, FREEZE_ADMIN_ACCOUNT, 100000) + + // query the balance + const balance = await new AccountBalanceQuery() + .setAccountId(FREEZE_ADMIN_ACCOUNT) + .execute(this.accountManager._nodeClient) + this.logger.debug(`Freeze admin account balance: ${balance.hbars}`) + + // set operator of freeze transaction as freeze admin account + client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) + + const prepareUpgradeTx = await new FreezeTransaction() + .setFreezeType(FreezeType.PrepareUpgrade) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(client) + .execute(client) + + const prepareUpgradeReceipt = await prepareUpgradeTx.getReceipt(client) + + this.logger.debug( + `sent prepare upgrade transaction [id: ${prepareUpgradeTx.transactionId.toString()}]`, + prepareUpgradeReceipt.status.toString() + ) + } catch (e) { + this.logger.error(`Error in prepare upgrade: ${e.message}`, e) + throw new FullstackTestingError(`Error in prepare upgrade: ${e.message}`, e) + } + } + + async freezeUpgradeNetworkNodes (freezeAdminPrivateKey, upgradeZipHash, client) { + try { + const futureDate = new Date() + this.logger.debug(`Current time: ${futureDate}`) + + futureDate.setTime(futureDate.getTime() + 5000) // 5 seconds in the future + this.logger.debug(`Freeze time: ${futureDate}`) + + client.setOperator(FREEZE_ADMIN_ACCOUNT, freezeAdminPrivateKey) + const freezeUpgradeTx = await new FreezeTransaction() + .setFreezeType(FreezeType.FreezeUpgrade) + .setStartTimestamp(Timestamp.fromDate(futureDate)) + .setFileId(constants.UPGRADE_FILE_ID) + .setFileHash(upgradeZipHash) + .freezeWith(client) + .execute(client) + + const freezeUpgradeReceipt = await freezeUpgradeTx.getReceipt(client) + this.logger.debug(`Upgrade frozen with transaction id: ${freezeUpgradeTx.transactionId.toString()}`, + freezeUpgradeReceipt.status.toString()) + } catch (e) { + this.logger.error(`Error in freeze upgrade: ${e.message}`, e) + throw new FullstackTestingError(`Error in freeze upgrade: ${e.message}`, e) + } + } + + startNodes (podNames, nodeIds, subTasks) { + for (const nodeId of nodeIds) { + const podName = podNames[nodeId] + subTasks.push({ + title: `Start node: ${chalk.yellow(nodeId)}`, + task: async () => { + await this.k8.execContainer(podName, constants.ROOT_CONTAINER, ['systemctl', 'restart', 'network-node']) + } + }) + } + } + + async copyGossipKeysToStaging (keyFormat, keysDir, stagingKeysDir, nodeIds) { + // copy gossip keys to the staging + for (const nodeId of nodeIds) { + switch (keyFormat) { + case constants.KEY_FORMAT_PEM: { + const signingKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.SIGNING_KEY_PREFIX) + await this._copyNodeKeys(signingKeyFiles, stagingKeysDir) + + // generate missing agreement keys + const agreementKeyFiles = this.keyManager.prepareNodeKeyFilePaths(nodeId, keysDir, constants.AGREEMENT_KEY_PREFIX) + await this._copyNodeKeys(agreementKeyFiles, stagingKeysDir) + break + } + + case constants.KEY_FORMAT_PFX: { + const privateKeyFile = Templates.renderGossipPfxPrivateKeyFile(nodeId) + fs.cpSync(`${keysDir}/${privateKeyFile}`, `${stagingKeysDir}/${privateKeyFile}`) + fs.cpSync(`${keysDir}/${constants.PUBLIC_PFX}`, `${stagingKeysDir}/${constants.PUBLIC_PFX}`) + break + } + + default: + throw new FullstackTestingError(`Unsupported key-format ${keyFormat}`) + } + } + } + // Command Definition /** * Return Yargs command definition for 'node' command @@ -2347,4 +2333,18 @@ export class NodeCommand extends BaseCommand { } } } + + async bumpHederaConfigVersion (configTxtPath) { + const lines = (await readFile(configTxtPath, 'utf-8')).split('\n') + + for (const line of lines) { + if (line.startsWith('hedera.config.version=')) { + const version = parseInt(line.split('=')[1]) + 1 + lines[lines.indexOf(line)] = `hedera.config.version=${version}` + break + } + } + + await writeFile(configTxtPath, lines.join('\n')) + } } From 5057474e7dda57d19d7cee3e7d3df23bf338508f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 14:08:16 +0100 Subject: [PATCH 38/56] removed commented out code Signed-off-by: Jeromy Cannon --- src/core/profile_manager.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/profile_manager.mjs b/src/core/profile_manager.mjs index 5a36afe3d..3931ff18a 100644 --- a/src/core/profile_manager.mjs +++ b/src/core/profile_manager.mjs @@ -156,10 +156,6 @@ export class ProfileManager { this._setValue(`hedera.nodes.${nodeIndex}.name`, nodeIds[nodeIndex], yamlRoot) this._setValue(`hedera.nodes.${nodeIndex}.accountId`, accountMap.get(nodeIds[nodeIndex]), yamlRoot) this._setChartItems(`hedera.nodes.${nodeIndex}`, profile.consensus, yamlRoot) - // if (nodeIndex === 0) { - // this._setValue(`hedera.nodes.${nodeIndex}.root.extraEnv.0.name`, 'JAVA_OPTS', yamlRoot) - // this._setValue(`hedera.nodes.${nodeIndex}.root.extraEnv.0.value`, '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005', yamlRoot) - // } } if (profile.consensus) { From 05d2b3342245f5ea70773cf86edaba7232bd76ae Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 14:11:52 +0100 Subject: [PATCH 39/56] captured issue and removed TODO Signed-off-by: Jeromy Cannon --- test/e2e/commands/node-add.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 0eebce9a4..c5facd1e0 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -47,7 +47,7 @@ describe('Node add', () => { }, 120000) it('should add a new node to the network successfully', async () => { - argv[flags.nodeID.name] = 'node4' // TODO: open an issue: node ID cannot have a hyphen, platform strips it out, also, can't have capital letters + argv[flags.nodeID.name] = 'node4' argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM From afde04afb56051a13fab7aea87e66decc1332818 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 21:18:38 +0100 Subject: [PATCH 40/56] updated builds for node-add Signed-off-by: Jeromy Cannon --- .github/workflows/flow-build-application.yaml | 47 +- .../workflows/flow-pull-request-checks.yaml | 79 ++-- .github/workflows/zxc-code-analysis.yaml | 53 ++- .github/workflows/zxc-env-vars.yaml | 40 +- package.json | 5 +- resources/support-zip.sh | 43 ++ src/commands/node.mjs | 14 +- src/core/helpers.mjs | 29 +- test/e2e/commands/account.test.mjs | 3 +- test/e2e/commands/cluster.test.mjs | 1 + test/e2e/commands/node-add.test.mjs | 61 ++- test/e2e/commands/node-local-ptt.test.mjs | 1 + ...op_add.test.mjs => node_pem_stop.test.mjs} | 4 +- ...ll_add.test.mjs => node_pfx_kill.test.mjs} | 4 +- test/e2e/e2e_node_util.js | 426 ++++++------------ test/test_util.js | 98 +++- 16 files changed, 498 insertions(+), 410 deletions(-) create mode 100644 resources/support-zip.sh rename test/e2e/commands/{node_pem_stop_add.test.mjs => node_pem_stop.test.mjs} (84%) rename test/e2e/commands/{node_pfx_kill_add.test.mjs => node_pfx_kill.test.mjs} (84%) diff --git a/.github/workflows/flow-build-application.yaml b/.github/workflows/flow-build-application.yaml index 3e1bc2386..5a90a28bc 100644 --- a/.github/workflows/flow-build-application.yaml +++ b/.github/workflows/flow-build-application.yaml @@ -91,7 +91,7 @@ jobs: coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} coverage-report-name: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} - e2e-node-pem-stop-add-tests: + e2e-node-pem-stop-tests: name: E2E Tests if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml @@ -99,12 +99,12 @@ jobs: - env-vars - code-style with: - custom-job-label: Node PEM Stop Add - npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} + custom-job-label: Node PEM Stop + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} - e2e-node-pfx-kill-add-tests: + e2e-node-pfx-kill-tests: name: E2E Tests if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml @@ -112,10 +112,10 @@ jobs: - env-vars - code-style with: - custom-job-label: Node PFX Kill Add - npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} + custom-job-label: Node PFX Kill + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} e2e-node-local-build-tests: name: E2E Tests @@ -129,6 +129,18 @@ jobs: coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} + e2e-node-add-tests: + name: E2E Tests + uses: ./.github/workflows/zxc-e2e-test.yaml + needs: + - env-vars + - code-style + with: + custom-job-label: Node Add + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} + e2e-relay-tests: name: E2E Tests if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} @@ -151,9 +163,10 @@ jobs: - unit-tests - e2e-tests - e2e-mirror-node-tests - - e2e-node-pem-stop-add-tests - - e2e-node-pfx-kill-add-tests + - e2e-node-pem-stop-tests + - e2e-node-pfx-kill-tests - e2e-node-local-build-tests + - e2e-node-add-tests - e2e-relay-tests if: ${{ (github.event_name == 'push' || github.event.inputs.enable-unit-tests == 'true' || github.event.inputs.enable-e2e-tests == 'true') && !failure() && !cancelled() }} with: @@ -164,15 +177,17 @@ jobs: enable-e2e-coverage-report: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} - e2e-node-pem-stop-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - e2e-node-pfx-kill-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} + e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + e2e-node-pfx-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} e2e-node-local-build-test-subdir: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} + e2e-node-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} - e2e-node-pem-stop-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} - e2e-node-pfx-kill-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} + e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} + e2e-node-pfx-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} e2e-node-local-build-coverage-report: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} + e2e-node-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} secrets: snyk-token: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/flow-pull-request-checks.yaml b/.github/workflows/flow-pull-request-checks.yaml index dc9ef43f0..c6079ef48 100644 --- a/.github/workflows/flow-pull-request-checks.yaml +++ b/.github/workflows/flow-pull-request-checks.yaml @@ -78,29 +78,29 @@ jobs: coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} coverage-report-name: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} - e2e-node-pem-stop-add-tests: + e2e-node-pem-stop-tests: name: E2E Tests uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars - code-style with: - custom-job-label: Node PEM Stop Add - npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} + custom-job-label: Node PEM Stop + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} - e2e-node-pfx-kill-add-tests: + e2e-node-pfx-kill-tests: name: E2E Tests uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars - code-style with: - custom-job-label: Node PFX Kill Add - npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} + custom-job-label: Node PFX Kill + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} e2e-node-local-build-tests: name: E2E Tests @@ -114,6 +114,18 @@ jobs: coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} + e2e-node-add-tests: + name: E2E Tests + uses: ./.github/workflows/zxc-e2e-test.yaml + needs: + - env-vars + - code-style + with: + custom-job-label: Node Add + npm-test-script: test-${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} + e2e-relay-tests: name: E2E Tests if: ${{ !cancelled() && always() }} @@ -136,9 +148,10 @@ jobs: - unit-tests - e2e-tests - e2e-mirror-node-tests - - e2e-node-pem-stop-add-tests - - e2e-node-pfx-kill-add-tests + - e2e-node-pem-stop-tests + - e2e-node-pfx-kill-tests - e2e-node-local-build-tests + - e2e-node-add-tests - e2e-relay-tests if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} with: @@ -147,15 +160,17 @@ jobs: enable-e2e-coverage-report: true e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} - e2e-node-pem-stop-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - e2e-node-pfx-kill-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} + e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + e2e-node-pfx-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} e2e-node-local-build-test-subdir: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} + e2e-node-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} - e2e-node-pem-stop-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} - e2e-node-pfx-kill-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} + e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} + e2e-node-pfx-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} e2e-node-local-build-coverage-report: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} + e2e-node-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} @@ -168,9 +183,10 @@ jobs: - unit-tests - e2e-tests - e2e-mirror-node-tests - - e2e-node-pem-stop-add-tests - - e2e-node-pfx-kill-add-tests + - e2e-node-pem-stop-tests + - e2e-node-pfx-kill-tests - e2e-node-local-build-tests + - e2e-node-add-tests - e2e-relay-tests if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} with: @@ -179,15 +195,17 @@ jobs: enable-e2e-coverage-report: true e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} - e2e-node-pem-stop-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} - e2e-node-pfx-kill-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} + e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} + e2e-node-pfx-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} e2e-node-local-build-test-subdir: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} + e2e-node-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} - e2e-node-pem-stop-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} - e2e-node-pfx-kill-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} + e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} + e2e-node-pfx-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} e2e-node-local-build-coverage-report: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} + e2e-node-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} secrets: codacy-project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} @@ -200,8 +218,10 @@ jobs: # - unit-tests # - e2e-tests # - e2e-mirror-node-tests -# - e2e-node-pem-stop-add-tests -# - e2e-node-pfx-kill-add-tests +# - e2e-node-pem-stop-tests +# - e2e-node-pfx-kill-tests +# - e2e-node-local-build-tests +# - e2e-node-add-tests # - e2e-relay-tests # if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name && github.actor != 'dependabot[bot]' }} # with: @@ -209,14 +229,17 @@ jobs: # enable-snyk-scan: true # e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} # e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} -# e2e-node-pem-stop-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-test-subdir }} -# e2e-node-pfx-kill-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-test-subdir }} +# e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} +# e2e-node-pfx-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-test-subdir }} +# e2e-node-local-build-test-subdir: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} +# e2e-node-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} # e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} # e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} # e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} -# e2e-node-pem-stop-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-add-coverage-report }} -# e2e-node-pfx-kill-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-add-coverage-report }} +# e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} +# e2e-node-pfx-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pfx-kill-coverage-report }} # e2e-node-local-build-coverage-report: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} +# e2e-node-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} # e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} # secrets: # snyk-token: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/zxc-code-analysis.yaml b/.github/workflows/zxc-code-analysis.yaml index 5033009f5..c252e651e 100644 --- a/.github/workflows/zxc-code-analysis.yaml +++ b/.github/workflows/zxc-code-analysis.yaml @@ -65,21 +65,26 @@ on: type: string required: false default: "e2e-mirror-node" - e2e-node-pem-stop-add-test-subdir: - description: "E2E Node PEM Stop Add Test Subdirectory:" + e2e-node-pem-stop-test-subdir: + description: "E2E Node PEM Stop Test Subdirectory:" type: string required: false - default: "e2e-node-pem-stop-add" - e2e-node-pfx-kill-add-test-subdir: - description: "E2E Node PFX Kill Add Test Subdirectory:" + default: "e2e-node-pem-stop" + e2e-node-pfx-kill-test-subdir: + description: "E2E Node PFX Kill Test Subdirectory:" type: string required: false - default: "e2e-node-pfx-kill-add" + default: "e2e-node-pfx-kill" e2e-node-local-build-test-subdir: description: "E2E Node Local Build Test Subdirectory:" type: string required: false default: "e2e-node-local-build" + e2e-node-add-test-subdir: + description: "E2E Node Add Test Subdirectory:" + type: string + required: false + default: "e2e-node-add" e2e-relay-test-subdir: description: "E2E Relay Test Subdirectory:" type: string @@ -95,21 +100,26 @@ on: type: string required: false default: "E2E Mirror Node Tests Coverage Report" - e2e-node-pem-stop-add-coverage-report: - description: "E2E Node PEM Stop Add Coverage Report:" + e2e-node-pem-stop-coverage-report: + description: "E2E Node PEM Stop Coverage Report:" type: string required: false - default: "E2E Node PEM Stop Add Tests Coverage Report" - e2e-node-pfx-kill-add-coverage-report: - description: "E2E Node PFX Kill Add Coverage Report:" + default: "E2E Node PEM Stop Tests Coverage Report" + e2e-node-pfx-kill-coverage-report: + description: "E2E Node PFX Kill Coverage Report:" type: string required: false - default: "E2E Node PFX Kill Add Tests Coverage Report" + default: "E2E Node PFX Kill Tests Coverage Report" e2e-node-local-build-coverage-report: description: "E2E Node Local Build Coverage Report:" type: string required: false default: "E2E Node Local Build Tests Coverage Report" + e2e-node-add-coverage-report: + description: "E2E Node Add Coverage Report:" + type: string + required: false + default: "E2E Node Add Tests Coverage Report" e2e-relay-coverage-report: description: "E2E Relay Coverage Report:" type: string @@ -179,19 +189,19 @@ jobs: name: ${{ inputs.e2e-mirror-node-coverage-report }} path: 'coverage/${{ inputs.e2e-mirror-node-test-subdir }}' - - name: Download E2E Node PEM Stop Add Coverage Report + - name: Download E2E Node PEM Stop Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-node-pem-stop-add-coverage-report }} - path: 'coverage/${{ inputs.e2e-node-pem-stop-add-test-subdir }}' + name: ${{ inputs.e2e-node-pem-stop-coverage-report }} + path: 'coverage/${{ inputs.e2e-node-pem-stop-test-subdir }}' - - name: Download E2E Node PFX Kill Add Coverage Report + - name: Download E2E Node PFX Kill Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-node-pfx-kill-add-coverage-report }} - path: 'coverage/${{ inputs.e2e-node-pfx-kill-add-test-subdir }}' + name: ${{ inputs.e2e-node-pfx-kill-coverage-report }} + path: 'coverage/${{ inputs.e2e-node-pfx-kill-test-subdir }}' - name: Download E2E Relay Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 @@ -207,6 +217,13 @@ jobs: name: ${{ inputs.e2e-node-local-build-test-coverage-report }} path: 'coverage/${{ inputs.e2e-node-local-build-test-subdir }}' + - name: Download E2E Add Coverage Report + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} + with: + name: ${{ inputs.e2e-node-add-test-coverage-report }} + path: 'coverage/${{ inputs.e2e-node-add-test-subdir }}' + - name: Publish To Codecov uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 if: ${{ inputs.enable-codecov-analysis && !cancelled() && !failure() }} diff --git a/.github/workflows/zxc-env-vars.yaml b/.github/workflows/zxc-env-vars.yaml index 45cea6797..6647e240f 100644 --- a/.github/workflows/zxc-env-vars.yaml +++ b/.github/workflows/zxc-env-vars.yaml @@ -32,15 +32,18 @@ on: e2e-mirror-node-test-subdir: description: "E2E Mirror Node Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_mirror_node_test_subdir }} - e2e-node-pem-stop-add-test-subdir: - description: "E2E Node PEM Stop Add Test Subdirectory" - value: ${{ jobs.env-vars.outputs.e2e_node_pem_stop_add_test_subdir }} - e2e-node-pfx-kill-add-test-subdir: - description: "E2E Node PFX Kill Add Test Subdirectory" - value: ${{ jobs.env-vars.outputs.e2e_node_pfx_kill_add_test_subdir }} + e2e-node-pem-stop-test-subdir: + description: "E2E Node PEM Stop Test Subdirectory" + value: ${{ jobs.env-vars.outputs.e2e_node_pem_stop_test_subdir }} + e2e-node-pfx-kill-test-subdir: + description: "E2E Node PFX Kill Test Subdirectory" + value: ${{ jobs.env-vars.outputs.e2e_node_pfx_kill_test_subdir }} e2e-node-local-build-test-subdir: description: "E2E Node Local Build Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_node_local_build_test_subdir }} + e2e-node-add-test-subdir: + description: "E2E Node Add Test Subdirectory" + value: ${{ jobs.env-vars.outputs.e2e_node_add_test_subdir }} e2e-relay-test-subdir: description: "E2E Relay Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_relay_test_subdir }} @@ -50,15 +53,18 @@ on: e2e-mirror-node-coverage-report: description: "E2E Mirror Node Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_mirror_node_coverage_report }} - e2e-node-pem-stop-add-coverage-report: - description: "E2E Node PEM Stop Add Tests Coverage Report" - value: ${{ jobs.env-vars.outputs.e2e_node_pem_stop_add_coverage_report }} - e2e-node-pfx-kill-add-coverage-report: - description: "E2E Node PFX Kill Add Tests Coverage Report" - value: ${{ jobs.env-vars.outputs.e2e_node_pfx_kill_add_coverage_report }} + e2e-node-pem-stop-coverage-report: + description: "E2E Node PEM Stop Tests Coverage Report" + value: ${{ jobs.env-vars.outputs.e2e_node_pem_stop_coverage_report }} + e2e-node-pfx-kill-coverage-report: + description: "E2E Node PFX Kill Tests Coverage Report" + value: ${{ jobs.env-vars.outputs.e2e_node_pfx_kill_coverage_report }} e2e-node-local-build-coverage-report: description: "E2E Node Local Build Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_node_local_build_coverage_report }} + e2e-node-add-coverage-report: + description: "E2E Node Add Tests Coverage Report" + value: ${{ jobs.env-vars.outputs.e2e_node_add_coverage_report }} e2e-relay-coverage-report: description: "E2E Relay Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_relay_coverage_report }} @@ -74,15 +80,17 @@ jobs: outputs: e2e_test_subdir: e2e e2e_mirror_node_test_subdir: e2e-mirror-node - e2e_node_pem_stop_add_test_subdir: e2e-node-pem-stop-add - e2e_node_pfx_kill_add_test_subdir: e2e-node-pfx-kill-add + e2e_node_pem_stop_test_subdir: e2e-node-pem-stop + e2e_node_pfx_kill_test_subdir: e2e-node-pfx-kill e2e_node_local_build_test_subdir: e2e-node-local-build + e2e_node_add_test_subdir: e2e-node-add e2e_relay_test_subdir: e2e-relay e2e_coverage_report: "E2E Tests Coverage Report" e2e_mirror_node_coverage_report: "E2E Mirror Node Tests Coverage Report" - e2e_node_pem_stop_add_coverage_report: "E2E Node PEM Stop Add Tests Coverage Report" - e2e_node_pfx_kill_add_coverage_report: "E2E Node PFX Kill Add Tests Coverage Report" + e2e_node_pem_stop_coverage_report: "E2E Node PEM Stop Tests Coverage Report" + e2e_node_pfx_kill_coverage_report: "E2E Node PFX Kill Tests Coverage Report" e2e_node_local_build_coverage_report: "E2E Node Local Build Tests Coverage Report" + e2e_node_add_coverage_report: "E2E Node Add Tests Coverage Report" e2e_relay_coverage_report: "E2E Relay Tests Coverage Report" steps: - run: echo "Exposing environment variables to reusable workflows" diff --git a/package.json b/package.json index 492babb62..25ea10c45 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@ "test-e2e-all": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E All Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e' --testPathIgnorePatterns=\".*/unit/.*\"", "test-e2e": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e' --testPathIgnorePatterns=\".*/unit/.*\" --testPathIgnorePatterns=\".*/e2e/commands/mirror_node.*\" --testPathIgnorePatterns=\".*/e2e/commands/node.*\" --testPathIgnorePatterns=\".*/e2e/commands/relay.*\"", "test-e2e-mirror-node": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Mirror Node Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-mirror-node.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-mirror-node' --testRegex=\".*\\/e2e\\/commands\\/mirror_node\\.test\\.mjs\"", - "test-e2e-node-pem-stop-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PEM Stop Add Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pem-stop-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pem-stop-add' --testRegex=\".*\\/e2e\\/commands\\/node_pem_stop_add\\.test\\.mjs\"", - "test-e2e-node-pfx-kill-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PFX Kill Add Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pfx-kill-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pfx-kill-add' --testRegex=\".*\\/e2e\\/commands\\/node_pfx_kill_add\\.test\\.mjs\"", + "test-e2e-node-pem-stop": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PEM Stop Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pem-stop.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pem-stop' --testRegex=\".*\\/e2e\\/commands\\/node_pem_stop\\.test\\.mjs\"", + "test-e2e-node-pfx-kill": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PFX Kill Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pfx-kill.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pfx-kill' --testRegex=\".*\\/e2e\\/commands\\/node_pfx_kill\\.test\\.mjs\"", "test-e2e-node-local-build": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Local Custom Build' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-local-build.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-local-build' --testRegex=\".*\\/e2e\\/commands\\/node-local.*\\.test\\.mjs\"", + "test-e2e-node-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Add Custom Build' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-add' --testRegex=\".*\\/e2e\\/commands\\/node-add.*\\.test\\.mjs\"", "test-e2e-relay": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Relay Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-relay.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-relay' --testRegex=\".*\\/e2e\\/commands\\/relay\\.test\\.mjs\"", "merge-clean": "rm -rf .nyc_output && mkdir .nyc_output && rm -rf coverage/lcov-report && rm -rf coverage/solo && rm coverage/*.*", "merge-e2e": "nyc merge ./coverage/e2e/ .nyc_output/coverage.json", diff --git a/resources/support-zip.sh b/resources/support-zip.sh new file mode 100644 index 000000000..82566d126 --- /dev/null +++ b/resources/support-zip.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# This script creates a zip file so that it can be copied out of the pod for research purposes + +readonly HAPI_DIR=/opt/hgcapp/services-hedera/HapiApp2.0 +readonly RESEARCH_ZIP=${HOSTNAME}.zip +readonly ZIP_FULLPATH=${HAPI_DIR}/${RESEARCH_ZIP} +readonly FILE_LIST=${HAPI_DIR}/support-zip-file-list.txt +readonly CONFIG_TXT=config.txt +readonly SETTINGS_TXT=settings.txt +readonly SETTINGS_USED_TXT=settingsUsed.txt +readonly OUTPUT_DIR=output +readonly DATA_DIR=data +readonly ADDRESS_BOOK_DIR=${DATA_DIR}/saved/address_book +readonly CONFIG_DIR=${DATA_DIR}/config +readonly KEYS_DIR=${DATA_DIR}/keys +readonly UPGRADE_DIR=${DATA_DIR}/upgrade + +AddToFileList() +{ + if [[ -d "${1}" ]];then + find "${1}" -name "*" -printf '\047%p\047\n' >>${FILE_LIST} + return + fi + + if [[ -f "${1}" ]];then + find . -maxdepth 1 -type f -name "${1}" -print >>${FILE_LIST} + else + echo "skipping: ${1}, file or directory not found" + fi +} + +cd ${HAPI_DIR} +echo -n > ${FILE_LIST} + +AddToFileList ${CONFIG_TXT} +AddToFileList ${SETTINGS_TXT} +AddToFileList ${SETTINGS_USED_TXT} +AddToFileList ${OUTPUT_DIR} +AddToFileList ${ADDRESS_BOOK_DIR} +AddToFileList ${CONFIG_DIR} +AddToFileList ${KEYS_DIR} +AddToFileList ${UPGRADE_DIR} +jar cvfM ${ZIP_FULLPATH} @${FILE_LIST} diff --git a/src/commands/node.mjs b/src/commands/node.mjs index e9b5f9dbf..c77bdeb55 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -1541,6 +1541,7 @@ export class NodeCommand extends BaseCommand { * @property {string} freezeAdminPrivateKey * @property {string} keysDir * @property {string} lastStateZipPath + * @property {Object} nodeClient * @property {string[]} nodeIds * @property {Object} podNames * @property {string} releasePrefix @@ -1567,6 +1568,7 @@ export class NodeCommand extends BaseCommand { 'freezeAdminPrivateKey', 'keysDir', 'lastStateZipPath', + 'nodeClient', 'nodeIds', 'podNames', 'releasePrefix', @@ -1592,7 +1594,7 @@ export class NodeCommand extends BaseCommand { constants.FULLSTACK_TESTING_CHART, constants.FULLSTACK_DEPLOYMENT_CHART) // initialize Node Client with existing network nodes prior to adding the new node which isn't functioning, yet - await this.accountManager.loadNodeClient(ctx.config.namespace) + ctx.config.nodeClient = await this.accountManager.loadNodeClient(ctx.config.namespace) const accountKeys = await this.accountManager.getAccountKeysFromSecret(FREEZE_ADMIN_ACCOUNT, config.namespace) config.freezeAdminPrivateKey = accountKeys.privateKey @@ -1749,9 +1751,8 @@ export class NodeCommand extends BaseCommand { title: 'Prepare upgrade zip file for node upgrade process', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - ctx.nodeClient = await this.accountManager.loadNodeClient(config.namespace) ctx.upgradeZipFile = await this.prepareUpgradeZip(config.stagingDir) - ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.upgradeZipFile, ctx.nodeClient) + ctx.upgradeZipHash = await this.uploadUpgradeZip(ctx.upgradeZipFile, config.nodeClient) } }, { @@ -1773,7 +1774,8 @@ export class NodeCommand extends BaseCommand { try { const nodeClient = this.accountManager._nodeClient - nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.adminKey.toString()) + const treasuryAccountPrivateKey = this.accountManager.getTreasuryAccountKeys(config.namespace).privateKey + nodeClient.setOperator(TREASURY_ACCOUNT_ID, treasuryAccountPrivateKey) const nodeCreateTx = await new NodeCreateTransaction() .setAccountId(ctx.newNode.accountId) @@ -1796,7 +1798,7 @@ export class NodeCommand extends BaseCommand { title: 'Send prepare upgrade transaction', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.prepareUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, ctx.nodeClient) + await this.prepareUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, config.nodeClient) } }, { @@ -1819,7 +1821,7 @@ export class NodeCommand extends BaseCommand { title: 'Send freeze upgrade transaction', task: async (ctx, task) => { const config = /** @type {NodeAddConfigClass} **/ ctx.config - await this.freezeUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, ctx.nodeClient) + await this.freezeUpgradeNetworkNodes(config.freezeAdminPrivateKey, ctx.upgradeZipHash, config.nodeClient) } }, { diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 4e620c9b2..2732e1af8 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -197,41 +197,30 @@ export function validatePath (input) { * @returns {Promise} A promise that resolves when the logs are downloaded */ export async function getNodeLogs (k8, namespace) { + k8.logger.debug('getNodeLogs: begin...') const pods = await k8.getPodsByLabel(['fullstack.hedera.com/type=network-node']) const timeString = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-') for (const pod of pods) { const podName = pod.metadata.name - const targetDir = `${SOLO_LOGS_DIR}/${namespace}/${podName}` + const targetDir = `${SOLO_LOGS_DIR}/${namespace}/${timeString}` try { if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }) } - await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/output/swirlds.log`, targetDir) - await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/output/hgcaa.log`, targetDir) - await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/config.txt`, targetDir) - await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settings.txt`, targetDir) - await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/settingsUsed.txt`, targetDir) - - // get the saved address books - const addressBookPath = `${HEDERA_HAPI_PATH}/data/saved/address_book/` - const output = await k8.execContainer(podName, ROOT_CONTAINER, - ['bash', '-c', `for file in ${addressBookPath}* ; do echo ; echo File: $file ; echo ; cat "$file" ; done`]) - fs.writeFileSync(`${targetDir}/address_book.txt`, output) - - // TODO: open issue, this will cause it to prefix the prefix if old files are already in the directory running locally - // rename all files with timeString as prefix to avoid overwrite - fs.readdirSync(targetDir).forEach(file => { - const oldPath = path.join(targetDir, file) - const newPath = path.join(targetDir, `${timeString}-${file}`) - fs.renameSync(oldPath, newPath) - }) + const scriptName = 'support-zip.sh' + const sourcePath = path.join(constants.RESOURCES_DIR, scriptName) // script source path + await k8.copyTo(podName, ROOT_CONTAINER, sourcePath, `${HEDERA_HAPI_PATH}`) + await k8.execContainer(podName, ROOT_CONTAINER, `chmod 0755 ${HEDERA_HAPI_PATH}/${scriptName}`) + await k8.execContainer(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/${scriptName}`) + await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/${podName}.zip`, targetDir) } catch (e) { // not throw error here, so we can continue to finish downloading logs from other pods // and also delete namespace in the end k8.logger.error(`failed to download logs from pod ${podName}`, e) } + k8.logger.debug('getNodeLogs: ...end') } } diff --git a/test/e2e/commands/account.test.mjs b/test/e2e/commands/account.test.mjs index 972a6491e..3845e0f70 100644 --- a/test/e2e/commands/account.test.mjs +++ b/test/e2e/commands/account.test.mjs @@ -56,11 +56,12 @@ describe('AccountCommand', () => { // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined const bootstrapResp = bootstrapNetwork(testName, argv) + const accountCmd = new AccountCommand(bootstrapResp.opts, testSystemAccounts) + bootstrapResp.cmd.accountCmd = accountCmd const k8 = bootstrapResp.opts.k8 const accountManager = bootstrapResp.opts.accountManager const configManager = bootstrapResp.opts.configManager const nodeCmd = bootstrapResp.cmd.nodeCmd - const accountCmd = new AccountCommand(bootstrapResp.opts, testSystemAccounts) afterAll(async () => { await getNodeLogs(k8, namespace) diff --git a/test/e2e/commands/cluster.test.mjs b/test/e2e/commands/cluster.test.mjs index 41b4d0939..0ca46ad54 100644 --- a/test/e2e/commands/cluster.test.mjs +++ b/test/e2e/commands/cluster.test.mjs @@ -66,6 +66,7 @@ describe('ClusterCommand', () => { const clusterCmd = bootstrapResp.cmd.clusterCmd afterAll(async () => { + await chartManager.isChartInstalled(constants.FULLSTACK_SETUP_NAMESPACE, constants.FULLSTACK_CLUSTER_SETUP_CHART) await getNodeLogs(k8, namespace) await k8.deleteNamespace(namespace) await accountManager.close() diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index c5facd1e0..ff3183ba1 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -13,45 +13,61 @@ * See the License for the specific language governing permissions and * limitations under the License. * + * @jest-environment steps */ -import { afterAll, describe, expect, it } from '@jest/globals' +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' import { constants } from '../../../src/core/index.mjs' import { + accountCreationShouldSucceed, + balanceQueryShouldSucceed, bootstrapNetwork, - getDefaultArgv, + getDefaultArgv, getNodeIdsPrivateKeysHash, getTestConfigManager, getTmpDir, HEDERA_PLATFORM_VERSION_TAG } from '../../test_util.js' import { getNodeLogs } from '../../../src/core/helpers.mjs' import { NodeCommand } from '../../../src/commands/node.mjs' describe('Node add', () => { - const TEST_NAMESPACE = 'node-add' + const defaultTimeout = 120000 + const namespace = 'node-add' + const nodeId = 'node4' const argv = getDefaultArgv() argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM argv[flags.nodeIDs.name] = 'node1,node2,node3' + argv[flags.nodeID.name] = nodeId argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true + argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG - - argv[flags.namespace.name] = TEST_NAMESPACE - const bootstrapResp = bootstrapNetwork(TEST_NAMESPACE, argv) + argv[flags.namespace.name] = namespace + const bootstrapResp = bootstrapNetwork(namespace, argv) const nodeCmd = bootstrapResp.cmd.nodeCmd + const accountCmd = bootstrapResp.cmd.accountCmd const k8 = bootstrapResp.opts.k8 + let existingServiceMap + let existingNodeIdsPrivateKeysHash + + beforeAll(async () => { + const configManager = getTestConfigManager(`${namespace}-solo.config`) + configManager.update(argv, true) + existingServiceMap = await nodeCmd.accountManager.getNodeServiceMap(namespace) + existingNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, constants.KEY_FORMAT_PEM, k8, getTmpDir()) + }, defaultTimeout) afterAll(async () => { - await getNodeLogs(k8, TEST_NAMESPACE) - await k8.deleteNamespace(TEST_NAMESPACE) - }, 120000) + await getNodeLogs(k8, namespace) + await k8.deleteNamespace(namespace) + }, defaultTimeout) - it('should add a new node to the network successfully', async () => { - argv[flags.nodeID.name] = 'node4' - argv[flags.generateGossipKeys.name] = true - argv[flags.generateTlsKeys.name] = true - argv[flags.keyFormat.name] = constants.KEY_FORMAT_PEM + it('should succeed with init command', async () => { + const status = await accountCmd.init(argv) + expect(status).toBeTruthy() + }, 450000) + it('should add a new node to the network successfully', async () => { await nodeCmd.add(argv) expect(nodeCmd.getUnusedConfigs(NodeCommand.ADD_CONFIGS_NAME)).toEqual([ flags.apiPermissionProperties.constName, @@ -63,4 +79,21 @@ describe('Node add', () => { flags.settingTxt.constName ]) }, 600000) + + balanceQueryShouldSucceed(nodeCmd.accountManager, nodeCmd, namespace) + + accountCreationShouldSucceed(nodeCmd.accountManager, nodeCmd, namespace) + + it('existing nodes private keys should not have changed', async () => { + const currentNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, keyFormat, k8, getTmpDir()) + + for (const [nodeId, existingKeyHashMap] of existingNodeIdsPrivateKeysHash.entries()) { + const currentNodeKeyHashMap = currentNodeIdsPrivateKeysHash.get(nodeId) + + for (const [keyFileName, existingKeyHash] of existingKeyHashMap.entries()) { + expect(`${nodeId}:${keyFileName}:${currentNodeKeyHashMap.get(keyFileName)}`).toEqual( + `${nodeId}:${keyFileName}:${existingKeyHash}`) + } + } + }, defaultTimeout) }) diff --git a/test/e2e/commands/node-local-ptt.test.mjs b/test/e2e/commands/node-local-ptt.test.mjs index 2ef952b0d..ec47b9795 100644 --- a/test/e2e/commands/node-local-ptt.test.mjs +++ b/test/e2e/commands/node-local-ptt.test.mjs @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * + * @jest-environment steps */ import { afterAll, diff --git a/test/e2e/commands/node_pem_stop_add.test.mjs b/test/e2e/commands/node_pem_stop.test.mjs similarity index 84% rename from test/e2e/commands/node_pem_stop_add.test.mjs rename to test/e2e/commands/node_pem_stop.test.mjs index d65d65d6a..055317bed 100644 --- a/test/e2e/commands/node_pem_stop_add.test.mjs +++ b/test/e2e/commands/node_pem_stop.test.mjs @@ -18,8 +18,8 @@ import { describe } from '@jest/globals' import { constants } from '../../../src/core/index.mjs' -import { e2eNodeKeyRefreshAddTest } from '../e2e_node_util.js' +import { e2eNodeKeyRefreshTest } from '../e2e_node_util.js' describe('NodeCommand', () => { - e2eNodeKeyRefreshAddTest(constants.KEY_FORMAT_PEM, 'node-cmd-e2e-pem', 'stop') + e2eNodeKeyRefreshTest(constants.KEY_FORMAT_PEM, 'node-cmd-e2e-pem', 'stop') }) diff --git a/test/e2e/commands/node_pfx_kill_add.test.mjs b/test/e2e/commands/node_pfx_kill.test.mjs similarity index 84% rename from test/e2e/commands/node_pfx_kill_add.test.mjs rename to test/e2e/commands/node_pfx_kill.test.mjs index 912b63521..f16f4b94a 100644 --- a/test/e2e/commands/node_pfx_kill_add.test.mjs +++ b/test/e2e/commands/node_pfx_kill.test.mjs @@ -18,8 +18,8 @@ import { describe } from '@jest/globals' import { constants } from '../../../src/core/index.mjs' -import { e2eNodeKeyRefreshAddTest } from '../e2e_node_util.js' +import { e2eNodeKeyRefreshTest } from '../e2e_node_util.js' describe('NodeCommand', () => { - e2eNodeKeyRefreshAddTest(constants.KEY_FORMAT_PFX, 'node-cmd-e2e-pfx', 'kill') + e2eNodeKeyRefreshTest(constants.KEY_FORMAT_PFX, 'node-cmd-e2e-pfx', 'kill') }) diff --git a/test/e2e/e2e_node_util.js b/test/e2e/e2e_node_util.js index ee39884dd..12f2b09d7 100644 --- a/test/e2e/e2e_node_util.js +++ b/test/e2e/e2e_node_util.js @@ -16,12 +16,6 @@ * @jest-environment steps */ -import { - AccountCreateTransaction, - Hbar, - HbarUnit, - PrivateKey -} from '@hashgraph/sdk' import { afterAll, afterEach, @@ -32,298 +26,172 @@ import { } from '@jest/globals' import { flags } from '../../src/commands/index.mjs' import { - constants, Templates -} from '../../src/core/index.mjs' -import { + accountCreationShouldSucceed, balanceQueryShouldSucceed, bootstrapNetwork, getDefaultArgv, getTestConfigManager, - getTmpDir, HEDERA_PLATFORM_VERSION_TAG, TEST_CLUSTER } from '../test_util.js' import { getNodeLogs, sleep } from '../../src/core/helpers.mjs' -import path from 'path' -import fs from 'fs' -import crypto from 'crypto' -import { ROOT_CONTAINER, SHORTER_SYSTEM_ACCOUNTS } from '../../src/core/constants.mjs' import { NodeCommand } from '../../src/commands/node.mjs' -import { AccountCommand } from '../../src/commands/account.mjs' -export function e2eNodeKeyRefreshAddTest (keyFormat, testName, mode, releaseTag = HEDERA_PLATFORM_VERSION_TAG) { +export function e2eNodeKeyRefreshTest (keyFormat, testName, mode, releaseTag = HEDERA_PLATFORM_VERSION_TAG) { const defaultTimeout = 120000 - describe(`NodeCommand [testName ${testName}, mode ${mode}, keyFormat: ${keyFormat}, release ${releaseTag}]`, () => { - const namespace = testName - const argv = getDefaultArgv() - argv[flags.namespace.name] = namespace - argv[flags.releaseTag.name] = releaseTag - argv[flags.keyFormat.name] = keyFormat - argv[flags.nodeIDs.name] = 'node0,node1,node2,node3' - argv[flags.generateGossipKeys.name] = true - argv[flags.generateTlsKeys.name] = true - argv[flags.clusterName.name] = TEST_CLUSTER - argv[flags.devMode.name] = true - // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts - argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - - const bootstrapResp = bootstrapNetwork(testName, argv) - const accountManager = bootstrapResp.opts.accountManager - const k8 = bootstrapResp.opts.k8 - const nodeCmd = bootstrapResp.cmd.nodeCmd - const accountCmd = new AccountCommand(bootstrapResp.opts, SHORTER_SYSTEM_ACCOUNTS) - - afterEach(async () => { - await nodeCmd.close() - await accountManager.close() - }, defaultTimeout) - - afterAll(async () => { - await getNodeLogs(k8, namespace) - await k8.deleteNamespace(namespace) - }, 180000) - - describe(`Node should have started successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, () => { - balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) - - accountCreationShouldSucceed(accountManager, nodeCmd, namespace) - - it(`Node Proxy should be UP [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}`, async () => { - expect.assertions(1) - - try { - await expect(k8.waitForPodReady( - ['app=haproxy-node0', 'fullstack.hedera.com/type=haproxy'], - 1, 300, 1000)).resolves.toBeTruthy() - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } finally { + describe( + `NodeCommand [testName ${testName}, mode ${mode}, keyFormat: ${keyFormat}, release ${releaseTag}]`, + () => { + const namespace = testName + const argv = getDefaultArgv() + argv[flags.namespace.name] = namespace + argv[flags.releaseTag.name] = releaseTag + argv[flags.keyFormat.name] = keyFormat + argv[flags.nodeIDs.name] = 'node0,node1,node2,node3' + argv[flags.generateGossipKeys.name] = true + argv[flags.generateTlsKeys.name] = true + argv[flags.clusterName.name] = TEST_CLUSTER + argv[flags.devMode.name] = true + // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts + argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR + ? process.env.SOLO_FST_CHARTS_DIR + : undefined + + const bootstrapResp = bootstrapNetwork(testName, argv) + const accountManager = bootstrapResp.opts.accountManager + const k8 = bootstrapResp.opts.k8 + const nodeCmd = bootstrapResp.cmd.nodeCmd + + afterEach(async () => { await nodeCmd.close() + await accountManager.close() + }, defaultTimeout) + + afterAll(async () => { + await getNodeLogs(k8, namespace) + await k8.deleteNamespace(namespace) + }, 180000) + + describe( + `Node should have started successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, + () => { + balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) + + accountCreationShouldSucceed(accountManager, nodeCmd, namespace) + + it(`Node Proxy should be UP [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}`, + async () => { + expect.assertions(1) + + try { + await expect(k8.waitForPodReady( + ['app=haproxy-node0', + 'fullstack.hedera.com/type=haproxy'], + 1, 300, 1000)).resolves.toBeTruthy() + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } finally { + await nodeCmd.close() + } + }, defaultTimeout) + }) + + describe( + `Node should refresh successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, + () => { + const nodeId = 'node0' + + beforeAll(async () => { + const podName = await nodeRefreshTestSetup(argv, testName, k8, + nodeId) + if (mode === 'kill') { + const resp = await k8.kubeClient.deleteNamespacedPod(podName, + namespace) + expect(resp.response.statusCode).toEqual(200) + await sleep(20000) // sleep to wait for pod to finish terminating + } else if (mode === 'stop') { + await expect(nodeCmd.stop(argv)).resolves.toBeTruthy() + await sleep(20000) // give time for node to stop and update its logs + } else { + throw new Error(`invalid mode: ${mode}`) + } + }, 120000) + + nodePodShouldBeRunning(nodeCmd, namespace, nodeId) + + nodeShouldNotBeActive(nodeCmd, nodeId) + + nodeRefreshShouldSucceed(nodeId, nodeCmd, argv) + + balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) + + accountCreationShouldSucceed(accountManager, nodeCmd, namespace) + }) + + function nodePodShouldBeRunning (nodeCmd, namespace, nodeId) { + it(`${nodeId} should be running`, async () => { + try { + await expect(nodeCmd.checkNetworkNodePod(namespace, + nodeId)).resolves.toBeTruthy() + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } finally { + await nodeCmd.close() + } + }, defaultTimeout) } - }, defaultTimeout) - }) - describe(`Node should refresh successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, () => { - const nodeId = 'node0' - - beforeAll(async () => { - const podName = await nodeRefreshTestSetup(argv, testName, k8, nodeId) - if (mode === 'kill') { - const resp = await k8.kubeClient.deleteNamespacedPod(podName, namespace) - expect(resp.response.statusCode).toEqual(200) - await sleep(20000) // sleep to wait for pod to finish terminating - } else if (mode === 'stop') { - await expect(nodeCmd.stop(argv)).resolves.toBeTruthy() - await sleep(20000) // give time for node to stop and update its logs - } else { - throw new Error(`invalid mode: ${mode}`) + function nodeRefreshShouldSucceed (nodeId, nodeCmd, argv) { + it(`${nodeId} refresh should succeed`, async () => { + try { + await expect(nodeCmd.refresh(argv)).resolves.toBeTruthy() + expect(nodeCmd.getUnusedConfigs( + NodeCommand.REFRESH_CONFIGS_NAME)).toEqual( + [flags.devMode.constName]) + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } finally { + await nodeCmd.close() + await sleep(10000) // sleep to wait for node to finish starting + } + }, 1200000) } - }, 120000) - - nodePodShouldBeRunning(nodeCmd, namespace, nodeId) - - nodeShouldNotBeActive(nodeCmd, nodeId) - - nodeRefreshShouldSucceed(nodeId, nodeCmd, argv) - - balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) - - accountCreationShouldSucceed(accountManager, nodeCmd, namespace) - }) - - describe(`Should add a new node to the network [release ${releaseTag}, keyFormat: ${keyFormat}]`, () => { - const nodeId = 'node4' - let existingServiceMap - let existingNodeIdsPrivateKeysHash - - beforeAll(async () => { - argv[flags.nodeIDs.name] = nodeId - const configManager = getTestConfigManager(`${testName}-solo.config`) - configManager.update(argv, true) - existingServiceMap = await accountManager.getNodeServiceMap(namespace) - existingNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, keyFormat, k8, getTmpDir()) - }, defaultTimeout) - - it(`${nodeId} should not exist`, async () => { - try { - await expect(nodeCmd.checkNetworkNodePod(namespace, nodeId, 10, 50)).rejects.toThrowError(`no pod found for nodeId: ${nodeId}`) - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } finally { - await nodeCmd.close() - } - }, 180000) - - balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) - accountCreationShouldSucceed(accountManager, nodeCmd, namespace) - - it('should succeed with init command', async () => { - const status = await accountCmd.init(argv) - expect(status).toBeTruthy() - }, 450000) - - it(`add ${nodeId} to the network`, async () => { - try { - await expect(nodeCmd.add(argv)).resolves.toBeTruthy() - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } finally { - await nodeCmd.close() - await sleep(10000) // sleep to wait for node to finish starting + function nodeShouldNotBeActive (nodeCmd, nodeId) { + it(`${nodeId} should not be ACTIVE`, async () => { + expect(2) + try { + await expect( + nodeCmd.checkNetworkNodeState(nodeId, + 5)).rejects.toThrowError() + } catch (e) { + expect(e).not.toBeNull() + } finally { + await nodeCmd.close() + } + }, defaultTimeout) } - }, 600000) - - balanceQueryShouldSucceed(accountManager, nodeCmd, namespace) - - accountCreationShouldSucceed(accountManager, nodeCmd, namespace) - - it('existing nodes private keys should not have changed', async () => { - const currentNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, keyFormat, k8, getTmpDir()) - for (const [nodeId, existingKeyHashMap] of existingNodeIdsPrivateKeysHash.entries()) { - const currentNodeKeyHashMap = currentNodeIdsPrivateKeysHash.get(nodeId) - - for (const [keyFileName, existingKeyHash] of existingKeyHashMap.entries()) { - expect(`${nodeId}:${keyFileName}:${currentNodeKeyHashMap.get(keyFileName)}`).toEqual( - `${nodeId}:${keyFileName}:${existingKeyHash}`) + async function nodeRefreshTestSetup (argv, testName, k8, nodeId) { + argv[flags.nodeIDs.name] = nodeId + const configManager = getTestConfigManager(`${testName}-solo.config`) + configManager.update(argv, true) + + const podArray = await k8.getPodsByLabel( + [`app=network-${nodeId}`, + 'fullstack.hedera.com/type=network-node']) + + if (podArray.length > 0) { + const podName = podArray[0].metadata.name + k8.logger.info(`nodeRefreshTestSetup: podName: ${podName}`) + return podName + } else { + throw new Error(`pod for ${nodeId} not found`) } } - }, defaultTimeout) - }) - }) - - function accountCreationShouldSucceed (accountManager, nodeCmd, namespace) { - it('Account creation should succeed', async () => { - expect.assertions(3) - - try { - await accountManager.loadNodeClient(namespace) - expect(accountManager._nodeClient).not.toBeNull() - 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.toBeNull() - expect(accountInfo.balance).toEqual(amount) - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } - }, defaultTimeout) - } - - function nodePodShouldBeRunning (nodeCmd, namespace, nodeId) { - it(`${nodeId} should be running`, async () => { - try { - await expect(nodeCmd.checkNetworkNodePod(namespace, nodeId)).resolves.toBeTruthy() - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } finally { - await nodeCmd.close() - } - }, defaultTimeout) - } - - function nodeRefreshShouldSucceed (nodeId, nodeCmd, argv) { - it(`${nodeId} refresh should succeed`, async () => { - try { - await expect(nodeCmd.refresh(argv)).resolves.toBeTruthy() - expect(nodeCmd.getUnusedConfigs(NodeCommand.REFRESH_CONFIGS_NAME)).toEqual([flags.devMode.constName]) - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } finally { - await nodeCmd.close() - await sleep(10000) // sleep to wait for node to finish starting - } - }, 1200000) - } - - function nodeShouldNotBeActive (nodeCmd, nodeId) { - it(`${nodeId} should not be ACTIVE`, async () => { - expect(2) - try { - await expect(nodeCmd.checkNetworkNodeState(nodeId, 5)).rejects.toThrowError() - } catch (e) { - expect(e).not.toBeNull() - } finally { - await nodeCmd.close() - } - }, defaultTimeout) - } - - async function nodeRefreshTestSetup (argv, testName, k8, nodeId) { - argv[flags.nodeIDs.name] = nodeId - const configManager = getTestConfigManager(`${testName}-solo.config`) - configManager.update(argv, true) - - const podArray = await k8.getPodsByLabel( - [`app=network-${nodeId}`, 'fullstack.hedera.com/type=network-node']) - - if (podArray.length > 0) { - const podName = podArray[0].metadata.name - k8.logger.info(`nodeRefreshTestSetup: podName: ${podName}`) - return podName - } else { - throw new Error(`pod for ${nodeId} not found`) - } - } - - async function getNodeIdsPrivateKeysHash (networkNodeServicesMap, namespace, keyFormat, k8, destDir) { - const dataKeysDir = `${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 nodeId = networkNodeServices.nodeName - const uniqueNodeDestDir = path.join(destDir, nodeId) - if (!fs.existsSync(uniqueNodeDestDir)) { - fs.mkdirSync(uniqueNodeDestDir, { recursive: true }) - } - switch (keyFormat) { - case constants.KEY_FORMAT_PFX: - await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPfxPrivateKeyFile(nodeId)) - break - case constants.KEY_FORMAT_PEM: - await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPemPrivateKeyFile(constants.SIGNING_KEY_PREFIX, nodeId)) - await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPemPrivateKeyFile(constants.AGREEMENT_KEY_PREFIX, nodeId)) - break - default: - throw new Error(`invalid keyFormat: ${keyFormat}`) - } - await addKeyHashToMap(k8, nodeId, tlsKeysDir, uniqueNodeDestDir, keyHashMap, 'hedera.key') - nodeKeyHashMap.set(nodeId, keyHashMap) - } - return nodeKeyHashMap - } - - async function addKeyHashToMap (k8, nodeId, keyDir, uniqueNodeDestDir, keyHashMap, privateKeyFileName) { - await k8.copyFrom( - Templates.renderNetworkPodName(nodeId), - ROOT_CONTAINER, - path.join(keyDir, privateKeyFileName), - uniqueNodeDestDir) - const keyBytes = await fs.readFileSync(path.join(uniqueNodeDestDir, privateKeyFileName)) - const keyString = keyBytes.toString() - keyHashMap.set(privateKeyFileName, crypto.createHash('sha256').update(keyString).digest('base64')) - } + }) } diff --git a/test/test_util.js b/test/test_util.js index 6472804b5..6e549127f 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. * + * @jest-environment steps */ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' import fs from 'fs' @@ -39,10 +40,17 @@ import { KeyManager, logging, PackageDownloader, - PlatformInstaller, ProfileManager, + PlatformInstaller, ProfileManager, Templates, Zippy } from '../src/core/index.mjs' -import { AccountBalanceQuery } from '@hashgraph/sdk' +import { + AccountBalanceQuery, + AccountCreateTransaction, Hbar, HbarUnit, + PrivateKey +} from '@hashgraph/sdk' +import { ROOT_CONTAINER } from '../src/core/constants.mjs' +import crypto from 'crypto' +import { AccountCommand } from '../src/commands/account.mjs' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' @@ -93,13 +101,15 @@ export function getDefaultArgv () { * @param clusterCmdArg an instance of command/ClusterCommand * @param networkCmdArg an instance of command/NetworkCommand * @param nodeCmdArg an instance of command/NodeCommand + * @param accountCmdArg an instance of command/AccountCommand */ export function bootstrapTestVariables (testName, argv, k8Arg = null, initCmdArg = null, clusterCmdArg = null, networkCmdArg = null, - nodeCmdArg = null + nodeCmdArg = null, + accountCmdArg = null ) { const namespace = argv[flags.namespace.name] || 'bootstrap-ns' const cacheDir = argv[flags.cacheDir.name] || getTestCacheDir(testName) @@ -139,6 +149,7 @@ export function bootstrapTestVariables (testName, argv, 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) return { namespace, opts, @@ -146,7 +157,8 @@ export function bootstrapTestVariables (testName, argv, initCmd, clusterCmd, networkCmd, - nodeCmd + nodeCmd, + accountCmd } } } @@ -161,15 +173,17 @@ export function bootstrapTestVariables (testName, argv, * @param clusterCmdArg an instance of command/ClusterCommand * @param networkCmdArg an instance of command/NetworkCommand * @param nodeCmdArg an instance of command/NodeCommand + * @param accountCmdArg an instance of command/AccountCommand */ export function bootstrapNetwork (testName, argv, k8Arg = null, initCmdArg = null, clusterCmdArg = null, networkCmdArg = null, - nodeCmdArg = null + nodeCmdArg = null, + accountCmdArg = null ) { - const bootstrapResp = bootstrapTestVariables(testName, argv, k8Arg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg) + const bootstrapResp = bootstrapTestVariables(testName, argv, k8Arg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg, accountCmdArg) const namespace = bootstrapResp.namespace const initCmd = bootstrapResp.cmd.initCmd const k8 = bootstrapResp.opts.k8 @@ -274,3 +288,75 @@ export function balanceQueryShouldSucceed (accountManager, cmd, namespace) { await sleep(1000) }, 120000) } + +export function accountCreationShouldSucceed (accountManager, nodeCmd, namespace) { + it('Account creation should succeed', async () => { + expect.assertions(3) + + try { + await accountManager.loadNodeClient(namespace) + expect(accountManager._nodeClient).not.toBeNull() + 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.toBeNull() + expect(accountInfo.balance).toEqual(amount) + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } + }, 120000) +} + +export async function getNodeIdsPrivateKeysHash (networkNodeServicesMap, namespace, keyFormat, k8, destDir) { + const dataKeysDir = `${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 nodeId = networkNodeServices.nodeName + const uniqueNodeDestDir = path.join(destDir, nodeId) + if (!fs.existsSync(uniqueNodeDestDir)) { + fs.mkdirSync(uniqueNodeDestDir, { recursive: true }) + } + switch (keyFormat) { + case constants.KEY_FORMAT_PFX: + await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPfxPrivateKeyFile(nodeId)) + break + case constants.KEY_FORMAT_PEM: + await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPemPrivateKeyFile(constants.SIGNING_KEY_PREFIX, nodeId)) + await addKeyHashToMap(k8, nodeId, dataKeysDir, uniqueNodeDestDir, keyHashMap, Templates.renderGossipPemPrivateKeyFile(constants.AGREEMENT_KEY_PREFIX, nodeId)) + break + default: + throw new Error(`invalid keyFormat: ${keyFormat}`) + } + await addKeyHashToMap(k8, nodeId, tlsKeysDir, uniqueNodeDestDir, keyHashMap, 'hedera.key') + nodeKeyHashMap.set(nodeId, keyHashMap) + } + return nodeKeyHashMap +} + +async function addKeyHashToMap (k8, nodeId, keyDir, uniqueNodeDestDir, keyHashMap, privateKeyFileName) { + await k8.copyFrom( + Templates.renderNetworkPodName(nodeId), + ROOT_CONTAINER, + path.join(keyDir, privateKeyFileName), + uniqueNodeDestDir) + const keyBytes = await fs.readFileSync(path.join(uniqueNodeDestDir, privateKeyFileName)) + const keyString = keyBytes.toString() + keyHashMap.set(privateKeyFileName, crypto.createHash('sha256').update(keyString).digest('base64')) +} From 9a3cfc8d9f52a1ea682b3b0034b45ac0d534f1b4 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 21:25:07 +0100 Subject: [PATCH 41/56] fixed lint errors Signed-off-by: Jeromy Cannon --- README.md | 18 ++++++++++++++---- docs/content/_index.md | 11 +++++++---- docs/content/contribution/contribution.md | 5 +++-- docs/content/contribution/docs.md | 5 +++-- docs/content/getting-started/deploy.md | 5 +++-- docs/content/getting-started/installation.md | 7 ++++--- docs/content/getting-started/setup.md | 5 +++-- 7 files changed, 37 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9286a8174..f78051a21 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Then run the following command to set the kubectl context to the new cluster: ```bash kind create cluster -n "${SOLO_CLUSTER_NAME}" ``` + Example output ``` @@ -184,6 +185,7 @@ Kubernetes Namespace : solo ✔ Generate gRPC TLS keys ✔ Finalize ``` + Key files are generated in `~/.solo/keys` directory. ``` @@ -192,6 +194,7 @@ $ ls ~/.solo/cache/keys hedera-node0.crt hedera-node1.crt hedera-node2.crt private-node0.pfx private-node2.pfx hedera-node0.key hedera-node1.key hedera-node2.key private-node1.pfx public.pfx ``` + * Setup cluster with shared components * In a separate terminal, you may run `k9s` to view the pod status. @@ -214,7 +217,6 @@ Kubernetes Namespace : solo ✔ Install 'fullstack-cluster-setup' chart ``` - * Deploy helm chart with Hedera network components * It may take a while (5~15 minutes depending on your internet speed) to download various docker images and get the pods started. * If it fails, ensure you have enough resources allocated for Docker engine and retry the command. @@ -334,6 +336,7 @@ Kubernetes Namespace : solo ✔ Check proxy for node: node0 ✔ Check node proxies are ACTIVE ``` + * Deploy mirror node ``` @@ -518,7 +521,9 @@ Kubernetes Namespace : solo ✔ Generate gRPC TLS keys ✔ Finalize ``` + PEM key files are generated in `~/.solo/keys` directory. + ``` $ ls ~/.solo/cache/keys a-private-node0.pem a-public-node1.pem hedera-node1.crt s-private-node0.pem s-public-node1.pem @@ -526,6 +531,7 @@ a-private-node1.pem a-public-node2.pem hedera-node1.key s-private-node1.pem a-private-node2.pem hedera-node0.crt hedera-node2.crt s-private-node2.pem a-public-node0.pem hedera-node0.key hedera-node2.key s-public-node0.pem ``` + * Setup cluster with shared components ``` @@ -561,16 +567,18 @@ $ solo node start # output is similar to example-1 ``` + ## For Developers Working on Hedera Service Repo First, pleaes clone hedera service repo `https://github.com/hashgraph/hedera-services/` and build the code with `./gradlew assemble`. If need to running nodes with different versions or releases, please duplicate the repo or build directories in -multiple directories, checkout to the respective version and build the code. +multiple directories, checkout to the respective version and build the code. To set customized `settings.txt` file, edit the file `~/.solo/cache/templates/settings.txt` after `solo init` command. Then you can start customized built hedera network with the following command: + ``` solo node setup --local-build-path ,node1=,node2= ``` @@ -578,16 +586,18 @@ solo node setup --local-build-path ,node1=,node1=,node2= --app PlatformTestingTool.jar --app-config ``` + ## Logs + You can find log for running solo command under the directory `~/.solo/logs/` -The file `solo.log` contains the logs for the solo command. +The file `solo.log` contains the logs for the solo command. The file `hashgraph-sdk.log` contains the logs from solo client when sending transactions to network nodes. - ## Support If you have a question on how to use the product, please see our [support guide](https://github.com/hashgraph/.github/blob/main/SUPPORT.md). diff --git a/docs/content/_index.md b/docs/content/_index.md index c63a844f7..01aa5c75a 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -1,13 +1,16 @@ ---- +*** + title: Welcome to Solo Documentation geekdocNav: true geekdocAlign: center geekdocAnchor: false geekdocDescription: Home page for Solo Documentation ---- +---------------------------------------------------- + + [![NPM Version](https://img.shields.io/npm/v/%40hashgraph%2Fsolo?logo=npm)](https://www.npmjs.com/package/@hashgraph/solo) @@ -21,8 +24,7 @@ Solo is an opinionated CLI tool to deploy and manage standalone test networks. {{< button size="large" relref="getting-started/installation.md" >}}Getting Started{{< /button >}} -Feature overview ------------------------------ +## Feature overview {{< columns >}} @@ -33,6 +35,7 @@ Stay focused on deployment and don't get overwhelmed by a complex design. {{< /columns >}} {{< columns >}} + ### Easy configuration Getting started in minutes. Solo comes with easy to use configuration. diff --git a/docs/content/contribution/contribution.md b/docs/content/contribution/contribution.md index d7b9166bf..df86715ed 100644 --- a/docs/content/contribution/contribution.md +++ b/docs/content/contribution/contribution.md @@ -1,6 +1,7 @@ ---- +*** + title: Solo Contribution weight: -20 geekdocNav: true geekdocAlign: center ---- +-------------------- diff --git a/docs/content/contribution/docs.md b/docs/content/contribution/docs.md index 5cc496695..1f02651bb 100644 --- a/docs/content/contribution/docs.md +++ b/docs/content/contribution/docs.md @@ -1,6 +1,7 @@ ---- +*** + title: Docs Contribution weight: -20 geekdocNav: true geekdocAlign: center ---- +-------------------- diff --git a/docs/content/getting-started/deploy.md b/docs/content/getting-started/deploy.md index 7185be62f..e0c528189 100644 --- a/docs/content/getting-started/deploy.md +++ b/docs/content/getting-started/deploy.md @@ -1,10 +1,11 @@ ---- +*** + title: Deploy weight: -20 geekdocNav: true geekdocAlign: center geekdocAnchor: false ---- +-------------------- ### Example - 1: Deploy a standalone test network (version `0.42.5`) diff --git a/docs/content/getting-started/installation.md b/docs/content/getting-started/installation.md index 957a68cf1..d9c08b923 100644 --- a/docs/content/getting-started/installation.md +++ b/docs/content/getting-started/installation.md @@ -1,14 +1,15 @@ ---- +*** + title: Installation weight: -20 geekdocNav: true geekdocAlign: center geekdocAnchor: false ---- +-------------------- ### Requirements -Node(>=20.14.0) (_lts/hydrogen_) +Node(>=20.14.0) (*lts/hydrogen*) ### Setup diff --git a/docs/content/getting-started/setup.md b/docs/content/getting-started/setup.md index f6101b042..7e6bb1edc 100644 --- a/docs/content/getting-started/setup.md +++ b/docs/content/getting-started/setup.md @@ -1,10 +1,11 @@ ---- +*** + title: Setup weight: -20 geekdocNav: true geekdocAlign: center geekdocAnchor: false ---- +-------------------- ### Remote cluster From 31b67f1cd0af69eeeaef74f33867346d53074879 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 21:25:54 +0100 Subject: [PATCH 42/56] fixed lint errors Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 1 - test/e2e/commands/node-add.test.mjs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index c77bdeb55..315297a1b 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -35,7 +35,6 @@ import { FileAppendTransaction, FreezeTransaction, FreezeType, - HbarUnit, ServiceEndpoint, Timestamp, PrivateKey, diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index ff3183ba1..25cd62daa 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -85,7 +85,7 @@ describe('Node add', () => { accountCreationShouldSucceed(nodeCmd.accountManager, nodeCmd, namespace) it('existing nodes private keys should not have changed', async () => { - const currentNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, keyFormat, k8, getTmpDir()) + const currentNodeIdsPrivateKeysHash = await getNodeIdsPrivateKeysHash(existingServiceMap, namespace, constants.KEY_FORMAT_PEM, k8, getTmpDir()) for (const [nodeId, existingKeyHashMap] of existingNodeIdsPrivateKeysHash.entries()) { const currentNodeKeyHashMap = currentNodeIdsPrivateKeysHash.get(nodeId) From cdf3212f1093f357f7a9ca5281752adada40a5cb Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 12 Aug 2024 21:32:57 +0100 Subject: [PATCH 43/56] fixed lint errors Signed-off-by: Jeromy Cannon --- docs/content/_index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/content/_index.md b/docs/content/_index.md index 01aa5c75a..13e1d34c5 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -5,7 +5,8 @@ geekdocNav: true geekdocAlign: center geekdocAnchor: false geekdocDescription: Home page for Solo Documentation ----------------------------------------------------- + +*** From 22bcc5b492de742f7bf92e1c242d5d53eaf9c868 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 07:44:22 +0100 Subject: [PATCH 44/56] fixed test case problem in node-add Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 26 ++++++++++++++++---------- test/e2e/commands/node-add.test.mjs | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 315297a1b..4d873e07c 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -175,6 +175,9 @@ export class NodeCommand extends BaseCommand { try { await this.accountManager.loadNodeClient(namespace) const client = this.accountManager._nodeClient + const treasuryKey = await this.accountManager.getTreasuryAccountKeys(namespace) + const treasuryPrivateKey = PrivateKey.fromStringED25519(treasuryKey.privateKey) + client.setOperator(TREASURY_ACCOUNT_ID, treasuryPrivateKey) // get some initial balance await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, HEDERA_NODE_DEFAULT_STAKE_AMOUNT + 1) @@ -191,9 +194,6 @@ export class NodeCommand extends BaseCommand { .setStakedNodeId(Templates.nodeNumberFromNodeId(nodeId) - 1) .freezeWith(client) - const treasuryKey = await this.accountManager.getTreasuryAccountKeys(namespace) - const treasuryPrivateKey = PrivateKey.fromStringED25519(treasuryKey.privateKey) - // Sign the transaction with the account's private key const signTx = await transaction.sign(treasuryPrivateKey) @@ -1545,6 +1545,7 @@ export class NodeCommand extends BaseCommand { * @property {Object} podNames * @property {string} releasePrefix * @property {Map} serviceMap + * @property {PrivateKey} treasuryKey * @property {string} stagingDir * @property {string} stagingKeysDir * -- methods -- @@ -1573,7 +1574,8 @@ export class NodeCommand extends BaseCommand { 'releasePrefix', 'serviceMap', 'stagingDir', - 'stagingKeysDir' + 'stagingKeysDir', + 'treasuryKey' ]) config.curDate = new Date() @@ -1598,6 +1600,10 @@ export class NodeCommand extends BaseCommand { const accountKeys = await this.accountManager.getAccountKeysFromSecret(FREEZE_ADMIN_ACCOUNT, config.namespace) config.freezeAdminPrivateKey = accountKeys.privateKey + const treasuryAccount = await this.accountManager.getTreasuryAccountKeys(config.namespace) + const treasuryAccountPrivateKey = treasuryAccount.privateKey + config.treasuryKey = PrivateKey.fromStringED25519(treasuryAccountPrivateKey) + self.logger.debug('Initialized config', { config }) } }, @@ -1772,9 +1778,7 @@ export class NodeCommand extends BaseCommand { const config = /** @type {NodeAddConfigClass} **/ ctx.config try { - const nodeClient = this.accountManager._nodeClient - const treasuryAccountPrivateKey = this.accountManager.getTreasuryAccountKeys(config.namespace).privateKey - nodeClient.setOperator(TREASURY_ACCOUNT_ID, treasuryAccountPrivateKey) + config.nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.treasuryKey) const nodeCreateTx = await new NodeCreateTransaction() .setAccountId(ctx.newNode.accountId) @@ -1783,10 +1787,10 @@ export class NodeCommand extends BaseCommand { .setGossipCaCertificate(ctx.signingCertDer) .setCertificateHash(ctx.tlsCertHash) .setAdminKey(config.adminKey.publicKey) - .freezeWith(nodeClient) + .freezeWith(config.nodeClient) const signedTx = await nodeCreateTx.sign(config.adminKey) - const txResp = await signedTx.execute(nodeClient) - const nodeCreateReceipt = await txResp.getReceipt(nodeClient) + const txResp = await signedTx.execute(config.nodeClient) + const nodeCreateReceipt = await txResp.getReceipt(config.nodeClient) this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) } catch (e) { throw new FullstackTestingError(`Error adding node to network: ${e.message}`, e) @@ -2062,6 +2066,7 @@ export class NodeCommand extends BaseCommand { // send some write transactions to invoke the handler that will trigger the stake weight recalculate for (const nodeId of config.allNodeIds) { const accountId = accountMap.get(nodeId) + config.nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.treasuryKey) await this.accountManager.transferAmount(constants.TREASURY_ACCOUNT_ID, accountId, 1) } } @@ -2083,6 +2088,7 @@ export class NodeCommand extends BaseCommand { try { await tasks.run() } catch (e) { + self.logger.error(`Error in setting up nodes: ${e.message}`, e) throw new FullstackTestingError(`Error in setting up nodes: ${e.message}`, e) } finally { await self.close() diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index 25cd62daa..bc0fd5da2 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -78,6 +78,7 @@ describe('Node add', () => { flags.log4j2Xml.constName, flags.settingTxt.constName ]) + await nodeCmd.accountManager.close() }, 600000) balanceQueryShouldSucceed(nodeCmd.accountManager, nodeCmd, namespace) From d106eb9f13d88dcfd4d8cb8ee34b8150d06bbc75 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 16:32:42 +0100 Subject: [PATCH 45/56] making tests more resilient Signed-off-by: Jeromy Cannon --- resources/support-zip.sh | 3 +- src/core/k8.mjs | 8 +- test/e2e/core/account_manager.test.mjs | 32 ++++-- test/e2e/core/chart_manager.test.mjs | 2 +- test/e2e/core/k8_e2e.test.mjs | 143 +++++++++++++++++-------- test/e2e/e2e_node_util.js | 2 +- test/e2e/setup-e2e.sh | 1 - test/test_util.js | 64 +++++------ 8 files changed, 163 insertions(+), 92 deletions(-) diff --git a/resources/support-zip.sh b/resources/support-zip.sh index 82566d126..84d007d18 100644 --- a/resources/support-zip.sh +++ b/resources/support-zip.sh @@ -14,6 +14,7 @@ readonly ADDRESS_BOOK_DIR=${DATA_DIR}/saved/address_book readonly CONFIG_DIR=${DATA_DIR}/config readonly KEYS_DIR=${DATA_DIR}/keys readonly UPGRADE_DIR=${DATA_DIR}/upgrade +readonly JOURNAL_CTL_LOG=${OUTPUT_DIR}/journalctl.log AddToFileList() { @@ -31,7 +32,7 @@ AddToFileList() cd ${HAPI_DIR} echo -n > ${FILE_LIST} - +journalctl > ${JOURNAL_CTL_LOG} AddToFileList ${CONFIG_TXT} AddToFileList ${SETTINGS_TXT} AddToFileList ${SETTINGS_USED_TXT} diff --git a/src/core/k8.mjs b/src/core/k8.mjs index 2b4e27574..2da966a6c 100644 --- a/src/core/k8.mjs +++ b/src/core/k8.mjs @@ -429,7 +429,7 @@ export class K8 { return await this.execContainer( podName, containerName, - ['bash', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] + ['sh', '-c', '[[ -d "' + destPath + '" ]] && echo -n "true" || echo -n "false"'] ) === 'true' } @@ -437,7 +437,7 @@ export class K8 { return this.execContainer( podName, containerName, - ['bash', '-c', 'mkdir -p "' + destPath + '"'] + ['sh', '-c', 'mkdir -p "' + destPath + '"'] ) } @@ -600,11 +600,11 @@ export class K8 { } /** - * Invoke bash command within a container and return the console output as string + * Invoke sh command within a container and return the console output as string * * @param podName pod name * @param containerName container name - * @param command bash commands as an array to be run within the containerName (e.g 'ls -la /opt/hgcapp') + * @param command sh commands as an array to be run within the containerName (e.g 'ls -la /opt/hgcapp') * @param timeoutMs timout in milliseconds * @returns {Promise} console output as string */ diff --git a/test/e2e/core/account_manager.test.mjs b/test/e2e/core/account_manager.test.mjs index 406c21e97..1edaaa62b 100644 --- a/test/e2e/core/account_manager.test.mjs +++ b/test/e2e/core/account_manager.test.mjs @@ -14,19 +14,33 @@ * limitations under the License. * */ -import { describe, expect, it } from '@jest/globals' -import path from 'path' +import { afterAll, describe, expect, it } from '@jest/globals' import { flags } from '../../../src/commands/index.mjs' -import { AccountManager } from '../../../src/core/account_manager.mjs' -import { ConfigManager, constants, K8 } from '../../../src/core/index.mjs' -import { getTestCacheDir, testLogger } from '../../test_util.js' +import { + bootstrapNetwork, + getDefaultArgv, + TEST_CLUSTER +} from '../../test_util.js' +import * as version from '../../../version.mjs' describe('AccountManager', () => { - const configManager = new ConfigManager(testLogger, path.join(getTestCacheDir('accountCmd'), 'solo.config')) - configManager.setFlag(flags.namespace, 'solo-e2e') + const namespace = 'account-mngr-e2e' + const argv = getDefaultArgv() + argv[flags.namespace.name] = namespace + argv[flags.nodeIDs.name] = 'node0' + argv[flags.clusterName.name] = TEST_CLUSTER + argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION + // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts + argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined + const bootstrapResp = bootstrapNetwork(namespace, argv, null, null, null, null, null, null, false) + const k8 = bootstrapResp.opts.k8 + const accountManager = bootstrapResp.opts.accountManager + const configManager = bootstrapResp.opts.configManager - const k8 = new K8(configManager, testLogger) - const accountManager = new AccountManager(testLogger, k8, constants) + afterAll(async () => { + await k8.deleteNamespace(namespace) + await accountManager.close() + }, 180000) it('should be able to stop port forwards', async () => { expect.assertions(4) diff --git a/test/e2e/core/chart_manager.test.mjs b/test/e2e/core/chart_manager.test.mjs index 5761ccc92..bfa2ab867 100644 --- a/test/e2e/core/chart_manager.test.mjs +++ b/test/e2e/core/chart_manager.test.mjs @@ -38,7 +38,7 @@ describe('ChartManager', () => { it('should be able to check if a chart is installed', async () => { const ns = configManager.getFlag(flags.namespace) expect(ns).not.toBeNull() - const isInstalled = await chartManager.isChartInstalled(ns, constants.FULLSTACK_DEPLOYMENT_CHART) + const isInstalled = await chartManager.isChartInstalled(ns, constants.FULLSTACK_CLUSTER_SETUP_CHART) expect(isInstalled).toBeTruthy() }) }) diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index 981f6f614..66d681ce0 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -14,7 +14,7 @@ * limitations under the License. * */ -import { beforeAll, describe, expect, it } from '@jest/globals' +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' import fs from 'fs' import net from 'net' import os from 'os' @@ -23,6 +23,14 @@ import { v4 as uuid4 } from 'uuid' import { FullstackTestingError } from '../../../src/core/errors.mjs' import { ConfigManager, constants, logging, Templates } from '../../../src/core/index.mjs' import { K8 } from '../../../src/core/k8.mjs' +import { flags } from '../../../src/commands/index.mjs' +import { + V1Container, + V1ObjectMeta, + V1PersistentVolumeClaim, V1PersistentVolumeClaimSpec, + V1Pod, + V1PodSpec, V1VolumeResourceRequirements +} from '@kubernetes/client-node' const defaultTimeout = 20000 @@ -30,9 +38,18 @@ describe('K8', () => { const testLogger = logging.NewLogger('debug', true) const configManager = new ConfigManager(testLogger) const k8 = new K8(configManager, testLogger) + const testNamespace = 'k8-e2e' + const argv = [] - beforeAll(() => { - configManager.load() + beforeAll(async () => { + argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + configManager.update(argv) + await k8.createNamespace(testNamespace) + }, defaultTimeout) + + afterAll(async () => { + await k8.kubeClient.deleteNamespacedPod('test-pod', testNamespace) + await k8.deleteNamespace(testNamespace) }, defaultTimeout) it('should be able to list clusters', async () => { @@ -58,41 +75,65 @@ describe('K8', () => { }, defaultTimeout) it('should be able to detect pod IP of a pod', async () => { - const podName = Templates.renderNetworkPodName('node0') + const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const podName = pods[0].metadata.name await expect(k8.getPodIP(podName)).resolves.not.toBeNull() await expect(k8.getPodIP('INVALID')).rejects.toThrow(FullstackTestingError) }, defaultTimeout) it('should be able to detect cluster IP', async () => { - const svcName = Templates.renderNetworkSvcName('node0') + const svcName = 'console' await expect(k8.getClusterIP(svcName)).resolves.not.toBeNull() await expect(k8.getClusterIP('INVALID')).rejects.toThrow(FullstackTestingError) }, defaultTimeout) it('should be able to check if a path is directory inside a container', async () => { - const podName = Templates.renderNetworkPodName('node0') - await expect(k8.hasDir(podName, constants.ROOT_CONTAINER, constants.HEDERA_USER_HOME_DIR)).resolves.toBeTruthy() + const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const podName = pods[0].metadata.name + await expect(k8.hasDir(podName, 'minio-operator', '/tmp')).resolves.toBeTruthy() }, defaultTimeout) it('should be able to copy a file to and from a container', async () => { - const podName = Templates.renderNetworkPodName('node0') - const containerName = constants.ROOT_CONTAINER - - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) - const destDir = constants.HEDERA_USER_HOME_DIR - const srcPath = 'test/data/pem/keys/a-private-node0.pem' - const destPath = `${destDir}/a-private-node0.pem` - - // upload the file - await expect(k8.copyTo(podName, containerName, srcPath, destDir)).resolves.toBeTruthy() - - // download the same file - await expect(k8.copyFrom(podName, containerName, destPath, tmpDir)).resolves.toBeTruthy() - - // rm file inside the container - await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves - - fs.rmdirSync(tmpDir, { recursive: true }) + const podName = 'test-pod' + const containerName = 'alpine' + const v1Pod = new V1Pod() + const v1Metadata = new V1ObjectMeta() + v1Metadata.name = podName + v1Metadata.namespace = testNamespace + v1Metadata.labels = { app: 'test' } + v1Pod.metadata = v1Metadata + const v1Container = new V1Container() + v1Container.name = containerName + v1Container.image = 'alpine:latest' + v1Container.command = ['/bin/sh', '-c', 'sleep 7200'] + const v1Spec = new V1PodSpec() + v1Spec.containers = [v1Container] + v1Pod.spec = v1Spec + await k8.kubeClient.createNamespacedPod(testNamespace, v1Pod) + try { + argv[flags.namespace.name] = testNamespace + configManager.update(argv) + const pods = await k8.waitForPodReady(['app=test'], 1, 20) + expect(pods.length).toStrictEqual(1) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) + const destDir = '/tmp' + const srcPath = 'test/data/pem/keys/a-private-node0.pem' + const destPath = `${destDir}/a-private-node0.pem` + + // upload the file + await expect(k8.copyTo(podName, containerName, srcPath, destDir)).resolves.toBeTruthy() + + // download the same file + await expect(k8.copyFrom(podName, containerName, destPath, tmpDir)).resolves.toBeTruthy() + + // rm file inside the container + await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves + + fs.rmdirSync(tmpDir, { recursive: true }) + } finally { + argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + configManager.update(argv) + } }, defaultTimeout) it('should be able to port forward gossip port', (done) => { @@ -122,11 +163,12 @@ describe('K8', () => { testLogger.showUserError(e) expect(e).toBeNull() } + // TODO why is this passing? }, defaultTimeout) it('should be able to run wait for pod', async () => { const labels = [ - 'fullstack.hedera.com/type=network-node' + 'app.kubernetes.io/instance=fullstack-cluster-setup-console' ] const pods = await k8.waitForPods([constants.POD_PHASE_RUNNING], labels, 1) @@ -135,7 +177,7 @@ describe('K8', () => { it('should be able to run wait for pod ready', async () => { const labels = [ - 'fullstack.hedera.com/type=network-node' + 'app.kubernetes.io/instance=fullstack-cluster-setup-console' ] const pods = await k8.waitForPodReady(labels, 1) @@ -144,7 +186,7 @@ describe('K8', () => { it('should be able to run wait for pod conditions', async () => { const labels = [ - 'fullstack.hedera.com/type=network-node' + 'app.kubernetes.io/instance=fullstack-cluster-setup-console' ] const conditions = new Map() @@ -155,25 +197,36 @@ describe('K8', () => { expect(pods.length).toStrictEqual(1) }, defaultTimeout) - it('should be able to cat a log file inside the container', async () => { - const podName = Templates.renderNetworkPodName('node0') - const containerName = constants.ROOT_CONTAINER - const testFileName = 'test.txt' - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) - const tmpFile = path.join(tmpDir, testFileName) - const destDir = constants.HEDERA_USER_HOME_DIR - const destPath = `${destDir}/${testFileName}` - fs.writeFileSync(tmpFile, 'TEST\nNow current platform status = ACTIVE') - - await expect(k8.copyTo(podName, containerName, tmpFile, destDir)).resolves.toBeTruthy() - const output = await k8.execContainer(podName, containerName, ['tail', '-10', destPath]) - expect(output.indexOf('Now current platform status = ACTIVE')).toBeGreaterThan(0) - - fs.rmdirSync(tmpDir, { recursive: true }) + it('should be able to cat a file inside the container', async () => { + const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const podName = pods[0].metadata.name + const containerName = 'minio-operator' + const output = await k8.execContainer(podName, containerName, ['cat', '/etc/hostname']) + expect(output.indexOf('console')).toEqual(0) }, defaultTimeout) it('should be able to list persistent volume claims', async () => { - const pvcs = await k8.listPvcsByNamespace(k8._getNamespace()) - expect(pvcs.length).toBeGreaterThan(0) + let response + try { + const v1Pvc = new V1PersistentVolumeClaim() + v1Pvc.name = 'test-pvc' + const v1Spec = new V1PersistentVolumeClaimSpec() + v1Spec.accessModes = ['ReadWriteOnce'] + const v1ResReq = new V1VolumeResourceRequirements() + v1ResReq.requests = { storage: '50Mi' } + v1Spec.resources = v1ResReq + v1Pvc.spec = v1Spec + const v1Metadata = new V1ObjectMeta() + v1Metadata.name = 'test-pvc' + v1Pvc.metadata = v1Metadata + // PersistentVolumeClaim "" is invalid: [metadata.name: Required value: name or generateName is required, spec.resources[storage]: Required value] + response = await k8.kubeClient.createNamespacedPersistentVolumeClaim(testNamespace, v1Pvc) + console.log(response) + const pvcs = await k8.listPvcsByNamespace(testNamespace) + expect(pvcs.length).toBeGreaterThan(0) + } catch (e) { + console.error(e) + throw e + } }, defaultTimeout) }) diff --git a/test/e2e/e2e_node_util.js b/test/e2e/e2e_node_util.js index 12f2b09d7..b65691055 100644 --- a/test/e2e/e2e_node_util.js +++ b/test/e2e/e2e_node_util.js @@ -48,7 +48,7 @@ export function e2eNodeKeyRefreshTest (keyFormat, testName, mode, releaseTag = H argv[flags.namespace.name] = namespace argv[flags.releaseTag.name] = releaseTag argv[flags.keyFormat.name] = keyFormat - argv[flags.nodeIDs.name] = 'node0,node1,node2,node3' + argv[flags.nodeIDs.name] = 'node0,node1,node2' argv[flags.generateGossipKeys.name] = true argv[flags.generateTlsKeys.name] = true argv[flags.clusterName.name] = TEST_CLUSTER diff --git a/test/e2e/setup-e2e.sh b/test/e2e/setup-e2e.sh index 57da9e452..5c94f50fb 100755 --- a/test/e2e/setup-e2e.sh +++ b/test/e2e/setup-e2e.sh @@ -20,4 +20,3 @@ kind create cluster -n "${SOLO_CLUSTER_NAME}" --image "${KIND_IMAGE}" || exit 1 solo init --namespace "${SOLO_NAMESPACE}" -i node0 -s "${SOLO_CLUSTER_SETUP_NAMESPACE}" -d "${SOLO_FST_CHARTS_DIR}" --dev || exit 1 # cache args for subsequent commands solo cluster setup || exit 1 helm list --all-namespaces -solo network deploy || exit 1 diff --git a/test/test_util.js b/test/test_util.js index 6e549127f..09dd6f319 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -174,6 +174,7 @@ export function bootstrapTestVariables (testName, argv, * @param networkCmdArg an instance of command/NetworkCommand * @param nodeCmdArg an instance of command/NodeCommand * @param accountCmdArg an instance of command/AccountCommand + * @param startNodes start nodes after deployment, default is true */ export function bootstrapNetwork (testName, argv, k8Arg = null, @@ -181,7 +182,8 @@ export function bootstrapNetwork (testName, argv, clusterCmdArg = null, networkCmdArg = null, nodeCmdArg = null, - accountCmdArg = null + accountCmdArg = null, + startNodes = true ) { const bootstrapResp = bootstrapTestVariables(testName, argv, k8Arg, initCmdArg, clusterCmdArg, networkCmdArg, nodeCmdArg, accountCmdArg) const namespace = bootstrapResp.namespace @@ -233,35 +235,37 @@ export function bootstrapNetwork (testName, argv, ]) }, 180000) - it('should succeed with node setup command', async () => { - expect.assertions(2) - try { - await expect(nodeCmd.setup(argv)).resolves.toBeTruthy() - expect(nodeCmd.getUnusedConfigs(NodeCommand.SETUP_CONFIGS_NAME)).toEqual([ - flags.apiPermissionProperties.constName, - flags.appConfig.constName, - flags.applicationProperties.constName, - flags.bootstrapProperties.constName, - flags.devMode.constName, - flags.localBuildPath.constName, - flags.log4j2Xml.constName, - flags.settingTxt.constName - ]) - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } - }, 240000) - - it('should succeed with node start command', async () => { - expect.assertions(1) - try { - await expect(nodeCmd.start(argv)).resolves.toBeTruthy() - } catch (e) { - nodeCmd.logger.showUserError(e) - expect(e).toBeNull() - } - }, 1800000) + if (startNodes) { + it('should succeed with node setup command', async () => { + expect.assertions(2) + try { + await expect(nodeCmd.setup(argv)).resolves.toBeTruthy() + expect(nodeCmd.getUnusedConfigs(NodeCommand.SETUP_CONFIGS_NAME)).toEqual([ + flags.apiPermissionProperties.constName, + flags.appConfig.constName, + flags.applicationProperties.constName, + flags.bootstrapProperties.constName, + flags.devMode.constName, + flags.localBuildPath.constName, + flags.log4j2Xml.constName, + flags.settingTxt.constName + ]) + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } + }, 240000) + + it('should succeed with node start command', async () => { + expect.assertions(1) + try { + await expect(nodeCmd.start(argv)).resolves.toBeTruthy() + } catch (e) { + nodeCmd.logger.showUserError(e) + expect(e).toBeNull() + } + }, 1800000) + } }) return bootstrapResp From e9105a7b0926708fe4e0819e2ccb766cfebd80e8 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 16:45:28 +0100 Subject: [PATCH 46/56] making tests more resilient Signed-off-by: Jeromy Cannon --- test/e2e/core/chart_manager.test.mjs | 4 +- test/e2e/core/k8_e2e.test.mjs | 2 + test/e2e/core/platform_installer_e2e.test.mjs | 42 +++++++++++++------ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/test/e2e/core/chart_manager.test.mjs b/test/e2e/core/chart_manager.test.mjs index bfa2ab867..29e33eb8e 100644 --- a/test/e2e/core/chart_manager.test.mjs +++ b/test/e2e/core/chart_manager.test.mjs @@ -23,9 +23,11 @@ describe('ChartManager', () => { const helm = new Helm(testLogger) const chartManager = new ChartManager(helm, testLogger) const configManager = new ConfigManager(testLogger) + const argv = [] beforeAll(() => { - configManager.load() + argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + configManager.update(argv) }) it('should be able to list installed charts', async () => { diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index 66d681ce0..f6a7faa40 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -50,6 +50,8 @@ describe('K8', () => { afterAll(async () => { await k8.kubeClient.deleteNamespacedPod('test-pod', testNamespace) await k8.deleteNamespace(testNamespace) + argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + configManager.update(argv) }, defaultTimeout) it('should be able to list clusters', async () => { diff --git a/test/e2e/core/platform_installer_e2e.test.mjs b/test/e2e/core/platform_installer_e2e.test.mjs index 40769ff81..a4856ea56 100644 --- a/test/e2e/core/platform_installer_e2e.test.mjs +++ b/test/e2e/core/platform_installer_e2e.test.mjs @@ -14,30 +14,48 @@ * limitations under the License. * */ -import { beforeAll, describe, expect, it } from '@jest/globals' +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' import { - PlatformInstaller, constants, - Templates, - ConfigManager, Templates as Template + Templates } from '../../../src/core/index.mjs' import * as fs from 'fs' -import { K8 } from '../../../src/core/k8.mjs' -import { getTestCacheDir, getTmpDir, testLogger } from '../../test_util.js' -import { AccountManager } from '../../../src/core/account_manager.mjs' +import { + bootstrapNetwork, + getDefaultArgv, + getTestCacheDir, + getTmpDir, TEST_CLUSTER, + testLogger +} from '../../test_util.js' +import { flags } from '../../../src/commands/index.mjs' +import * as version from '../../../version.mjs' const defaultTimeout = 20000 describe('PackageInstallerE2E', () => { - const configManager = new ConfigManager(testLogger) - const k8 = new K8(configManager, testLogger) - const accountManager = new AccountManager(testLogger, k8) - const installer = new PlatformInstaller(testLogger, k8, configManager, accountManager) + const namespace = 'pkg-installer-e2e' + const argv = getDefaultArgv() + argv[flags.namespace.name] = namespace + argv[flags.nodeIDs.name] = 'node0' + argv[flags.clusterName.name] = TEST_CLUSTER + argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION + // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts + argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined + const bootstrapResp = bootstrapNetwork(namespace, argv, null, null, null, null, null, null, false) + const k8 = bootstrapResp.opts.k8 + const accountManager = bootstrapResp.opts.accountManager + const configManager = bootstrapResp.opts.configManager + const installer = bootstrapResp.opts.platformInstaller const testCacheDir = getTestCacheDir() const podName = 'network-node0-0' const packageVersion = 'v0.42.5' + afterAll(async () => { + await k8.deleteNamespace(namespace) + await accountManager.close() + }, 180000) + beforeAll(async () => { if (!fs.existsSync(testCacheDir)) { fs.mkdirSync(testCacheDir) @@ -144,7 +162,7 @@ describe('PackageInstallerE2E', () => { describe('copyTLSKeys', () => { it('should succeed to copy TLS keys for node0', async () => { const nodeId = 'node0' - const podName = Template.renderNetworkPodName(nodeId) + const podName = Templates.renderNetworkPodName(nodeId) const tmpDir = getTmpDir() // create mock files From eb4e598d78cbdb40b4bf974d7d13e16775fbb031 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 16:54:42 +0100 Subject: [PATCH 47/56] beta version v2.50.0-beta.3 with DAB functionality Signed-off-by: Jeromy Cannon --- package-lock.json | 108 +++++++++++++--------------------------------- package.json | 2 +- 2 files changed, 32 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13bbb9e5b..825c44953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ ], "dependencies": { "@hashgraph/proto": "^2.15.0", - "@hashgraph/sdk": "^2.49.2", + "@hashgraph/sdk": "^2.50.0-beta.3", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", "@peculiar/x509": "^1.12.0", @@ -1166,9 +1166,9 @@ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/@hashgraph/cryptography": { - "version": "1.4.8-beta.5", - "resolved": "https://registry.npmjs.org/@hashgraph/cryptography/-/cryptography-1.4.8-beta.5.tgz", - "integrity": "sha512-soq2vGLRkdl2Evr+gIvIjCXJjqA1hOAjysBGG+dhP6tKx2PEgEjb3hON/sMbxm3Q4qQdkML/vEthdAV707+flw==", + "version": "1.4.8-beta.6", + "resolved": "https://registry.npmjs.org/@hashgraph/cryptography/-/cryptography-1.4.8-beta.6.tgz", + "integrity": "sha512-FR8uG5XpLj/rFpEBNEhqB/988spnblAiCSJEOVxfc8X8AH1dVH3kWF5/4sI9fmbviySM7rgoXNdcM0mRZvXMJw==", "dependencies": { "asn1js": "^3.0.5", "bignumber.js": "^9.1.1", @@ -1202,29 +1202,6 @@ } } }, - "node_modules/@hashgraph/cryptography/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/@hashgraph/proto": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0.tgz", @@ -1238,16 +1215,16 @@ } }, "node_modules/@hashgraph/sdk": { - "version": "2.49.2", - "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.49.2.tgz", - "integrity": "sha512-HqESeH6gF/QEm69qmEyPZ40i9w2jBXsyXFqT/kRsrb7yEyCdVrG1Wnt88HxOSP9XyOsm3hj4OBTEiy2sI+kl+A==", + "version": "2.50.0-beta.3", + "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.50.0-beta.3.tgz", + "integrity": "sha512-0mGnzbeEl7e3UqlCDmXLIm/jUJNF+VNhuxaqq6wSxZe8fau7TXkOjat5nP4XUcYOTgEHJ4rWeO+WEukMDL2h3w==", "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", "@ethersproject/bytes": "^5.7.0", "@ethersproject/rlp": "^5.7.0", "@grpc/grpc-js": "1.8.2", - "@hashgraph/cryptography": "1.4.8-beta.5", + "@hashgraph/cryptography": "1.4.8-beta.6", "@hashgraph/proto": "2.15.0-beta.3", "axios": "^1.6.4", "bignumber.js": "^9.1.1", @@ -3378,6 +3355,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9481,29 +9481,6 @@ "split2": "^4.0.0" } }, - "node_modules/pino-abstract-transport/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/pino-abstract-transport/node_modules/readable-stream": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", @@ -9543,29 +9520,6 @@ "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/pino-pretty/node_modules/readable-stream": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", diff --git a/package.json b/package.json index 25ea10c45..ace3b436f 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "license": "Apache2.0", "dependencies": { "@hashgraph/proto": "^2.15.0", - "@hashgraph/sdk": "^2.49.2", + "@hashgraph/sdk": "^2.50.0-beta.3", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", "@peculiar/x509": "^1.12.0", From 8f170735bfce08d833fdb817375d4ea87d4fd70e Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 17:06:16 +0100 Subject: [PATCH 48/56] fix static code analysis issues Signed-off-by: Jeromy Cannon --- resources/support-zip.sh | 2 +- src/core/helpers.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/support-zip.sh b/resources/support-zip.sh index 84d007d18..56da3ab76 100644 --- a/resources/support-zip.sh +++ b/resources/support-zip.sh @@ -41,4 +41,4 @@ AddToFileList ${ADDRESS_BOOK_DIR} AddToFileList ${CONFIG_DIR} AddToFileList ${KEYS_DIR} AddToFileList ${UPGRADE_DIR} -jar cvfM ${ZIP_FULLPATH} @${FILE_LIST} +jar cvfM "${ZIP_FULLPATH}" "@${FILE_LIST}" diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index 2732e1af8..a79ffdb47 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -204,7 +204,7 @@ export async function getNodeLogs (k8, namespace) { for (const pod of pods) { const podName = pod.metadata.name - const targetDir = `${SOLO_LOGS_DIR}/${namespace}/${timeString}` + const targetDir = path.join(SOLO_LOGS_DIR, namespace, timeString) try { if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }) From 999ebf480a2987df742ecd5378e2f9ad3827150d Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 18:53:09 +0100 Subject: [PATCH 49/56] all of the sudden helm is not being found in the pipeline? Signed-off-by: Jeromy Cannon --- test/e2e/setup-e2e.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/setup-e2e.sh b/test/e2e/setup-e2e.sh index 5c94f50fb..f5f8c68e7 100755 --- a/test/e2e/setup-e2e.sh +++ b/test/e2e/setup-e2e.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash readonly KIND_IMAGE="kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72" echo "SOLO_FST_CHARTS_DIR: ${SOLO_FST_CHARTS_DIR}" +export PATH=${PATH}:~/.solo/bin SOLO_CLUSTER_NAME=solo-e2e SOLO_NAMESPACE=solo-e2e From c8a44bb8ed74e52241601a671e4a6e46904a0f61 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 21:53:40 +0100 Subject: [PATCH 50/56] cleanup Signed-off-by: Jeromy Cannon --- src/commands/node.mjs | 40 ++++++++++++++++------------- src/core/helpers.mjs | 4 +-- src/core/key_manager.mjs | 3 +-- test/e2e/commands/node-add.test.mjs | 2 +- test/e2e/core/k8_e2e.test.mjs | 3 +-- test/test_util.js | 2 +- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 4d873e07c..5e3885a28 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -44,7 +44,8 @@ import { import * as crypto from 'crypto' import { FREEZE_ADMIN_ACCOUNT, - HEDERA_NODE_DEFAULT_STAKE_AMOUNT, TREASURY_ACCOUNT_ID + HEDERA_NODE_DEFAULT_STAKE_AMOUNT, + TREASURY_ACCOUNT_ID } from '../core/constants.mjs' /** @@ -934,6 +935,7 @@ export class NodeCommand extends BaseCommand { ]) ctx.config = { + app: self.configManager.getFlag(flags.app), cacheDir: self.configManager.getFlag(flags.cacheDir), namespace: self.configManager.getFlag(flags.namespace), nodeIds: helpers.parseNodeIds(self.configManager.getFlag(flags.nodeIDs)) @@ -1019,23 +1021,25 @@ export class NodeCommand extends BaseCommand { { title: 'Add node stakes', task: (ctx, task) => { - const subTasks = [] - const accountMap = getNodeAccountMap(ctx.config.nodeIds) - for (const nodeId of ctx.config.nodeIds) { - const accountId = accountMap.get(nodeId) - subTasks.push({ - title: `Adding stake for node: ${chalk.yellow(nodeId)}`, - task: () => self.addStake(ctx.config.namespace, accountId, nodeId) + if (ctx.config.app === '' || ctx.config.app === constants.HEDERA_APP_NAME) { + const subTasks = [] + const accountMap = getNodeAccountMap(ctx.config.nodeIds) + for (const nodeId of ctx.config.nodeIds) { + const accountId = accountMap.get(nodeId) + subTasks.push({ + title: `Adding stake for node: ${chalk.yellow(nodeId)}`, + task: () => self.addStake(ctx.config.namespace, accountId, nodeId) + }) + } + + // set up the sub-tasks + return task.newListr(subTasks, { + concurrent: false, + rendererOptions: { + collapseSubtasks: false + } }) } - - // set up the sub-tasks - return task.newListr(subTasks, { - concurrent: false, - rendererOptions: { - collapseSubtasks: false - } - }) } }], { concurrent: false, @@ -1778,8 +1782,6 @@ export class NodeCommand extends BaseCommand { const config = /** @type {NodeAddConfigClass} **/ ctx.config try { - config.nodeClient.setOperator(TREASURY_ACCOUNT_ID, config.treasuryKey) - const nodeCreateTx = await new NodeCreateTransaction() .setAccountId(ctx.newNode.accountId) .setGossipEndpoints(ctx.gossipEndpoints) @@ -1793,6 +1795,7 @@ export class NodeCommand extends BaseCommand { const nodeCreateReceipt = await txResp.getReceipt(config.nodeClient) this.logger.debug(`NodeCreateReceipt: ${nodeCreateReceipt.toString()}`) } catch (e) { + this.logger.error(`Error adding node to network: ${e.message}`, e) throw new FullstackTestingError(`Error adding node to network: ${e.message}`, e) } } @@ -2230,6 +2233,7 @@ export class NodeCommand extends BaseCommand { command: 'start', desc: 'Start a node', builder: y => flags.setCommandFlags(y, + flags.app, flags.namespace, flags.nodeIDs ), diff --git a/src/core/helpers.mjs b/src/core/helpers.mjs index db21b01cc..772c42f6e 100644 --- a/src/core/helpers.mjs +++ b/src/core/helpers.mjs @@ -149,7 +149,7 @@ export function backupOldTlsKeys (nodeIds, keysDir, curDate = new Date(), dirPre const fileMap = new Map() for (const nodeId of nodeIds) { const srcPath = path.join(keysDir, Templates.renderTLSPemPrivateKeyFile(nodeId)) - const destPath = path.join(backupDir, Templates.renderTLSPemPublicKeyFile(nodeId)) + const destPath = path.join(backupDir, Templates.renderTLSPemPrivateKeyFile(nodeId)) fileMap.set(srcPath, destPath) } @@ -163,7 +163,7 @@ export function backupOldPemKeys (nodeIds, keysDir, curDate = new Date(), dirPre const fileMap = new Map() for (const nodeId of nodeIds) { const srcPath = path.join(keysDir, Templates.renderGossipPemPrivateKeyFile(nodeId)) - const destPath = path.join(backupDir, Templates.renderGossipPemPublicKeyFile(nodeId)) + const destPath = path.join(backupDir, Templates.renderGossipPemPrivateKeyFile(nodeId)) fileMap.set(srcPath, destPath) } diff --git a/src/core/key_manager.mjs b/src/core/key_manager.mjs index 8eb927f75..c3381db5b 100644 --- a/src/core/key_manager.mjs +++ b/src/core/key_manager.mjs @@ -193,8 +193,7 @@ export class KeyManager { }) self.logger.debug(`Stored ${keyName} key for node: ${nodeId}`, { - nodeKeyFiles, - cert: certPems[0] + nodeKeyFiles }) resolve(nodeKeyFiles) diff --git a/test/e2e/commands/node-add.test.mjs b/test/e2e/commands/node-add.test.mjs index bc0fd5da2..1c7715be2 100644 --- a/test/e2e/commands/node-add.test.mjs +++ b/test/e2e/commands/node-add.test.mjs @@ -60,7 +60,7 @@ describe('Node add', () => { afterAll(async () => { await getNodeLogs(k8, namespace) await k8.deleteNamespace(namespace) - }, defaultTimeout) + }, 600000) it('should succeed with init command', async () => { const status = await accountCmd.init(argv) diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index f6a7faa40..99baef974 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -48,7 +48,6 @@ describe('K8', () => { }, defaultTimeout) afterAll(async () => { - await k8.kubeClient.deleteNamespacedPod('test-pod', testNamespace) await k8.deleteNamespace(testNamespace) argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE configManager.update(argv) @@ -165,7 +164,7 @@ describe('K8', () => { testLogger.showUserError(e) expect(e).toBeNull() } - // TODO why is this passing? + // TODO enhance this test to do something with the port, this pod isn't even running, but it is still passing }, defaultTimeout) it('should be able to run wait for pod', async () => { diff --git a/test/test_util.js b/test/test_util.js index 09dd6f319..10816f608 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -327,7 +327,7 @@ export function accountCreationShouldSucceed (accountManager, nodeCmd, namespace } export async function getNodeIdsPrivateKeysHash (networkNodeServicesMap, namespace, keyFormat, k8, destDir) { - const dataKeysDir = `${constants.HEDERA_HAPI_PATH}/data/keys` + 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()) { From 3066b5af91edc6e7177de3dfbbbc1adbbd0d88ba Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 21:55:51 +0100 Subject: [PATCH 51/56] cleanup Signed-off-by: Jeromy Cannon --- test/e2e/core/k8_e2e.test.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index 99baef974..8bf678afd 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -220,7 +220,6 @@ describe('K8', () => { const v1Metadata = new V1ObjectMeta() v1Metadata.name = 'test-pvc' v1Pvc.metadata = v1Metadata - // PersistentVolumeClaim "" is invalid: [metadata.name: Required value: name or generateName is required, spec.resources[storage]: Required value] response = await k8.kubeClient.createNamespacedPersistentVolumeClaim(testNamespace, v1Pvc) console.log(response) const pvcs = await k8.listPvcsByNamespace(testNamespace) From 19b54ca4d0cd116973039c836b9e6d3ea70aab8f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Tue, 13 Aug 2024 22:16:59 +0100 Subject: [PATCH 52/56] fixed mirror node test case Signed-off-by: Jeromy Cannon --- test/e2e/commands/mirror_node.test.mjs | 5 +++++ test/e2e/commands/node-local-hedera.test.mjs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/test/e2e/commands/mirror_node.test.mjs b/test/e2e/commands/mirror_node.test.mjs index 53711ce70..f017eb708 100644 --- a/test/e2e/commands/mirror_node.test.mjs +++ b/test/e2e/commands/mirror_node.test.mjs @@ -29,6 +29,7 @@ import { constants } from '../../../src/core/index.mjs' import { + accountCreationShouldSucceed, balanceQueryShouldSucceed, bootstrapNetwork, getDefaultArgv, @@ -160,6 +161,10 @@ describe('MirrorNodeCommand', () => { } }, 60000) + // trigger some extra transactions to trigger MirrorNode to fetch the transactions + accountCreationShouldSucceed(accountManager, mirrorNodeCmd, namespace) + accountCreationShouldSucceed(accountManager, mirrorNodeCmd, namespace) + it('Check submit message result should success', async () => { expect.assertions(1) try { diff --git a/test/e2e/commands/node-local-hedera.test.mjs b/test/e2e/commands/node-local-hedera.test.mjs index dbb6f5ef6..3e805a9d4 100644 --- a/test/e2e/commands/node-local-hedera.test.mjs +++ b/test/e2e/commands/node-local-hedera.test.mjs @@ -44,7 +44,7 @@ describe('Node local build', () => { afterAll(async () => { await getNodeLogs(hederaK8, LOCAL_HEDERA) await hederaK8.deleteNamespace(LOCAL_HEDERA) - }, 120000) + }, 600000) describe('Node for hedera app should start successfully', () => { console.log('Starting local build for Hedera app') From c8c9aac04e4c926905bebd427bc34dfef217f12f Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Aug 2024 08:14:00 +0100 Subject: [PATCH 53/56] fixed k8 and afterAll in node e2e Signed-off-by: Jeromy Cannon --- test/e2e/core/k8_e2e.test.mjs | 61 ++++++++++++++++------------------- test/e2e/e2e_node_util.js | 2 +- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index 8bf678afd..cc44b2525 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -40,11 +40,28 @@ describe('K8', () => { const k8 = new K8(configManager, testLogger) const testNamespace = 'k8-e2e' const argv = [] + const podName = 'test-pod' + const containerName = 'alpine' + const podLabel = 'app=test' beforeAll(async () => { - argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + argv[flags.namespace.name] = testNamespace configManager.update(argv) await k8.createNamespace(testNamespace) + const v1Pod = new V1Pod() + const v1Metadata = new V1ObjectMeta() + v1Metadata.name = podName + v1Metadata.namespace = testNamespace + v1Metadata.labels = { app: 'test' } + v1Pod.metadata = v1Metadata + const v1Container = new V1Container() + v1Container.name = containerName + v1Container.image = 'alpine:latest' + v1Container.command = ['/bin/sh', '-c', 'sleep 7200'] + const v1Spec = new V1PodSpec() + v1Spec.containers = [v1Container] + v1Pod.spec = v1Spec + await k8.kubeClient.createNamespacedPod(testNamespace, v1Pod) }, defaultTimeout) afterAll(async () => { @@ -76,45 +93,30 @@ describe('K8', () => { }, defaultTimeout) it('should be able to detect pod IP of a pod', async () => { - const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const pods = await k8.getPodsByLabel([podLabel]) const podName = pods[0].metadata.name await expect(k8.getPodIP(podName)).resolves.not.toBeNull() await expect(k8.getPodIP('INVALID')).rejects.toThrow(FullstackTestingError) }, defaultTimeout) it('should be able to detect cluster IP', async () => { + // TODO const svcName = 'console' await expect(k8.getClusterIP(svcName)).resolves.not.toBeNull() await expect(k8.getClusterIP('INVALID')).rejects.toThrow(FullstackTestingError) }, defaultTimeout) it('should be able to check if a path is directory inside a container', async () => { - const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const pods = await k8.getPodsByLabel([podLabel]) const podName = pods[0].metadata.name - await expect(k8.hasDir(podName, 'minio-operator', '/tmp')).resolves.toBeTruthy() + await expect(k8.hasDir(podName, containerName, '/tmp')).resolves.toBeTruthy() }, defaultTimeout) it('should be able to copy a file to and from a container', async () => { - const podName = 'test-pod' - const containerName = 'alpine' - const v1Pod = new V1Pod() - const v1Metadata = new V1ObjectMeta() - v1Metadata.name = podName - v1Metadata.namespace = testNamespace - v1Metadata.labels = { app: 'test' } - v1Pod.metadata = v1Metadata - const v1Container = new V1Container() - v1Container.name = containerName - v1Container.image = 'alpine:latest' - v1Container.command = ['/bin/sh', '-c', 'sleep 7200'] - const v1Spec = new V1PodSpec() - v1Spec.containers = [v1Container] - v1Pod.spec = v1Spec - await k8.kubeClient.createNamespacedPod(testNamespace, v1Pod) try { argv[flags.namespace.name] = testNamespace configManager.update(argv) - const pods = await k8.waitForPodReady(['app=test'], 1, 20) + const pods = await k8.waitForPodReady([podLabel], 1, 20) expect(pods.length).toStrictEqual(1) const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) const destDir = '/tmp' @@ -168,27 +170,21 @@ describe('K8', () => { }, defaultTimeout) it('should be able to run wait for pod', async () => { - const labels = [ - 'app.kubernetes.io/instance=fullstack-cluster-setup-console' - ] + const labels = [podLabel] const pods = await k8.waitForPods([constants.POD_PHASE_RUNNING], labels, 1) expect(pods.length).toStrictEqual(1) }, defaultTimeout) it('should be able to run wait for pod ready', async () => { - const labels = [ - 'app.kubernetes.io/instance=fullstack-cluster-setup-console' - ] + const labels = [podLabel] const pods = await k8.waitForPodReady(labels, 1) expect(pods.length).toStrictEqual(1) }, defaultTimeout) it('should be able to run wait for pod conditions', async () => { - const labels = [ - 'app.kubernetes.io/instance=fullstack-cluster-setup-console' - ] + const labels = [podLabel] const conditions = new Map() .set(constants.POD_CONDITION_INITIALIZED, constants.POD_CONDITION_STATUS_TRUE) @@ -199,11 +195,10 @@ describe('K8', () => { }, defaultTimeout) it('should be able to cat a file inside the container', async () => { - const pods = await k8.getPodsByLabel(['app.kubernetes.io/instance=fullstack-cluster-setup-console']) + const pods = await k8.getPodsByLabel([podLabel]) const podName = pods[0].metadata.name - const containerName = 'minio-operator' const output = await k8.execContainer(podName, containerName, ['cat', '/etc/hostname']) - expect(output.indexOf('console')).toEqual(0) + expect(output.indexOf(podName)).toEqual(0) }, defaultTimeout) it('should be able to list persistent volume claims', async () => { diff --git a/test/e2e/e2e_node_util.js b/test/e2e/e2e_node_util.js index b65691055..44104bf77 100644 --- a/test/e2e/e2e_node_util.js +++ b/test/e2e/e2e_node_util.js @@ -71,7 +71,7 @@ export function e2eNodeKeyRefreshTest (keyFormat, testName, mode, releaseTag = H afterAll(async () => { await getNodeLogs(k8, namespace) await k8.deleteNamespace(namespace) - }, 180000) + }, 600000) describe( `Node should have started successfully [mode ${mode}, release ${releaseTag}, keyFormat: ${keyFormat}]`, From bb0605ba96420c856e20211b4d8c93022acbc306 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Aug 2024 13:50:33 +0100 Subject: [PATCH 54/56] fixed k8 e2e test Signed-off-by: Jeromy Cannon --- test/e2e/core/account_manager.test.mjs | 2 +- test/e2e/core/k8_e2e.test.mjs | 161 ++++++++++-------- test/e2e/core/platform_installer_e2e.test.mjs | 2 +- 3 files changed, 93 insertions(+), 72 deletions(-) diff --git a/test/e2e/core/account_manager.test.mjs b/test/e2e/core/account_manager.test.mjs index 1edaaa62b..d7c79ce60 100644 --- a/test/e2e/core/account_manager.test.mjs +++ b/test/e2e/core/account_manager.test.mjs @@ -32,7 +32,7 @@ describe('AccountManager', () => { argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - const bootstrapResp = bootstrapNetwork(namespace, argv, null, null, null, null, null, null, false) + const bootstrapResp = bootstrapNetwork(namespace, argv, undefined, undefined, undefined, undefined, undefined, undefined, false) const k8 = bootstrapResp.opts.k8 const accountManager = bootstrapResp.opts.accountManager const configManager = bootstrapResp.opts.configManager diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index cc44b2525..c85428116 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -27,9 +27,14 @@ import { flags } from '../../../src/commands/index.mjs' import { V1Container, V1ObjectMeta, - V1PersistentVolumeClaim, V1PersistentVolumeClaimSpec, + V1PersistentVolumeClaim, + V1PersistentVolumeClaimSpec, V1Pod, - V1PodSpec, V1VolumeResourceRequirements + V1PodSpec, + V1Service, + V1ServicePort, + V1ServiceSpec, + V1VolumeResourceRequirements } from '@kubernetes/client-node' const defaultTimeout = 20000 @@ -43,31 +48,56 @@ describe('K8', () => { const podName = 'test-pod' const containerName = 'alpine' const podLabel = 'app=test' + const serviceName = 'test-service' beforeAll(async () => { - argv[flags.namespace.name] = testNamespace - configManager.update(argv) - await k8.createNamespace(testNamespace) - const v1Pod = new V1Pod() - const v1Metadata = new V1ObjectMeta() - v1Metadata.name = podName - v1Metadata.namespace = testNamespace - v1Metadata.labels = { app: 'test' } - v1Pod.metadata = v1Metadata - const v1Container = new V1Container() - v1Container.name = containerName - v1Container.image = 'alpine:latest' - v1Container.command = ['/bin/sh', '-c', 'sleep 7200'] - const v1Spec = new V1PodSpec() - v1Spec.containers = [v1Container] - v1Pod.spec = v1Spec - await k8.kubeClient.createNamespacedPod(testNamespace, v1Pod) + try { + argv[flags.namespace.name] = testNamespace + configManager.update(argv) + await k8.createNamespace(testNamespace) + const v1Pod = new V1Pod() + const v1Metadata = new V1ObjectMeta() + v1Metadata.name = podName + v1Metadata.namespace = testNamespace + v1Metadata.labels = { app: 'test' } + v1Pod.metadata = v1Metadata + const v1Container = new V1Container() + v1Container.name = containerName + v1Container.image = 'alpine:latest' + v1Container.command = ['/bin/sh', '-c', 'sleep 7200'] + const v1Spec = new V1PodSpec() + v1Spec.containers = [v1Container] + v1Pod.spec = v1Spec + await k8.kubeClient.createNamespacedPod(testNamespace, v1Pod) + const v1Svc = new V1Service() + const v1SvcMetadata = new V1ObjectMeta() + v1SvcMetadata.name = serviceName + v1SvcMetadata.namespace = testNamespace + v1SvcMetadata.labels = { app: 'svc-test' } + v1Svc.metadata = v1SvcMetadata + const v1SvcSpec = new V1ServiceSpec() + const v1SvcPort = new V1ServicePort() + v1SvcPort.port = 80 + v1SvcPort.targetPort = 80 + v1SvcSpec.ports = [v1SvcPort] + v1Svc.spec = v1SvcSpec + await k8.kubeClient.createNamespacedService(testNamespace, v1Svc) + } catch (e) { + console.log(e) + throw e + } }, defaultTimeout) afterAll(async () => { - await k8.deleteNamespace(testNamespace) - argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE - configManager.update(argv) + try { + await k8.kubeClient.deleteNamespacedPod(podName, testNamespace, undefined, undefined, 1) + await k8.deleteNamespace(testNamespace) + argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE + configManager.update(argv) + } catch (e) { + console.log(e) + throw e + } }, defaultTimeout) it('should be able to list clusters', async () => { @@ -92,6 +122,31 @@ describe('K8', () => { await expect(k8.deleteNamespace(name)).resolves.toBeTruthy() }, defaultTimeout) + it('should be able to run wait for pod', async () => { + const labels = [podLabel] + + const pods = await k8.waitForPods([constants.POD_PHASE_RUNNING], labels, 1) + expect(pods.length).toStrictEqual(1) + }, defaultTimeout) + + it('should be able to run wait for pod ready', async () => { + const labels = [podLabel] + + const pods = await k8.waitForPodReady(labels, 1) + expect(pods.length).toStrictEqual(1) + }, defaultTimeout) + + it('should be able to run wait for pod conditions', async () => { + const labels = [podLabel] + + const conditions = new Map() + .set(constants.POD_CONDITION_INITIALIZED, constants.POD_CONDITION_STATUS_TRUE) + .set(constants.POD_CONDITION_POD_SCHEDULED, constants.POD_CONDITION_STATUS_TRUE) + .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) + const pods = await k8.waitForPodConditions(conditions, labels, 1) + expect(pods.length).toStrictEqual(1) + }, defaultTimeout) + it('should be able to detect pod IP of a pod', async () => { const pods = await k8.getPodsByLabel([podLabel]) const podName = pods[0].metadata.name @@ -100,9 +155,7 @@ describe('K8', () => { }, defaultTimeout) it('should be able to detect cluster IP', async () => { - // TODO - const svcName = 'console' - await expect(k8.getClusterIP(svcName)).resolves.not.toBeNull() + await expect(k8.getClusterIP(serviceName)).resolves.not.toBeNull() await expect(k8.getClusterIP('INVALID')).rejects.toThrow(FullstackTestingError) }, defaultTimeout) @@ -113,30 +166,23 @@ describe('K8', () => { }, defaultTimeout) it('should be able to copy a file to and from a container', async () => { - try { - argv[flags.namespace.name] = testNamespace - configManager.update(argv) - const pods = await k8.waitForPodReady([podLabel], 1, 20) - expect(pods.length).toStrictEqual(1) - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) - const destDir = '/tmp' - const srcPath = 'test/data/pem/keys/a-private-node0.pem' - const destPath = `${destDir}/a-private-node0.pem` + const pods = await k8.waitForPodReady([podLabel], 1, 20) + expect(pods.length).toStrictEqual(1) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) + const destDir = '/tmp' + const srcPath = 'test/data/pem/keys/a-private-node0.pem' + const destPath = `${destDir}/a-private-node0.pem` - // upload the file - await expect(k8.copyTo(podName, containerName, srcPath, destDir)).resolves.toBeTruthy() + // upload the file + await expect(k8.copyTo(podName, containerName, srcPath, destDir)).resolves.toBeTruthy() - // download the same file - await expect(k8.copyFrom(podName, containerName, destPath, tmpDir)).resolves.toBeTruthy() + // download the same file + await expect(k8.copyFrom(podName, containerName, destPath, tmpDir)).resolves.toBeTruthy() - // rm file inside the container - await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves + // rm file inside the container + await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves - fs.rmdirSync(tmpDir, { recursive: true }) - } finally { - argv[flags.namespace.name] = constants.FULLSTACK_SETUP_NAMESPACE - configManager.update(argv) - } + fs.rmdirSync(tmpDir, { recursive: true }) }, defaultTimeout) it('should be able to port forward gossip port', (done) => { @@ -169,31 +215,6 @@ describe('K8', () => { // TODO enhance this test to do something with the port, this pod isn't even running, but it is still passing }, defaultTimeout) - it('should be able to run wait for pod', async () => { - const labels = [podLabel] - - const pods = await k8.waitForPods([constants.POD_PHASE_RUNNING], labels, 1) - expect(pods.length).toStrictEqual(1) - }, defaultTimeout) - - it('should be able to run wait for pod ready', async () => { - const labels = [podLabel] - - const pods = await k8.waitForPodReady(labels, 1) - expect(pods.length).toStrictEqual(1) - }, defaultTimeout) - - it('should be able to run wait for pod conditions', async () => { - const labels = [podLabel] - - const conditions = new Map() - .set(constants.POD_CONDITION_INITIALIZED, constants.POD_CONDITION_STATUS_TRUE) - .set(constants.POD_CONDITION_POD_SCHEDULED, constants.POD_CONDITION_STATUS_TRUE) - .set(constants.POD_CONDITION_READY, constants.POD_CONDITION_STATUS_TRUE) - const pods = await k8.waitForPodConditions(conditions, labels, 1) - expect(pods.length).toStrictEqual(1) - }, defaultTimeout) - it('should be able to cat a file inside the container', async () => { const pods = await k8.getPodsByLabel([podLabel]) const podName = pods[0].metadata.name diff --git a/test/e2e/core/platform_installer_e2e.test.mjs b/test/e2e/core/platform_installer_e2e.test.mjs index a4856ea56..adc502573 100644 --- a/test/e2e/core/platform_installer_e2e.test.mjs +++ b/test/e2e/core/platform_installer_e2e.test.mjs @@ -42,7 +42,7 @@ describe('PackageInstallerE2E', () => { argv[flags.fstChartVersion.name] = version.FST_CHART_VERSION // set the env variable SOLO_FST_CHARTS_DIR if developer wants to use local FST charts argv[flags.chartDirectory.name] = process.env.SOLO_FST_CHARTS_DIR ? process.env.SOLO_FST_CHARTS_DIR : undefined - const bootstrapResp = bootstrapNetwork(namespace, argv, null, null, null, null, null, null, false) + const bootstrapResp = bootstrapNetwork(namespace, argv, undefined, undefined, undefined, undefined, undefined, undefined, false) const k8 = bootstrapResp.opts.k8 const accountManager = bootstrapResp.opts.accountManager const configManager = bootstrapResp.opts.configManager From b7aea1aab3060b0b29a25485bb9d50b2e771a62a Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Aug 2024 14:59:02 +0100 Subject: [PATCH 55/56] match proto version to what sdk is using Signed-off-by: Jeromy Cannon --- package-lock.json | 20 ++++---------------- package.json | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ae117ccf..a732b4996 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "win32" ], "dependencies": { - "@hashgraph/proto": "^2.15.0", + "@hashgraph/proto": "^2.15.0-beta.3", "@hashgraph/sdk": "^2.50.0-beta.3", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", @@ -1203,9 +1203,9 @@ } }, "node_modules/@hashgraph/proto": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0.tgz", - "integrity": "sha512-ULSNIwQZIroTssrEfNoUcIcWEJ9BIwKZiAsaRvJ2+Rr3XIr+np7UXv6sEkJU+jSyzk97LrTdiRAoc/hJO9Vx8Q==", + "version": "2.15.0-beta.3", + "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0-beta.3.tgz", + "integrity": "sha512-/95cydqBQRaO1gagBOenNpcfJIcnRx+vzefhuzSFNp4pfl0AM3oXau39hM2raKLhFHoKZysSjkXkMp24AaCE5w==", "dependencies": { "long": "^4.0.0", "protobufjs": "^7.2.5" @@ -1250,18 +1250,6 @@ } } }, - "node_modules/@hashgraph/sdk/node_modules/@hashgraph/proto": { - "version": "2.15.0-beta.3", - "resolved": "https://registry.npmjs.org/@hashgraph/proto/-/proto-2.15.0-beta.3.tgz", - "integrity": "sha512-/95cydqBQRaO1gagBOenNpcfJIcnRx+vzefhuzSFNp4pfl0AM3oXau39hM2raKLhFHoKZysSjkXkMp24AaCE5w==", - "dependencies": { - "long": "^4.0.0", - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", diff --git a/package.json b/package.json index 248061402..85b71ebbe 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "author": "Swirlds Labs", "license": "Apache2.0", "dependencies": { - "@hashgraph/proto": "^2.15.0", + "@hashgraph/proto": "^2.15.0-beta.3", "@hashgraph/sdk": "^2.50.0-beta.3", "@kubernetes/client-node": "^0.21.0", "@listr2/prompt-adapter-enquirer": "^2.0.11", From fd366cd3faea206febacc47e3f31a2c72ea2d70d Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Wed, 14 Aug 2024 15:42:26 +0100 Subject: [PATCH 56/56] fix coverage analysis bug Signed-off-by: Jeromy Cannon --- .github/workflows/zxc-code-analysis.yaml | 4 ++-- src/core/account_manager.mjs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zxc-code-analysis.yaml b/.github/workflows/zxc-code-analysis.yaml index c252e651e..23877e7f6 100644 --- a/.github/workflows/zxc-code-analysis.yaml +++ b/.github/workflows/zxc-code-analysis.yaml @@ -214,14 +214,14 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-node-local-build-test-coverage-report }} + name: ${{ inputs.e2e-node-local-build-coverage-report }} path: 'coverage/${{ inputs.e2e-node-local-build-test-subdir }}' - name: Download E2E Add Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-node-add-test-coverage-report }} + name: ${{ inputs.e2e-node-add-coverage-report }} path: 'coverage/${{ inputs.e2e-node-add-test-subdir }}' - name: Publish To Codecov diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index 6b7556f7d..f464f1704 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -623,6 +623,7 @@ export class AccountManager { // ensure serviceEndpoint.ipAddressV4 value for all nodes in the addressBook is a 4 bytes array instead of string // See: https://github.com/hashgraph/hedera-protobufs/blob/main/services/basic_types.proto#L1309 + // TODO: with v0.53 will mirror node no longer need this and we can remove @hashgraph/proto? const addressBook = HashgraphProto.proto.NodeAddressBook.decode(addressBookBytes) const hasAlphaRegEx = /[a-zA-Z]+/ let modified = false