From 19c396601d4bd6178da11e4db31e276ec7c3e1c1 Mon Sep 17 00:00:00 2001 From: Jeromy Cannon Date: Mon, 26 Feb 2024 09:09:32 +0000 Subject: [PATCH] chore: Sync 0.21.1 to release 0.21 (#64) Signed-off-by: dependabot[bot] Signed-off-by: Jeromy Cannon Signed-off-by: Nathan Klick Signed-off-by: Lenin Mehedy Signed-off-by: Jeffrey Tang Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nathan Klick Co-authored-by: Lenin Mehedy Co-authored-by: JeffreyDallas <39912573+JeffreyDallas@users.noreply.github.com> Co-authored-by: Hedera Eng Automation --- .github/dependabot.yml | 1 + .../flow-deploy-release-artifact.yaml | 6 +- .github/workflows/zxc-compile-code.yaml | 9 ++ README.md | 56 ++++++++----- jest.config.mjs | 3 + package-lock.json | 23 ++++-- package.json | 13 ++- src/commands/account.mjs | 22 +---- src/commands/network.mjs | 4 +- src/commands/node.mjs | 82 ++++++++++++++++++- src/core/account_manager.mjs | 73 ++++++++++++++--- src/core/chart_manager.mjs | 2 +- src/core/constants.mjs | 2 +- src/core/k8.mjs | 57 +++++++++++++ test/e2e/commands/01_node.test.mjs | 4 + test/jest/fail_fast.mjs | 61 ++++++++++++++ 16 files changed, 348 insertions(+), 70 deletions(-) create mode 100644 test/jest/fail_fast.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 907768bb1..46502a759 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,4 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 15 + versioning-strategy: increase diff --git a/.github/workflows/flow-deploy-release-artifact.yaml b/.github/workflows/flow-deploy-release-artifact.yaml index 28d633ed0..5844b1c70 100644 --- a/.github/workflows/flow-deploy-release-artifact.yaml +++ b/.github/workflows/flow-deploy-release-artifact.yaml @@ -50,7 +50,7 @@ jobs: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 @@ -111,12 +111,12 @@ jobs: git_tag_gpgsign: false - name: Setup Node - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 20 - name: Setup JFrog CLI - uses: jfrog/setup-jfrog-cli@8fc3d0018a8721b9a797cd7fd97c8e4833f5a3d1 # v3.5.3 + uses: jfrog/setup-jfrog-cli@26da2259ee7690e63b5410d7451b2938d08ce1f9 # v4.0.0 env: JF_URL: ${{ vars.JF_URL }} JF_ACCESS_TOKEN: ${{ secrets.JF_ACCESS_TOKEN }} diff --git a/.github/workflows/zxc-compile-code.yaml b/.github/workflows/zxc-compile-code.yaml index f9d313b78..02013808a 100644 --- a/.github/workflows/zxc-compile-code.yaml +++ b/.github/workflows/zxc-compile-code.yaml @@ -152,6 +152,15 @@ jobs: if: ${{ inputs.enable-e2e-tests && !cancelled() && !failure() }} run: npm run test-e2e + - name: Upload E2E Logs to GitHub + if: ${{ inputs.enable-e2e-tests && !cancelled() }} + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: solo.log + path: ~/.solo/logs/solo.log + overwrite: true + if-no-files-found: error + - name: Publish E2E Test Report uses: EnricoMi/publish-unit-test-result-action@8885e273a4343cd7b48eaa72428dea0c3067ea98 # v2.14.0 if: ${{ inputs.enable-e2e-tests && steps.npm-deps.conclusion == 'success' && !cancelled() }} diff --git a/README.md b/README.md index bd1de9cb1..c4606914e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Solo -An opinionated CLI tool to deploy and manage private Hedera Networks. +[![NPM Version](https://img.shields.io/npm/v/%40hashgraph%2Fsolo?logo=npm)](https://www.npmjs.com/package/@hashgraph/solo) +[![GitHub License](https://img.shields.io/github/license/hashgraph/solo?logo=apache\&logoColor=red)](LICENSE) +![node-lts](https://img.shields.io/node/v-lts/%40hashgraph%2Fsolo) +[![Build Application](https://github.com/hashgraph/solo/actions/workflows/flow-build-application.yaml/badge.svg)](https://github.com/hashgraph/solo/actions/workflows/flow-build-application.yaml) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/83a423a3a1c942459127b3aec62ab0b5)](https://app.codacy.com/gh/hashgraph/solo/dashboard?utm_source=gh\&utm_medium=referral\&utm_content=\&utm_campaign=Badge_grade) +[![codecov](https://codecov.io/gh/hashgraph/solo/graph/badge.svg?token=hBkQdB1XO5)](https://codecov.io/gh/hashgraph/solo) + +An opinionated CLI tool to deploy and manage standalone test networks. ## Table of Contents @@ -12,8 +19,8 @@ An opinionated CLI tool to deploy and manage private Hedera Networks. * [Legacy keys (.pfx file)](#legacy-keys-pfx-file) * [Standard keys (.pem file)](#standard-keys-pem-file) * [Examples](#examples) - * [Example - 1: Deploy a private Hedera network (version `0.42.5`)](#example---1-deploy-a-private-hedera-network-version-0425) - * [Example - 2: Deploy a private Hedera network (version `0.47.0-alpha.0`)](#example---2-deploy-a-private-hedera-network-version-0470-alpha0) + * [Example - 1: Deploy a standalone test network (version `0.42.5`)](#example---1-deploy-a-standalone-test-network-version-0425) + * [Example - 2: Deploy a standalone test network (version `0.47.0-alpha.0`)](#example---2-deploy-a-standalone-test-network-version-0470-alpha0) * [Support](#support) * [Contributing](#contributing) * [Code of Conduct](#code-of-conduct) @@ -30,8 +37,8 @@ An opinionated CLI tool to deploy and manage private Hedera Networks. * Install [Node](https://nodejs.org/en/download). You may also use [nvm](https://github.com/nvm-sh/nvm) to manage different Node versions locally: ``` -$ nvm install lts/hydrogen -$ nvm use lts/hydrogen +nvm install lts/hydrogen +nvm use lts/hydrogen ``` * Install [kubectl](https://kubernetes.io/docs/tasks/tools/) @@ -51,20 +58,28 @@ $ nvm use lts/hydrogen Check and select appropriate kubernetes context using `kubectx` command as below: ``` -$ kubectx +kubectx ``` * For a local cluster, you may use [kind](https://kind.sigs.k8s.io/) and [kubectl](https://kubernetes.io/docs/tasks/tools/) to create a cluster and namespace as below. * In this case, ensure your Docker engine has enough resources (e.g. Memory >=8Gb, CPU: >=4). +First, use the following command to set up the environment variables: +``` +export SOLO_CLUSTER_NAME=solo +export SOLO_NAMESPACE=solo +export SOLO_CLUSTER_SETUP_NAMESPACE=solo-cluster +``` + +Then run the following command to set the kubectl context to the new cluster: +``` +kind create cluster -n "${SOLO_CLUSTER_NAME}" +kubectl create ns "${SOLO_NAMESPACE}" +kubectl create ns "${SOLO_CLUSTER_SETUP_NAMESPACE}" ``` -$ export SOLO_CLUSTER_NAME=solo -$ export SOLO_NAMESPACE=solo -$ export SOLO_CLUSTER_SETUP_NAMESPACE=solo-cluster -$ kind create cluster -n "${SOLO_CLUSTER_NAME}" -$ kubectl create ns "${SOLO_NAMESPACE}" -$ kubectl create ns "${SOLO_CLUSTER_SETUP_NAMESPACE}" +and the command output should look like this: +``` Creating cluster "solo" ... ✓ Ensuring node image (kindest/node:v1.27.3) 🖼 ✓ Preparing nodes 📦 @@ -117,10 +132,10 @@ cache directory (`$HOME/.solo/cache/keys`): ``` # Option - 1: Generate keys for default node IDs: node0,node1,node2 -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/hashgraph/full-stack-testing/main/solo/test/scripts/gen-legacy-keys.sh)" +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/hashgraph/solo/main/test/scripts/gen-legacy-keys.sh)" # Option - 2: Generate keys for custom node IDs -curl https://raw.githubusercontent.com/hashgraph/full-stack-testing/main/solo/test/scripts/gen-legacy-keys.sh -o gen-legacy-keys.sh +curl https://raw.githubusercontent.com/hashgraph/solo/main/test/scripts/gen-legacy-keys.sh -o gen-legacy-keys.sh chmod +x gen-legacy-keys.sh ./gen-legacy-keys.sh alice,bob,carol ``` @@ -132,7 +147,7 @@ You may run `solo node keys --gossip-keys --tls-keys --key-format pem -i node0,n ## Examples -### Example - 1: Deploy a private Hedera network (version `0.42.5`) +### Example - 1: Deploy a standalone test network (version `0.42.5`) * Initialize `solo` with tag `v0.42.5` and list of node names `node0,node1,node2`: @@ -155,12 +170,13 @@ Kubernetes Namespace : solo * Generate `pfx` node keys (You will need `curl`, `keytool` and `openssl`) ``` -$ curl https://raw.githubusercontent.com/hashgraph/full-stack-testing/main/solo/test/scripts/gen-legacy-keys.sh -o gen-legacy-keys.sh -$ chmod +x gen-legacy-keys.sh -$ ./gen-legacy-keys.sh node0,node1,node2 +curl https://raw.githubusercontent.com/hashgraph/solo/main/test/scripts/gen-legacy-keys.sh -o gen-legacy-keys.sh +chmod +x gen-legacy-keys.sh +./gen-legacy-keys.sh node0,node1,node2 # view the list of generated keys in the cache folder -$ ls ~/.solo/cache/keys + +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 @@ -362,7 +378,7 @@ Once the nodes are up, you may now expose various services (using `k9s` (shift-f │ │ ``` -### Example - 2: Deploy a private Hedera network (version `0.47.0-alpha.0`) +### Example - 2: Deploy a standalone test network (version `0.47.0-alpha.0`) * Initialize `solo` with tag `v0.47.0-alpha.0` and list of node names `n0,n1,n2`: diff --git a/jest.config.mjs b/jest.config.mjs index dafb4b20f..c60177e4a 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -15,6 +15,9 @@ * */ const config = { + rootDir: '.', + testRunner: 'jest-circus/runner', + testEnvironment: '/test/jest/fail_fast.mjs', testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(mjs?)$', moduleFileExtensions: ['js', 'mjs'], verbose: true, diff --git a/package-lock.json b/package-lock.json index a8e7a8792..b67359602 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,30 +1,32 @@ { "name": "@hashgraph/solo", - "version": "0.21.0", + "version": "0.21.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@hashgraph/solo", - "version": "0.21.0", + "version": "0.21.1", "license": "Apache2.0", "os": [ "darwin", "linux" ], "dependencies": { + "@hashgraph/proto": "^2.14.0-beta.3", "@hashgraph/sdk": "^2.41.0", "@kubernetes/client-node": "^0.20.0", "@listr2/prompt-adapter-enquirer": "^2.0.2", "@peculiar/x509": "^1.9.7", "adm-zip": "^0.5.10", "chalk": "^5.3.0", - "dotenv": "^16.4.4", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", "esm": "^3.2.25", "figlet": "^1.7.0", "got": "^14.2.0", "inquirer": "^9.2.15", + "js-base64": "^3.7.6", "listr2": "^8.0.2", "tar": "^6.2.0", "uuid": "^9.0.1", @@ -37,6 +39,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@jest/test-sequencer": "^29.7.0", "eslint": "^8.56.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-headers": "^1.1.2", @@ -44,6 +47,8 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "jest": "^29.7.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-junit": "^16.0.0", "remark-cli": "^12.0.0", "remark-lint-list-item-indent": "^3.1.2", @@ -4007,9 +4012,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz", - "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, @@ -7126,9 +7131,9 @@ } }, "node_modules/js-base64": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.5.tgz", - "integrity": "sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==" + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.6.tgz", + "integrity": "sha512-NPrWuHFxFUknr1KqJRDgUQPexQF0uIJWjeT+2KjEePhitQxQEx5EJBG1lVn5/hc8aLycTpXrDOgPQ6Zq+EDiTA==" }, "node_modules/js-sha3": { "version": "0.8.0", diff --git a/package.json b/package.json index 6acbf0b06..33bea855f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hashgraph/solo", - "version": "0.21.0", + "version": "0.21.1", "description": "An opinionated CLI tool to deploy and manage private Hedera Networks.", "main": "src/index.mjs", "type": "module", @@ -25,18 +25,20 @@ "author": "Swirlds Labs", "license": "Apache2.0", "dependencies": { + "@hashgraph/proto": "^2.14.0-beta.3", "@hashgraph/sdk": "^2.41.0", "@kubernetes/client-node": "^0.20.0", "@listr2/prompt-adapter-enquirer": "^2.0.2", "@peculiar/x509": "^1.9.7", "adm-zip": "^0.5.10", "chalk": "^5.3.0", - "dotenv": "^16.4.4", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", "esm": "^3.2.25", "figlet": "^1.7.0", "got": "^14.2.0", "inquirer": "^9.2.15", + "js-base64": "^3.7.6", "listr2": "^8.0.2", "tar": "^6.2.0", "uuid": "^9.0.1", @@ -46,6 +48,7 @@ }, "devDependencies": { "@jest/globals": "^29.7.0", + "@jest/test-sequencer": "^29.7.0", "eslint": "^8.56.0", "eslint-config-standard": "^17.1.0", "eslint-plugin-headers": "^1.1.2", @@ -53,12 +56,14 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "jest": "^29.7.0", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", "jest-junit": "^16.0.0", "remark-cli": "^12.0.0", "remark-lint-list-item-indent": "^3.1.2", + "remark-lint-unordered-list-marker-style": "^3.1.2", "remark-preset-lint-consistent": "^5.1.2", - "remark-preset-lint-recommended": "^6.1.3", - "remark-lint-unordered-list-marker-style": "^3.1.2" + "remark-preset-lint-recommended": "^6.1.3" }, "repository": { "type": "git", diff --git a/src/commands/account.mjs b/src/commands/account.mjs index b594dde50..bf18b4170 100644 --- a/src/commands/account.mjs +++ b/src/commands/account.mjs @@ -73,23 +73,10 @@ export class AccountCommand extends BaseCommand { const serviceMap = await this.accountManager.getNodeServiceMap(ctx.config.namespace) ctx.nodeClient = await this.accountManager.getNodeClient(ctx.config.namespace, - serviceMap, ctx.treasuryAccountId, ctx.treasuryPrivateKey) + serviceMap, ctx.treasuryAccountInfo.accountId, ctx.treasuryAccountInfo.privateKey) this.nodeClient = ctx.nodeClient // store in class so that we can make sure connections are closed } - async loadTreasuryAccount (ctx) { - ctx.treasuryAccountId = constants.TREASURY_ACCOUNT_ID - // check to see if the treasure account is in the secrets - const accountInfo = await this.accountManager.getAccountKeysFromSecret(ctx.treasuryAccountId, ctx.config.namespace) - - // if it isn't in the secrets we can load genesis key - if (accountInfo) { - ctx.treasuryPrivateKey = accountInfo.privateKey - } else { - ctx.treasuryPrivateKey = constants.GENESIS_KEY - } - } - async getAccountInfo (ctx) { return this.accountManager.accountInfoQuery(ctx.config.accountId, ctx.nodeClient) } @@ -101,7 +88,6 @@ export class AccountCommand extends BaseCommand { this.logger.error(`failed to update account keys for accountId ${ctx.accountInfo.accountId}`) return false } - this.logger.debug(`sent account key update for account ${ctx.accountInfo.accountId}`) } else { amount = amount || flags.amount.definition.defaultValue } @@ -156,7 +142,7 @@ export class AccountCommand extends BaseCommand { self.logger.debug('Initialized config', { config }) - await self.loadTreasuryAccount(ctx) + ctx.treasuryAccountInfo = await self.accountManager.getTreasuryAccountKeys(ctx.config.namespace) await self.loadNodeClient(ctx) } }, @@ -219,7 +205,7 @@ export class AccountCommand extends BaseCommand { { title: 'get the account info', task: async (ctx, task) => { - await self.loadTreasuryAccount(ctx) + ctx.treasuryAccountInfo = await self.accountManager.getTreasuryAccountKeys(ctx.config.namespace) await self.loadNodeClient(ctx) ctx.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, ctx.config.privateKey) } @@ -286,7 +272,7 @@ export class AccountCommand extends BaseCommand { { title: 'get the account info', task: async (ctx, task) => { - await self.loadTreasuryAccount(ctx) + ctx.treasuryAccountInfo = await self.accountManager.getTreasuryAccountKeys(ctx.config.namespace) await self.loadNodeClient(ctx) self.accountInfo = await self.buildAccountInfo(await self.getAccountInfo(ctx), ctx.config.namespace, false) this.logger.showJSON('account info', self.accountInfo) diff --git a/src/commands/network.mjs b/src/commands/network.mjs index 0f6cfbce1..5d0da17c9 100644 --- a/src/commands/network.mjs +++ b/src/commands/network.mjs @@ -76,7 +76,9 @@ export class NetworkCommand extends BaseCommand { valuesArg += this.prepareValuesFiles(config.valuesFile) - valuesArg += ` --set hedera-mirror-node.enabled=${config.deployMirrorNode} --set hedera-explorer.enabled=${config.deployHederaExplorer}` + // do not deploy mirror node until after we have the updated address book + valuesArg += ' --set hedera-mirror-node.enabled=false --set hedera-explorer.enabled=false' + valuesArg += ` --set telemetry.prometheus.svcMonitor.enabled=${config.enablePrometheusSvcMonitor}` if (config.enableHederaExplorerTls) { diff --git a/src/commands/node.mjs b/src/commands/node.mjs index 19b553bff..91b43ecb8 100644 --- a/src/commands/node.mjs +++ b/src/commands/node.mjs @@ -419,16 +419,29 @@ export class NodeCommand extends BaseCommand { self.configManager.update(argv) await prompts.execute(task, self.configManager, [ flags.namespace, + flags.chartDirectory, + flags.fstChartVersion, flags.nodeIDs, + flags.deployHederaExplorer, + flags.deployMirrorNode, flags.updateAccountKeys ]) ctx.config = { namespace: self.configManager.getFlag(flags.namespace), + chartDir: this.configManager.getFlag(flags.chartDirectory), + fstChartVersion: this.configManager.getFlag(flags.fstChartVersion), nodeIds: helpers.parseNodeIDs(self.configManager.getFlag(flags.nodeIDs)), + deployMirrorNode: this.configManager.getFlag(flags.deployMirrorNode), + deployHederaExplorer: this.configManager.getFlag(flags.deployHederaExplorer), updateAccountKeys: self.configManager.getFlag(flags.updateAccountKeys) } + ctx.config.chartPath = await this.prepareChartPath(ctx.config.chartDir, + constants.FULLSTACK_TESTING_CHART, constants.FULLSTACK_DEPLOYMENT_CHART) + + ctx.config.valuesArg = ` --set hedera-mirror-node.enabled=${ctx.config.deployMirrorNode} --set hedera-explorer.enabled=${ctx.config.deployHederaExplorer}` + if (!await this.k8.hasNamespace(ctx.config.namespace)) { throw new FullstackTestingError(`namespace ${ctx.config.namespace} does not exist`) } @@ -483,6 +496,36 @@ export class NodeCommand extends BaseCommand { }) } }, + { + title: 'Enable mirror node', + task: async (ctx, parentTask) => { + const subTasks = [ + { + title: 'Get the mirror node importer address book', + task: async (ctx, _) => { + ctx.addressBook = await self.getAddressBook(ctx.config.namespace) + ctx.config.valuesArg += ` --set "hedera-mirror-node.importer.addressBook=${ctx.addressBook}"` + } + }, + { + title: `Upgrade chart '${constants.FULLSTACK_DEPLOYMENT_CHART}'`, + task: async (ctx, _) => { + await this.chartManager.upgrade( + ctx.config.namespace, + constants.FULLSTACK_DEPLOYMENT_CHART, + ctx.config.chartPath, + ctx.config.valuesArg + ) + } + } + ] + + return parentTask.newListr(subTasks, { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }) + } + }, { title: 'Update special account keys', task: async (ctx, task) => { @@ -493,8 +536,17 @@ export class NodeCommand extends BaseCommand { 'skipping special account keys update, special accounts will retain genesis private keys')) } } - } - ], { + }, + { + title: 'Waiting for explorer pod to be ready', + task: async (ctx, _) => { + if (ctx.config.deployHederaExplorer) { + await this.k8.waitForPod(constants.POD_STATUS_RUNNING, [ + 'app.kubernetes.io/component=hedera-explorer', 'app.kubernetes.io/name=hedera-explorer' + ], 1) + } + } + }], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION }) @@ -508,6 +560,32 @@ export class NodeCommand extends BaseCommand { return true } + /** + * Will get the address book from the network (base64 encoded) + * @param namespace + * @returns {Promise} the base64 encoded address book for the network + */ + async getAddressBook (namespace) { + const treasuryAccountInfo = await this.accountManager.getTreasuryAccountKeys(namespace) + const serviceMap = await this.accountManager.getNodeServiceMap(namespace) + + const nodeClient = await this.accountManager.getNodeClient(namespace, + serviceMap, treasuryAccountInfo.accountId, treasuryAccountInfo.privateKey) + + try { + // Retrieve the AddressBook as base64 + return await this.accountManager.prepareAddressBookBase64(nodeClient) + } catch (e) { + throw new FullstackTestingError('an error was encountered while trying to prepare the address book') + } finally { + await this.accountManager.stopPortForwards() + if (nodeClient) { + nodeClient.close() + } + await sleep(5) // sleep a few ticks to allow network connections to close + } + } + async stop (argv) { const self = this diff --git a/src/core/account_manager.mjs b/src/core/account_manager.mjs index 7419cdcc9..0d88f9bd5 100644 --- a/src/core/account_manager.mjs +++ b/src/core/account_manager.mjs @@ -14,13 +14,15 @@ * limitations under the License. * */ +import * as HashgraphProto from '@hashgraph/proto' +import * as Base64 from 'js-base64' import * as constants from './constants.mjs' import { AccountCreateTransaction, AccountId, AccountInfoQuery, AccountUpdateTransaction, - Client, + Client, FileContentsQuery, FileId, Hbar, HbarUnit, KeyList, @@ -90,6 +92,29 @@ export class AccountManager { } } + /** + * Gets the treasury account private key from Kubernetes secret if it exists, else + * returns the Genesis private key, then will return an AccountInfo object with the + * accountId, privateKey, publicKey + * @param namespace the namespace that the secret is in + * @returns {Promise<{accountId: string, privateKey: string, publicKey: string}>} + */ + 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 + } + /** * Prepares the accounts with updated keys so that they do not contain the default genesis keys * @param namespace the namespace to run the update of account keys for @@ -271,7 +296,6 @@ export class AccountManager { let keys try { keys = await this.getAccountKeys(accountId, nodeClient) - this.logger.debug(`retrieved keys for account ${accountId.toString()}`) } catch (e) { this.logger.error(`failed to get keys for accountId ${accountId.toString()}, e: ${e.toString()}\n ${e.stack}`) return { @@ -309,7 +333,6 @@ export class AccountManager { value: accountId.toString() } } - this.logger.debug(`created k8s secret for account ${accountId.toString()}`) } catch (e) { this.logger.error(`failed to create secret for accountId ${accountId.toString()}, e: ${e.toString()}`) return { @@ -328,7 +351,6 @@ export class AccountManager { value: accountId.toString() } } - this.logger.debug(`sent account key update for account ${accountId.toString()}`) } catch (e) { this.logger.error(`failed to update account keys for accountId ${accountId.toString()}, e: ${e.toString()}`) return { @@ -385,9 +407,6 @@ export class AccountManager { * @returns {Promise} whether the update was successful */ async sendAccountKeyUpdate (accountId, newPrivateKey, nodeClient, oldPrivateKey) { - this.logger.debug( - `Updating account ${accountId.toString()} with new public and private keys`) - if (typeof newPrivateKey === 'string') { newPrivateKey = PrivateKey.fromStringED25519(newPrivateKey) } @@ -412,9 +431,6 @@ export class AccountManager { // Request the receipt of the transaction const receipt = await txResponse.getReceipt(nodeClient) - this.logger.debug( - `The transaction consensus status for update of accountId ${accountId.toString()} is ${receipt.status}`) - return receipt.status === Status.Success } @@ -483,7 +499,6 @@ export class AccountManager { throw new FullstackTestingError(`failed to create secret for accountId ${accountInfo.accountId.toString()}, keys were sent to log file`) } - this.logger.debug(`created k8s secret for account ${accountInfo.accountId}`) return accountInfo } @@ -515,4 +530,40 @@ export class AccountManager { throw new FullstackTestingError(errorMessage, e) } } + + /** + * Fetch and prepare address book as a base64 string + * @param nodeClient node client + * @return {Promise} + */ + async prepareAddressBookBase64 (nodeClient) { + // fetch AddressBook + const fileQuery = new FileContentsQuery().setFileId(FileId.ADDRESS_BOOK) + let addressBookBytes = await fileQuery.execute(nodeClient) + + // 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 + const addressBook = HashgraphProto.proto.NodeAddressBook.decode(addressBookBytes) + let modified = false + for (const nodeAddress of addressBook.nodeAddress) { + // overwrite ipAddressV4 as 4 bytes array if required + if (nodeAddress.serviceEndpoint[0].ipAddressV4.byteLength !== 4) { + const ipAddress = nodeAddress.serviceEndpoint[0].ipAddressV4.toString() + const parts = ipAddress.split('.') + if (parts.length !== 4) { + throw new FullstackTestingError(`expected node IP address to have 4 parts, found ${parts.length}: ${ipAddress}`) + } + + nodeAddress.serviceEndpoint[0].ipAddressV4 = Uint8Array.from(parts) + modified = true + } + } + + if (modified) { + addressBookBytes = HashgraphProto.proto.NodeAddressBook.encode(addressBook).finish() + } + + // convert addressBook into base64 + return Base64.encode(addressBookBytes) + } } diff --git a/src/core/chart_manager.mjs b/src/core/chart_manager.mjs index 570b7f916..d16096f62 100644 --- a/src/core/chart_manager.mjs +++ b/src/core/chart_manager.mjs @@ -129,7 +129,7 @@ export class ChartManager { async upgrade (namespaceName, chartName, chartPath, valuesArg = '') { try { this.logger.showUser(chalk.cyan('> upgrading chart:'), chalk.yellow(`${chartName}`)) - await this.helm.upgrade(`-n ${namespaceName} ${chartName} ${chartPath} ${valuesArg}`) + await this.helm.upgrade(`-n ${namespaceName} ${chartName} ${chartPath} --reuse-values ${valuesArg}`) this.logger.showUser(chalk.green('OK'), `chart '${chartName}' is upgraded`) } catch (e) { throw new FullstackTestingError(`failed to upgrade chart ${chartName}: ${e.message}`, e) diff --git a/src/core/constants.mjs b/src/core/constants.mjs index 2213836f3..00de7f220 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -24,7 +24,7 @@ import chalk from 'chalk' export const CUR_FILE_DIR = dirname(fileURLToPath(import.meta.url)) export const USER = `${process.env.USER}` export const USER_SANITIZED = USER.replace(/[\W_]+/g, '-') -export const SOLO_HOME_DIR = `${process.env.HOME}/.solo` +export const SOLO_HOME_DIR = process.env.SOLO_HOME || `${process.env.HOME}/.solo` export const SOLO_LOGS_DIR = `${SOLO_HOME_DIR}/logs` export const SOLO_CACHE_DIR = `${SOLO_HOME_DIR}/cache` export const DEFAULT_NAMESPACE = 'default' diff --git a/src/core/k8.mjs b/src/core/k8.mjs index 385b94f9b..74023666e 100644 --- a/src/core/k8.mjs +++ b/src/core/k8.mjs @@ -195,6 +195,63 @@ export class K8 { return this.filterItem(resp.body.items, { name }) } + /** + * Get pods by labels + * @param labels list of labels + * @return {Promise>} + */ + async getPodsByLabel (labels = []) { + const ns = this._getNamespace() + const labelSelector = labels.join(',') + const result = await this.kubeClient.listNamespacedPod( + ns, + undefined, + undefined, + undefined, + undefined, + labelSelector + ) + + return result.body.items + } + + /** + * Get secrets by labels + * @param labels list of labels + * @return {Promise>} + */ + async getSecretsByLabel (labels = []) { + const ns = this._getNamespace() + const labelSelector = labels.join(',') + const result = await this.kubeClient.listNamespacedSecret( + ns, + undefined, + undefined, + undefined, + undefined, + labelSelector + ) + + return result.body.items + } + + /** + * Updates a kubernetes secrets + * @param secretObject + * @return {Promise} + */ + async updateSecret (secretObject) { + const ns = this._getNamespace() + try { + // patch is broke, need to use delete/create (workaround/fix in 1.0.0-rc4): https://github.com/kubernetes-client/javascript/issues/893 + // await k8.kubeClient.patchNamespacedSecret(secret.name, ctx.config.namespace, secret.data) + await this.kubeClient.deleteNamespacedSecret(secretObject.metadata.name, ns) + await this.kubeClient.createNamespacedSecret(ns, secretObject) + } catch (e) { + throw new FullstackTestingError(`failed to update secret ${secretObject.metadata.name}: ${e.message}`, e) + } + } + /** * Get host IP of a podName * @param podNameName name of the podName diff --git a/test/e2e/commands/01_node.test.mjs b/test/e2e/commands/01_node.test.mjs index 84e0f6e5b..e2a25793e 100644 --- a/test/e2e/commands/01_node.test.mjs +++ b/test/e2e/commands/01_node.test.mjs @@ -94,6 +94,7 @@ describe.each([ argv[flags.keyFormat.name] = testKeyFormat argv[flags.nodeIDs.name] = 'node0,node1,node2' argv[flags.cacheDir.name] = cacheDir + argv[flags.chartDirectory.name] = './charts' argv[flags.force.name] = false argv[flags.chainId.name] = constants.HEDERA_CHAIN_ID argv[flags.generateGossipKeys.name] = false @@ -107,6 +108,9 @@ describe.each([ argv[flags.clusterName.name] = 'kind-solo-e2e' argv[flags.clusterSetupNamespace.name] = 'solo-e2e-cluster' argv[flags.updateAccountKeys.name] = true + argv[flags.fstChartVersion.name] = flags.fstChartVersion.definition.defaultValue + argv[flags.deployHederaExplorer.name] = true + argv[flags.deployMirrorNode.name] = true configManager.update(argv) const nodeIds = argv[flags.nodeIDs.name].split(',') diff --git a/test/jest/fail_fast.mjs b/test/jest/fail_fast.mjs new file mode 100644 index 000000000..8cea3d783 --- /dev/null +++ b/test/jest/fail_fast.mjs @@ -0,0 +1,61 @@ +/** + * 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 { TestEnvironment } from 'jest-environment-node' + +/** + * Custom Jest Environment where a failed test would ensure later tests are skipped + * + * This code is customized based on the following sources: + * * - https://github.com/jestjs/jest/issues/6527#issuecomment-734917527 + * * - https://stackoverflow.com/questions/51250006/jest-stop-test-suite-after-first-fail + */ +export default class JestEnvironmentFailFast extends TestEnvironment { + failedMap = new Map() + + async handleTestEvent (event, state) { + switch (event.name) { + case 'hook_failure': { + // hook errors are not displayed if tests are skipped, so display them manually + event.hook.parent.name = `[${event.hook.type} - ERROR]: ${event.hook.parent.name}` + this.failedMap.set(event.hook.parent.name, true) + break + } + + case 'test_fn_failure': { + this.failedMap.set(event.test.parent.name, true) + break + } + + case 'test_start': { + if (this.failedMap.has(event.test.parent.name)) { + event.test.mode = 'todo' + } + break + } + + case 'test_skip': { + event.test.name = `SKIPPED: ${event.test.name}` + break + } + } + + if (super.handleTestEvent) { + super.handleTestEvent(event, state) + } + } +}