From 44c31321719e05d04f11a7da2771d661e6a19ef8 Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Fri, 25 Oct 2024 18:03:25 +0300 Subject: [PATCH] feat: add separate commands for update (#735) Signed-off-by: Ivo Yankov Signed-off-by: Jeromy Cannon Co-authored-by: Jeromy Cannon --- .github/workflows/flow-build-application.yaml | 1 + .../workflows/flow-pull-request-checks.yaml | 1 + .github/workflows/templates/config.yaml | 13 +- .github/workflows/zxc-code-analysis.yaml | 17 ++ .github/workflows/zxc-env-vars.yaml | 8 + package.json | 1 + src/commands/flags.ts | 4 +- src/commands/node/configs.ts | 4 +- src/commands/node/flags.ts | 54 +++++- src/commands/node/handlers.ts | 125 ++++++++++---- src/commands/node/index.ts | 21 +++ src/commands/node/tasks.ts | 2 +- src/core/helpers.ts | 52 +++++- test/e2e/commands/node_update.test.ts | 4 +- .../e2e/commands/separate_node_update.test.ts | 154 ++++++++++++++++++ 15 files changed, 409 insertions(+), 52 deletions(-) create mode 100644 test/e2e/commands/separate_node_update.test.ts diff --git a/.github/workflows/flow-build-application.yaml b/.github/workflows/flow-build-application.yaml index e549fba0b..379c6a6fc 100644 --- a/.github/workflows/flow-build-application.yaml +++ b/.github/workflows/flow-build-application.yaml @@ -86,6 +86,7 @@ jobs: - { name: "Node Add Local", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-add-local-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-add-local-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-add-local-coverage-report }}" } - { name: "Node Add - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-add-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-add-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-add-separate-commands-coverage-report }}" } - { name: "Node Update", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-update-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-update-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-update-coverage-report }}" } + - { name: "Node Update - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-update-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-update-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-update-separate-commands-coverage-report }}" } - { name: "Node Delete", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-delete-coverage-report }}" } - { name: "Node Delete - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-coverage-report }}" } - { name: "Node Upgrade", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-upgrade-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-upgrade-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-upgrade-coverage-report }}" } diff --git a/.github/workflows/flow-pull-request-checks.yaml b/.github/workflows/flow-pull-request-checks.yaml index 79701f798..c6e5ab4d3 100644 --- a/.github/workflows/flow-pull-request-checks.yaml +++ b/.github/workflows/flow-pull-request-checks.yaml @@ -85,6 +85,7 @@ jobs: - { name: "Node Add Local", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-add-local-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-add-local-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-add-local-coverage-report }}" } - { name: "Node Add - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-add-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-add-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-add-separate-commands-coverage-report }}" } - { name: "Node Update", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-update-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-update-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-update-coverage-report }}" } + - { name: "Node Update - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-update-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-update-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-update-separate-commands-coverage-report }}" } - { name: "Node Delete", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-delete-coverage-report }}" } - { name: "Node Delete - Separate commands", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-delete-separate-commands-coverage-report }}" } - { name: "Node Upgrade", npm-test-script: "test-${{ needs.env-vars.outputs.e2e-node-upgrade-test-subdir }}", coverage-subdirectory: "${{ needs.env-vars.outputs.e2e-node-upgrade-test-subdir }}", coverage-report-name: "${{ needs.env-vars.outputs.e2e-node-upgrade-coverage-report }}" } diff --git a/.github/workflows/templates/config.yaml b/.github/workflows/templates/config.yaml index aad9ccab6..ea6f0e369 100644 --- a/.github/workflows/templates/config.yaml +++ b/.github/workflows/templates/config.yaml @@ -32,19 +32,22 @@ tests: mochaPostfix: "'test/e2e/commands/node_add_local.test.ts'" - name: Node Add - Separate commands - mochaPostfix: "'test/e2e/commands/separate_node_add*.test.ts'" + mochaPostfix: "'test/e2e/commands/separate_node_add.test.ts'" - name: Node Update - mochaPostfix: "'test/e2e/commands/node_update*.test.ts'" + mochaPostfix: "'test/e2e/commands/node_update.test.ts'" + + - name: Node Update - Separate commands + mochaPostfix: "'test/e2e/commands/separate_node_update.test.ts'" - name: Node Delete - mochaPostfix: "'test/e2e/commands/node_delete*.test.ts'" + mochaPostfix: "'test/e2e/commands/node_delete.test.ts'" - name: Node Delete - Separate commands - mochaPostfix: "'test/e2e/commands/separate_node_delete*.test.ts'" + mochaPostfix: "'test/e2e/commands/separate_node_delete.test.ts'" - name: Node Upgrade - mochaPostfix: "'test/e2e/commands/node_upgrade*.test.ts'" + mochaPostfix: "'test/e2e/commands/node_upgrade.test.ts'" - name: Relay mochaPostfix: "'test/e2e/commands/relay.test.ts'" diff --git a/.github/workflows/zxc-code-analysis.yaml b/.github/workflows/zxc-code-analysis.yaml index a88f18b51..f80c02326 100644 --- a/.github/workflows/zxc-code-analysis.yaml +++ b/.github/workflows/zxc-code-analysis.yaml @@ -110,6 +110,11 @@ on: type: string required: false default: "e2e-node-update" + e2e-node-update-separate-commands-test-subdir: + description: "E2E Node Update - Separate commands Test Subdirectory:" + type: string + required: false + default: "e2e-node-update-separate-commands" e2e-node-delete-test-subdir: description: "E2E Node Delete Test Subdirectory:" type: string @@ -185,6 +190,11 @@ on: type: string required: false default: "E2E Node Update Tests Coverage Report" + e2e-node-update-separate-commands-coverage-report: + description: "E2E Node Update - Separate commands Coverage Report:" + type: string + required: false + default: "E2E Node Update - Separate commands Tests Coverage Report" e2e-node-delete-coverage-report: description: "E2E Node Delete Coverage Report:" type: string @@ -337,6 +347,13 @@ jobs: name: ${{ inputs.e2e-node-update-coverage-report }} path: 'coverage/${{ inputs.e2e-node-update-test-subdir }}' + - name: Download E2E Node Update - Separate commands 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-update-separate-commands-coverage-report }} + path: 'coverage/${{ inputs.e2e-node-update-separate-commands-test-subdir }}' + - name: Download E2E Node Delete 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() }} diff --git a/.github/workflows/zxc-env-vars.yaml b/.github/workflows/zxc-env-vars.yaml index 786590a46..52027b95b 100644 --- a/.github/workflows/zxc-env-vars.yaml +++ b/.github/workflows/zxc-env-vars.yaml @@ -59,6 +59,9 @@ on: e2e-node-update-test-subdir: description: "E2E Node Update Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_node_update_test_subdir }} + e2e-node-update-separate-commands-test-subdir: + description: "E2E Node Update - Separate commands Test Subdirectory" + value: ${{ jobs.env-vars.outputs.e2e_node_update_separate_commands_test_subdir }} e2e-node-delete-test-subdir: description: "E2E Node Delete Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_node_delete_test_subdir }} @@ -104,6 +107,9 @@ on: e2e-node-update-coverage-report: description: "E2E Node Update Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_node_update_coverage_report }} + e2e-node-update-separate-commands-coverage-report: + description: "E2E Node Update - Separate commands Tests Coverage Report" + value: ${{ jobs.env-vars.outputs.e2e_node_update_separate_commands_coverage_report }} e2e-node-delete-coverage-report: description: "E2E Node Delete Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_node_delete_coverage_report }} @@ -140,6 +146,7 @@ jobs: e2e_node_add_local_test_subdir: e2e-node-add-local e2e_node_add_separate_commands_test_subdir: e2e-node-add-separate-commands e2e_node_update_test_subdir: e2e-node-update + e2e_node_update_separate_commands_test_subdir: e2e-node-update-separate-commands e2e_node_delete_test_subdir: e2e-node-delete e2e_node_delete_separate_commands_test_subdir: e2e-node-delete-separate-commands e2e_node_upgrade_test_subdir: e2e-node-upgrade @@ -155,6 +162,7 @@ jobs: e2e_node_add_local_coverage_report: "E2E Node Add Local Tests Coverage Report" e2e_node_add_separate_commands_coverage_report: "E2E Node Add - Separate commands Tests Coverage Report" e2e_node_update_coverage_report: "E2E Node Update Tests Coverage Report" + e2e_node_update_separate_commands_coverage_report: "E2E Node Update - Separate commands Tests Coverage Report" e2e_node_delete_coverage_report: "E2E Node Delete Tests Coverage Report" e2e_node_delete_separate_commands_coverage_report: "E2E Node Delete - Separate commands Tests Coverage Report" e2e_node_upgrade_coverage_report: "E2E Node Upgrade Tests Coverage Report" diff --git a/package.json b/package.json index 06513f814..71df55cd2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test-e2e-node-add-local": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Add Local Tests\" c8 --report-dir='coverage/e2e-node-add-local' mocha 'test/e2e/commands/node_add_local.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-add-local.xml", "test-e2e-node-add-separate-commands": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Add - Separate commands Tests\" c8 --report-dir='coverage/e2e-node-add-separate-commands' mocha 'test/e2e/commands/separate_node_add*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-add-separate-commands.xml", "test-e2e-node-update": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Update Tests\" c8 --report-dir='coverage/e2e-node-update' mocha 'test/e2e/commands/node_update*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-update.xml", + "test-e2e-node-update-separate-commands": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Update - Separate commands Tests\" c8 --report-dir='coverage/e2e-node-update-separate-commands' mocha 'test/e2e/commands/separate_node_update.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-update-separate-commands.xml", "test-e2e-node-delete": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Delete Tests\" c8 --report-dir='coverage/e2e-node-delete' mocha 'test/e2e/commands/node_delete*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-delete.xml", "test-e2e-node-delete-separate-commands": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Delete - Separate commands Tests\" c8 --report-dir='coverage/e2e-node-delete-separate-commands' mocha 'test/e2e/commands/separate_node_delete*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-delete-separate-commands.xml", "test-e2e-node-upgrade": "cross-env MOCHA_SUITE_NAME=\"Mocha E2E Node Upgrade Tests\" c8 --report-dir='coverage/e2e-node-upgrade' mocha 'test/e2e/commands/node_upgrade*.test.ts' --reporter-options configFile=mocha-multi-reporter.json,cmrOutput=mocha-junit-reporter+mochaFile+junit-e2e-node-upgrade.xml", diff --git a/src/commands/flags.ts b/src/commands/flags.ts index 50e2c87db..e7dc3e7bb 100644 --- a/src/commands/flags.ts +++ b/src/commands/flags.ts @@ -760,7 +760,9 @@ export const allFlags: CommandFlag[] = [ tlsPublicKey, updateAccountKeys, valuesFile, - mirrorNodeVersion + mirrorNodeVersion, + inputDir, + outputDir ] /** Resets the definition.disablePrompt for all flags */ diff --git a/src/commands/node/configs.ts b/src/commands/node/configs.ts index e0c5a18d5..4882806a0 100644 --- a/src/commands/node/configs.ts +++ b/src/commands/node/configs.ts @@ -461,7 +461,7 @@ export interface NodeUpdateConfigClass { localBuildPath: string namespace: string newAccountNumber: string - newAdminKey: string + newAdminKey: PrivateKey nodeAlias: NodeAlias releaseTag: string tlsPrivateKey: string @@ -470,7 +470,7 @@ export interface NodeUpdateConfigClass { allNodeAliases: NodeAliases chartPath: string existingNodeAliases: NodeAliases - freezeAdminPrivateKey: string + freezeAdminPrivateKey: PrivateKey keysDir: string nodeClient: any podNames: Record diff --git a/src/commands/node/flags.ts b/src/commands/node/flags.ts index 525146a87..b6cee8a65 100644 --- a/src/commands/node/flags.ts +++ b/src/commands/node/flags.ts @@ -23,23 +23,59 @@ export const DEFAULT_FLAGS = { optionalFlags: [flags.devMode] } +const COMMON_UPDATE_FLAGS_REQUIRED_FLAGS = [flags.cacheDir, flags.namespace, flags.releaseTag] +const COMMON_UPDATE_FLAGS_REQUIRED_NO_PROMPT_FLAGS = [ + flags.app, + flags.debugNodeAlias, + flags.endpointType, + flags.soloChartVersion, + +] +const COMMON_UPDATE_FLAGS_OPTIONAL_FLAGS = [ + flags.chartDirectory, flags.devMode, flags.quiet, flags.localBuildPath, flags.force, flags.gossipEndpoints, flags.grpcEndpoints +] + export const UPDATE_FLAGS = { - requiredFlags: [flags.cacheDir, flags.namespace, flags.nodeAlias, flags.releaseTag], + requiredFlags: [...COMMON_UPDATE_FLAGS_REQUIRED_FLAGS, flags.nodeAlias], requiredFlagsWithDisabledPrompt: [ - flags.app, - flags.debugNodeAlias, - flags.endpointType, - flags.soloChartVersion, + ...COMMON_UPDATE_FLAGS_REQUIRED_NO_PROMPT_FLAGS, + flags.newAdminKey, + flags.newAccountNumber, + flags.tlsPublicKey, flags.gossipPrivateKey, flags.gossipPublicKey, - flags.newAccountNumber, + flags.tlsPrivateKey + ], + optionalFlags: COMMON_UPDATE_FLAGS_OPTIONAL_FLAGS +} + +export const UPDATE_PREPARE_FLAGS = { + requiredFlags: [...COMMON_UPDATE_FLAGS_REQUIRED_FLAGS, flags.outputDir, flags.nodeAlias], + requiredFlagsWithDisabledPrompt: [ + ...COMMON_UPDATE_FLAGS_REQUIRED_NO_PROMPT_FLAGS, flags.newAdminKey, - flags.tlsPrivateKey, - flags.tlsPublicKey + flags.newAccountNumber, + flags.tlsPublicKey, + flags.gossipPrivateKey, + flags.gossipPublicKey, + flags.tlsPrivateKey ], - optionalFlags: [flags.chartDirectory, flags.devMode, flags.quiet, flags.localBuildPath, flags.force, flags.gossipEndpoints, flags.grpcEndpoints] + optionalFlags: [...COMMON_UPDATE_FLAGS_OPTIONAL_FLAGS] +} + +export const UPDATE_SUBMIT_TRANSACTIONS_FLAGS = { + requiredFlags: [...COMMON_UPDATE_FLAGS_REQUIRED_FLAGS, flags.inputDir], + requiredFlagsWithDisabledPrompt: [...COMMON_UPDATE_FLAGS_REQUIRED_NO_PROMPT_FLAGS], + optionalFlags: [...COMMON_UPDATE_FLAGS_OPTIONAL_FLAGS] } +export const UPDATE_EXECUTE_FLAGS = { + requiredFlags: [...COMMON_UPDATE_FLAGS_REQUIRED_FLAGS, flags.inputDir], + requiredFlagsWithDisabledPrompt: [...COMMON_UPDATE_FLAGS_REQUIRED_NO_PROMPT_FLAGS], + optionalFlags: [...COMMON_UPDATE_FLAGS_OPTIONAL_FLAGS] +} + + const COMMON_DELETE_REQUIRED_FLAGS = [ flags.cacheDir, flags.namespace, diff --git a/src/commands/node/handlers.ts b/src/commands/node/handlers.ts index e5b3ab760..0d92936e9 100644 --- a/src/commands/node/handlers.ts +++ b/src/commands/node/handlers.ts @@ -73,6 +73,7 @@ export class NodeCommandHandlers { static readonly ADD_CONTEXT_FILE = 'node-add.json' static readonly DELETE_CONTEXT_FILE = 'node-delete.json' + static readonly UPDATE_CONTEXT_FILE = 'node-update.json' async close () { await this.accountManager.close() @@ -180,6 +181,48 @@ export class NodeCommandHandlers { ] } + updatePrepareTasks (argv, lease: LeaseWrapper) { + return [ + this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), + this.tasks.identifyExistingNodes(), + this.tasks.loadAdminKey(), + this.tasks.prepareUpgradeZip(), + this.tasks.checkExistingNodesStakedAmount(), + ] + } + + updateSubmitTransactionsTasks (argv) { + return [ + this.tasks.sendNodeUpdateTransaction(), + this.tasks.sendPrepareUpgradeTransaction(), + this.tasks.downloadNodeGeneratedFiles(), + this.tasks.sendFreezeUpgradeTransaction(), + ] + } + + updateExecuteTasks (argv) { + return [ + this.tasks.prepareStagingDirectory('allNodeAliases'), + this.tasks.copyNodeKeysToSecrets(), + this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), + this.tasks.getNodeLogsAndConfigs(), + this.tasks.updateChartWithConfigMap( + 'Update chart to use new configMap due to account number change', + (ctx: any) => !ctx.config.newAccountNumber && !ctx.config.debugNodeAlias + ), + this.tasks.killNodesAndUpdateConfigMap(), + this.tasks.checkNodePodsAreRunning(), + this.tasks.fetchPlatformSoftware('allNodeAliases'), + this.tasks.setupNetworkNodes('allNodeAliases'), + this.tasks.startNodes('allNodeAliases'), + this.tasks.enablePortForwarding(), + this.tasks.checkAllNodesAreActive('allNodeAliases'), + this.tasks.checkAllNodeProxiesAreActive(), + this.tasks.triggerStakeWeightCalculate(), + this.tasks.finalize() + ] + } + /** ******** Handlers **********/ async prepareUpgrade (argv: any) { @@ -240,43 +283,63 @@ export class NodeCommandHandlers { const lease = this.leaseManager.instantiateLease() + const action = helpers.commandActionBuilder([ + ...this.updatePrepareTasks(argv, lease), + ...this.updateSubmitTransactionsTasks(argv), + ...this.updateExecuteTasks(argv), + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }, 'Error in updating nodes', lease) + + await action(argv, this) + return true + } + + async updatePrepare (argv) { + argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_PREPARE_FLAGS) + const lease = this.leaseManager.instantiateLease() + + const action = helpers.commandActionBuilder([ + ...this.updatePrepareTasks(argv, lease), + this.tasks.saveContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, helpers.updateSaveContextParser) + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }, 'Error in preparing node update', lease) + + await action(argv, this) + return true + } + + async updateSubmitTransactions (argv) { + const lease = this.leaseManager.instantiateLease() + argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_SUBMIT_TRANSACTIONS_FLAGS) const action = helpers.commandActionBuilder([ this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), - this.tasks.identifyExistingNodes(), - this.tasks.prepareGossipEndpoints(), - this.tasks.prepareGrpcServiceEndpoints(), + this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, helpers.updateLoadContextParser), this.tasks.loadAdminKey(), - this.tasks.prepareUpgradeZip(), - this.tasks.checkExistingNodesStakedAmount(), - this.tasks.sendNodeUpdateTransaction(), - this.tasks.sendPrepareUpgradeTransaction(), - this.tasks.downloadNodeGeneratedFiles(), - this.tasks.sendFreezeUpgradeTransaction(), - this.tasks.prepareStagingDirectory('allNodeAliases'), - this.tasks.copyNodeKeysToSecrets(), - this.tasks.checkAllNodesAreFrozen('existingNodeAliases'), - this.tasks.getNodeLogsAndConfigs(), - this.tasks.updateChartWithConfigMap( - 'Update chart to use new configMap due to account number change', - (ctx: any) => !ctx.config.newAccountNumber && !ctx.config.debugNodeAlias - ), - this.tasks.killNodesAndUpdateConfigMap(), - this.tasks.checkNodePodsAreRunning(), - this.tasks.fetchPlatformSoftware('allNodeAliases'), - this.tasks.setupNetworkNodes('allNodeAliases'), + ...this.updateSubmitTransactionsTasks(argv) + ], { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION + }, 'Error in submitting transactions for node update', lease) - // START BE - this.tasks.startNodes('allNodeAliases'), + await action(argv, this) + return true + } - this.tasks.enablePortForwarding(), - this.tasks.checkAllNodesAreActive('allNodeAliases'), - this.tasks.checkAllNodeProxiesAreActive(), - this.tasks.triggerStakeWeightCalculate(), - this.tasks.finalize() + async updateExecute (argv) { + const lease = this.leaseManager.instantiateLease() + argv = helpers.addFlagsToArgv(argv, NodeFlags.UPDATE_EXECUTE_FLAGS) + const action = helpers.commandActionBuilder([ + this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), + this.tasks.loadContextData(argv, NodeCommandHandlers.UPDATE_CONTEXT_FILE, helpers.updateLoadContextParser), + ...this.updateExecuteTasks(argv) ], { concurrent: false, rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION - }, 'Error in updating nodes', lease) + }, 'Error in executing node update', lease) await action(argv, this) return true @@ -284,9 +347,7 @@ export class NodeCommandHandlers { async delete (argv: any) { argv = helpers.addFlagsToArgv(argv, NodeFlags.DELETE_FLAGS) - const lease = this.leaseManager.instantiateLease() - const action = helpers.commandActionBuilder([ ...this.deletePrepareTaskList(argv, lease), ...this.deleteSubmitTransactionsTaskList(argv), @@ -323,7 +384,7 @@ export class NodeCommandHandlers { const lease = this.leaseManager.instantiateLease() const action = helpers.commandActionBuilder([ - this.tasks.initialize(argv, updateConfigBuilder.bind(this), lease), + this.tasks.initialize(argv, deleteConfigBuilder.bind(this), lease), this.tasks.loadContextData(argv, NodeCommandHandlers.DELETE_CONTEXT_FILE, helpers.deleteLoadContextParser), ...this.deleteSubmitTransactionsTaskList(argv) ], { diff --git a/src/commands/node/index.ts b/src/commands/node/index.ts index a61751bd6..1a9f91400 100644 --- a/src/commands/node/index.ts +++ b/src/commands/node/index.ts @@ -170,6 +170,27 @@ export class NodeCommand extends BaseCommand { handler: 'update' }, NodeFlags.UPDATE_FLAGS)) + .command(new YargsCommand({ + command: 'update-prepare', + description: 'Prepare the deployment to update a node with a specific version of Hedera platform', + commandDef: nodeCmd, + handler: 'updatePrepare' + }, NodeFlags.UPDATE_PREPARE_FLAGS)) + + .command(new YargsCommand({ + command: 'update-submit-transactions', + description: 'Submit transactions for updating a node with a specific version of Hedera platform', + commandDef: nodeCmd, + handler: 'updateSubmitTransactions' + }, NodeFlags.UPDATE_SUBMIT_TRANSACTIONS_FLAGS)) + + .command(new YargsCommand({ + command: 'update-execute', + description: 'Executes the updating of a node with a specific version of Hedera platform', + commandDef: nodeCmd, + handler: 'updateExecute' + }, NodeFlags.UPDATE_SUBMIT_TRANSACTIONS_FLAGS)) + .command(new YargsCommand({ command: 'delete', description: 'Delete a node with a specific version of Hedera platform', diff --git a/src/commands/node/tasks.ts b/src/commands/node/tasks.ts index 4358f020f..88013db5c 100644 --- a/src/commands/node/tasks.ts +++ b/src/commands/node/tasks.ts @@ -1058,7 +1058,7 @@ export class NodeCommandTasks { let parsedNewKey if (config.newAdminKey) { - parsedNewKey = PrivateKey.fromStringED25519(config.newAdminKey) + parsedNewKey = PrivateKey.fromStringED25519(config.newAdminKey.toString()) nodeUpdateTx.setAdminKey(parsedNewKey.publicKey) } await nodeUpdateTx.freezeWith(config.nodeClient) diff --git a/src/core/helpers.ts b/src/core/helpers.ts index 407291061..f715de9a2 100644 --- a/src/core/helpers.ts +++ b/src/core/helpers.ts @@ -27,7 +27,7 @@ import { FileContentsQuery, FileId, PrivateKey, ServiceEndpoint } from '@hashgra import { Listr } from 'listr2' import { type AccountManager } from './account_manager.ts' import { type NodeAlias, type NodeAliases, type PodName } from '../types/aliases.ts' -import { type NodeDeleteConfigClass } from '../commands/node/configs.ts' +import { type NodeDeleteConfigClass, type NodeUpdateConfigClass } from '../commands/node/configs.ts' import { type CommandFlag } from '../types/index.ts' import { type V1Pod } from '@kubernetes/client-node' import { type SoloLogger } from './logging.ts' @@ -374,6 +374,56 @@ export function deleteLoadContextParser (ctx: { config: NodeDeleteConfigClass, u config.podNames = {} } +/** + * Returns an object that can be written to a file without data loss. + * Contains fields needed for updating a node through separate commands + * @param ctx - accumulator object + * @returns file writable object + */ +export function updateSaveContextParser (ctx: { config: NodeUpdateConfigClass, upgradeZipHash: any }) { + const exportedCtx: any = {} + + const config = /** @type {NodeUpdateConfigClass} **/ ctx.config + exportedCtx.newAdminKey = config.newAdminKey.toString() + exportedCtx.freezeAdminPrivateKey = config.freezeAdminPrivateKey.toString() + exportedCtx.treasuryKey = config.treasuryKey.toString() + exportedCtx.existingNodeAliases = config.existingNodeAliases + exportedCtx.upgradeZipHash = ctx.upgradeZipHash + exportedCtx.nodeAlias = config.nodeAlias + exportedCtx.newAccountNumber = config.newAccountNumber + exportedCtx.tlsPublicKey = config.tlsPublicKey + exportedCtx.tlsPrivateKey = config.tlsPrivateKey + exportedCtx.gossipPublicKey = config.gossipPublicKey + exportedCtx.gossipPrivateKey = config.gossipPrivateKey + exportedCtx.allNodeAliases = config.allNodeAliases + + return exportedCtx +} + +/** + * Initializes objects in the context from a provided string + * Contains fields needed for updating a node through separate commands + * @param ctx - accumulator object + * @param ctxData - data in string format + * @returns file writable object + */ +export function updateLoadContextParser (ctx: { config: NodeUpdateConfigClass, upgradeZipHash: any }, ctxData: any) { + const config = ctx.config + config.newAdminKey = PrivateKey.fromStringED25519(ctxData.newAdminKey) + config.freezeAdminPrivateKey = PrivateKey.fromStringED25519(ctxData.freezeAdminPrivateKey) + config.treasuryKey = PrivateKey.fromStringED25519(ctxData.treasuryKey) + config.existingNodeAliases = ctxData.existingNodeAliases + config.nodeAlias = ctxData.nodeAlias + config.newAccountNumber = ctxData.newAccountNumber + config.tlsPublicKey = ctxData.tlsPublicKey + config.tlsPrivateKey = ctxData.tlsPrivateKey + config.gossipPublicKey = ctxData.gossipPublicKey + config.gossipPrivateKey = ctxData.gossipPrivateKey + config.allNodeAliases = ctxData.allNodeAliases + ctx.upgradeZipHash = ctxData.upgradeZipHash + config.podNames = {} +} + export function prepareEndpoints (endpointType: string, endpoints: string[], defaultPort: number | string) { const ret: ServiceEndpoint[] = [] for (const endpoint of endpoints) { diff --git a/test/e2e/commands/node_update.test.ts b/test/e2e/commands/node_update.test.ts index a54fdc558..482873fab 100644 --- a/test/e2e/commands/node_update.test.ts +++ b/test/e2e/commands/node_update.test.ts @@ -98,7 +98,9 @@ e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefi expect(nodeCmd.getUnusedConfigs(NodeCommandConfigs.UPDATE_CONFIGS_NAME)).to.deep.equal([ flags.devMode.constName, flags.quiet.constName, - flags.force.constName + flags.force.constName, + flags.gossipEndpoints.constName, + flags.grpcEndpoints.constName, ]) await bootstrapResp.opts.accountManager.close() }).timeout(30 * MINUTES) diff --git a/test/e2e/commands/separate_node_update.test.ts b/test/e2e/commands/separate_node_update.test.ts new file mode 100644 index 000000000..23bba32d1 --- /dev/null +++ b/test/e2e/commands/separate_node_update.test.ts @@ -0,0 +1,154 @@ +/** + * 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 { it, describe, after } from 'mocha' +import { expect } from 'chai' + +import { flags } from '../../../src/commands/index.ts' +import { constants } from '../../../src/core/index.ts' +import { + accountCreationShouldSucceed, + balanceQueryShouldSucceed, + e2eTestSuite, + getDefaultArgv, getNodeAliasesPrivateKeysHash, getTmpDir, + HEDERA_PLATFORM_VERSION_TAG +} from '../../test_util.ts' +import { getNodeLogs } from '../../../src/core/helpers.ts' +import { HEDERA_HAPI_PATH, MINUTES, ROOT_CONTAINER } from '../../../src/core/constants.ts' +import fs from 'fs' +import type { PodName } from '../../../src/types/aliases.ts' +import * as NodeCommandConfigs from '../../../src/commands/node/configs.ts' + +const defaultTimeout = 2 * MINUTES +const namespace = 'node-update-separate' +const updateNodeId = 'node2' +const newAccountId = '0.0.7' +const argv = getDefaultArgv() +argv[flags.nodeAliasesUnparsed.name] = 'node1,node2,node3' +argv[flags.nodeAlias.name] = updateNodeId + +argv[flags.newAccountNumber.name] = newAccountId +argv[flags.newAdminKey.name] = '302e020100300506032b6570042204200cde8d512569610f184b8b399e91e46899805c6171f7c2b8666d2a417bcc66c2' + +argv[flags.generateGossipKeys.name] = true +argv[flags.generateTlsKeys.name] = true +// set the env variable SOLO_CHARTS_DIR if developer wants to use local Solo charts +argv[flags.chartDirectory.name] = process.env.SOLO_CHARTS_DIR ?? undefined +argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG +argv[flags.namespace.name] = namespace +argv[flags.persistentVolumeClaims.name] = true +argv[flags.quiet.name] = true + +e2eTestSuite(namespace, argv, undefined, undefined, undefined, undefined, undefined, undefined, true, (bootstrapResp) => { + describe('Node update via separated commands', async () => { + const nodeCmd = bootstrapResp.cmd.nodeCmd + const accountCmd = bootstrapResp.cmd.accountCmd + const k8 = bootstrapResp.opts.k8 + let existingServiceMap + let existingNodeIdsPrivateKeysHash + + after(async function () { + this.timeout(10 * MINUTES) + + await getNodeLogs(k8, namespace) + await nodeCmd.handlers.stop(argv) + await k8.deleteNamespace(namespace) + }) + + it('cache current version of private keys', async () => { + existingServiceMap = await bootstrapResp.opts.accountManager.getNodeServiceMap(namespace) + existingNodeIdsPrivateKeysHash = await getNodeAliasesPrivateKeysHash(existingServiceMap, namespace, k8, getTmpDir()) + }).timeout(8 * MINUTES) + + it('should succeed with init command', async () => { + const status = await accountCmd.init(argv) + expect(status).to.be.ok + }).timeout(8 * MINUTES) + + it('should update a new node property successfully', async () => { + // generate gossip and tls keys for the updated node + const tmpDir = getTmpDir() + + const signingKey = await bootstrapResp.opts.keyManager.generateSigningKey(updateNodeId) + const signingKeyFiles = await bootstrapResp.opts.keyManager.storeSigningKey(updateNodeId, signingKey, tmpDir) + nodeCmd.logger.debug(`generated test gossip signing keys for node ${updateNodeId} : ${signingKeyFiles.certificateFile}`) + argv[flags.gossipPublicKey.name] = signingKeyFiles.certificateFile + argv[flags.gossipPrivateKey.name] = signingKeyFiles.privateKeyFile + + const tlsKey = await bootstrapResp.opts.keyManager.generateGrpcTlsKey(updateNodeId) + const tlsKeyFiles = await bootstrapResp.opts.keyManager.storeTLSKey(updateNodeId, tlsKey, tmpDir) + nodeCmd.logger.debug(`generated test TLS keys for node ${updateNodeId} : ${tlsKeyFiles.certificateFile}`) + argv[flags.tlsPublicKey.name] = tlsKeyFiles.certificateFile + argv[flags.tlsPrivateKey.name] = tlsKeyFiles.privateKeyFile + + const tempDir = 'contextDir' + const argvPrepare = Object.assign({}, argv) + argvPrepare[flags.outputDir.name] = tempDir + + const argvExecute = Object.assign({}, getDefaultArgv()) + argvExecute[flags.inputDir.name] = tempDir + + await nodeCmd.handlers.updatePrepare(argvPrepare) + await nodeCmd.handlers.updateSubmitTransactions(argvExecute) + await nodeCmd.handlers.updateExecute(argvExecute) + + expect(nodeCmd.getUnusedConfigs(NodeCommandConfigs.UPDATE_CONFIGS_NAME)).to.deep.equal([ + flags.devMode.constName, + flags.quiet.constName, + flags.force.constName, + flags.gossipEndpoints.constName, + flags.grpcEndpoints.constName, + 'freezeAdminPrivateKey' + ]) + await bootstrapResp.opts.accountManager.close() + }).timeout(30 * MINUTES) + + balanceQueryShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace) + + accountCreationShouldSucceed(bootstrapResp.opts.accountManager, nodeCmd, namespace) + + it('signing key and tls key should not match previous one', async () => { + const currentNodeIdsPrivateKeysHash = await getNodeAliasesPrivateKeysHash(existingServiceMap, namespace, k8, getTmpDir()) + + for (const [nodeAlias, existingKeyHashMap] of existingNodeIdsPrivateKeysHash.entries()) { + const currentNodeKeyHashMap = currentNodeIdsPrivateKeysHash.get(nodeAlias) + + for (const [keyFileName, existingKeyHash] of existingKeyHashMap.entries()) { + if (nodeAlias === updateNodeId && + (keyFileName.startsWith(constants.SIGNING_KEY_PREFIX) || keyFileName.startsWith('hedera'))) { + expect(`${nodeAlias}:${keyFileName}:${currentNodeKeyHashMap.get(keyFileName)}`).not.to.equal( + `${nodeAlias}:${keyFileName}:${existingKeyHash}`) + } else { + expect(`${nodeAlias}:${keyFileName}:${currentNodeKeyHashMap.get(keyFileName)}`).to.equal( + `${nodeAlias}:${keyFileName}:${existingKeyHash}`) + } + } + } + }).timeout(defaultTimeout) + + it('config.txt should be changed with new account id', async () => { + // read config.txt file from first node, read config.txt line by line, it should not contain value of newAccountId + const pods = await k8.getPodsByLabel(['solo.hedera.com/type=network-node']) + const podName = pods[0].metadata.name as PodName + const tmpDir = getTmpDir() + await k8.copyFrom(podName, ROOT_CONTAINER, `${HEDERA_HAPI_PATH}/config.txt`, tmpDir) + const configTxt = fs.readFileSync(`${tmpDir}/config.txt`, 'utf8') + console.log('config.txt:', configTxt) + + expect(configTxt).to.contain(newAccountId) + }).timeout(10 * MINUTES) + }) +})