diff --git a/.github/workflows/flow-task-test.yaml b/.github/workflows/flow-task-test.yaml index 6aa191d33..8d4792dca 100644 --- a/.github/workflows/flow-task-test.yaml +++ b/.github/workflows/flow-task-test.yaml @@ -63,13 +63,6 @@ jobs: verbosity: 3 wait: 120s - - name: Install Dependencies - id: npm-deps - run: npm ci - - - name: Compile Project - run: npm run build - - name: Run Example Task File Test run: | task default-with-relay diff --git a/.github/workflows/zxc-update-readme.yaml b/.github/workflows/zxc-update-readme.yaml index 49dac6020..903e4a1ef 100644 --- a/.github/workflows/zxc-update-readme.yaml +++ b/.github/workflows/zxc-update-readme.yaml @@ -146,7 +146,7 @@ jobs: if : ${{ github.event.inputs.dry-run-enabled != 'true' && !cancelled() && !failure() && inputs.commit-changes }} uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1 with: - commit_message: "auto update docs/content/User/StepByStepGuide.md [skip ci]" + commit_message: "chore: auto update docs/content/User/StepByStepGuide.md" commit_options: '--no-verify --signoff' add_options: '-u' file_pattern: 'docs/content/User/StepByStepGuide.md' diff --git a/README.md b/README.md index f52604dd4..1728baa0c 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,4 @@ expected to uphold this code of conduct. ## License -[Apache License 2.0](LICENSE) +[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/Taskfile.helper.yml b/Taskfile.helper.yml index ccc17a41d..2e6f9760e 100644 --- a/Taskfile.helper.yml +++ b/Taskfile.helper.yml @@ -33,6 +33,7 @@ env: tasks: init: cmds: + - task: "install:solo" - task: "var:check" - task: "run:build" diff --git a/Taskfile.yml b/Taskfile.yml index 1f7261e03..5994169b9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -21,7 +21,6 @@ tasks: - echo "This command is meant to deploy a Solo network to a Kind cluster on your local machine, " - echo "ctrl-c if this is not what you want to do." - sleep 5 - - task: "install:solo" - task: "install" - task: "start" diff --git a/docs/content/User/SDK.md b/docs/content/User/SDK.md index 3156e1974..cbb5ef1f8 100644 --- a/docs/content/User/SDK.md +++ b/docs/content/User/SDK.md @@ -1,8 +1,8 @@ # Using Solo with Hedera JavaScript SDK First, please follow solo repository README to install solo and Docker Desktop. -You also need to install the Taskfile tool following the instructions here: -https://taskfile.dev/installation/ +You also need to install the Taskfile tool following the instructions [here](https://taskfile.dev/installation/). + Then we start with launching a local Solo network with the following commands: diff --git a/docs/layouts/_default/_markup/render-link.html b/docs/layouts/_default/_markup/render-link.html index 22e9e912d..aebd36bc9 100644 --- a/docs/layouts/_default/_markup/render-link.html +++ b/docs/layouts/_default/_markup/render-link.html @@ -1,2 +1,6 @@ +{{ $destination := .Destination }} +{{ if strings.HasPrefix $destination "http" }} +{{ .Text | safeHTML }} +{{ else }} {{ .Text | safeHTML }} - +{{ end }} diff --git a/examples/Taskfile.examples.yml b/examples/Taskfile.examples.yml index 38ac0d4db..f8db6a49b 100644 --- a/examples/Taskfile.examples.yml +++ b/examples/Taskfile.examples.yml @@ -11,7 +11,6 @@ tasks: cmds: - task: "install:kubectl:darwin" - task: "install:kubectl:linux" - - task: "install:solo" - task: "install" - task: "start" diff --git a/package-lock.json b/package-lock.json index e68bd5c09..0771cb67a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.13", "@types/yargs": "^17.0.33", - "@typescript-eslint/utils": "^8.19.0", + "@typescript-eslint/utils": "^8.19.1", "c8": "^10.1.3", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", @@ -105,7 +105,7 @@ "tsx": "^4.19.2", "typedoc": "^0.27.6", "typescript": "^5.7.2", - "typescript-eslint": "^8.19.0" + "typescript-eslint": "^8.19.1" }, "engines": { "node": ">=20.14.0", @@ -2863,20 +2863,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.0.tgz", - "integrity": "sha512-NggSaEZCdSrFddbctrVjkVZvFC6KGfKfNK0CU7mNK/iKHGKbzT4Wmgm08dKpcZECBu9f5FypndoMyRHkdqfT1Q==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", + "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/type-utils": "8.19.0", - "@typescript-eslint/utils": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/type-utils": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2891,16 +2891,28 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.0.tgz", - "integrity": "sha512-6M8taKyOETY1TKHp0x8ndycipTVgmp4xtg5QpEZzXxDhNvvHOJi5rLRkLr8SK3jTgD5l4fTlvBiRdfsuWydxBw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", + "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4" }, "engines": { @@ -2916,13 +2928,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.0.tgz", - "integrity": "sha512-hkoJiKQS3GQ13TSMEiuNmSCvhz7ujyqD1x3ShbaETATHrck+9RaDdUbt+osXaUuns9OFwrDTTrjtwsU8gJyyRA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0" + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2933,15 +2945,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.0.tgz", - "integrity": "sha512-TZs0I0OSbd5Aza4qAMpp1cdCYVnER94IziudE3JU328YUHgWu9gwiwhag+fuLeJ2LkWLXI+F/182TbG+JaBdTg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", + "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.0", - "@typescript-eslint/utils": "8.19.0", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/utils": "8.19.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2955,10 +2967,22 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/@typescript-eslint/types": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.0.tgz", - "integrity": "sha512-8XQ4Ss7G9WX8oaYvD4OOLCjIQYgRQxO+qCiR2V2s2GxI9AUpo7riNwo6jDhKtTcaJjT8PY54j2Yb33kWtSJsmA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2969,19 +2993,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.0.tgz", - "integrity": "sha512-WW9PpDaLIFW9LCbucMSdYUuGeFUz1OkWYS/5fwZwTA+l2RwlWFdJvReQqMUMBw4yJWJOfqd7An9uwut2Oj8sLw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/visitor-keys": "8.19.0", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3018,16 +3042,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.0.tgz", - "integrity": "sha512-PTBG+0oEMPH9jCZlfg07LCB2nYI0I317yyvXGfxnvGvw4SHIOuRnQ3kadyyXY6tGdChusIHIbM5zfIbp4M6tCg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.0", - "@typescript-eslint/types": "8.19.0", - "@typescript-eslint/typescript-estree": "8.19.0" + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3042,12 +3078,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.0.tgz", - "integrity": "sha512-mCFtBbFBJDCNCWUl5y6sZSCHXw1DEFEk3c/M3nRK2a4XUB8StGFtmcEMizdjKuBzB6e/smJAAWYug3VrdLMr1w==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.19.0", + "@typescript-eslint/types": "8.19.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -12357,14 +12393,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.0.tgz", - "integrity": "sha512-Ni8sUkVWYK4KAcTtPjQ/UTiRk6jcsuDhPpxULapUDi8A/l8TSBk+t1GtJA1RsCzIJg0q6+J7bf35AwQigENWRQ==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.1.tgz", + "integrity": "sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.0", - "@typescript-eslint/parser": "8.19.0", - "@typescript-eslint/utils": "8.19.0" + "@typescript-eslint/eslint-plugin": "8.19.1", + "@typescript-eslint/parser": "8.19.1", + "@typescript-eslint/utils": "8.19.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index acba6cc2c..72b73686f 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.13", "@types/yargs": "^17.0.33", - "@typescript-eslint/utils": "^8.19.0", + "@typescript-eslint/utils": "^8.19.1", "c8": "^10.1.3", "chai": "^5.1.2", "chai-as-promised": "^8.0.1", @@ -134,7 +134,7 @@ "tsx": "^4.19.2", "typedoc": "^0.27.6", "typescript": "^5.7.2", - "typescript-eslint": "^8.19.0" + "typescript-eslint": "^8.19.1" }, "repository": { "type": "git", diff --git a/src/commands/base.ts b/src/commands/base.ts index cecdcbaca..38e756761 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -98,6 +98,10 @@ export abstract class BaseCommand extends ShellRunner { return this.configManager; } + getChartManager(): ChartManager { + return this.chartManager; + } + /** * Dynamically builds a class with properties from the provided list of flags * and extra properties, will keep track of which properties are used. Call @@ -171,6 +175,10 @@ export abstract class BaseCommand extends ShellRunner { return newConfigInstance; } + getLeaseManager(): LeaseManager { + return this.leaseManager; + } + /** * Get the list of unused configurations that were not accessed * @returns an array of unused configurations diff --git a/src/commands/cluster.ts b/src/commands/cluster.ts deleted file mode 100644 index ee9beb889..000000000 --- a/src/commands/cluster.ts +++ /dev/null @@ -1,394 +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. - * - */ -import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer'; -import {Listr} from 'listr2'; -import {SoloError} from '../core/errors.js'; -import {Flags as flags} from './flags.js'; -import {BaseCommand} from './base.js'; -import chalk from 'chalk'; -import * as constants from '../core/constants.js'; -import path from 'path'; -import {ListrLease} from '../core/lease/listr_lease.js'; -import {type CommandBuilder} from '../types/aliases.js'; - -/** - * Define the core functionalities of 'cluster' command - */ -export class ClusterCommand extends BaseCommand { - showClusterList() { - this.logger.showList('Clusters', this.k8.getClusters()); - return true; - } - - /** Get cluster-info for the given cluster name */ - getClusterInfo() { - try { - const cluster = this.k8.getKubeConfig().getCurrentCluster(); - this.logger.showJSON(`Cluster Information (${cluster.name})`, cluster); - this.logger.showUser('\n'); - return true; - } catch (e: Error | any) { - this.logger.showUserError(e); - } - - return false; - } - - /** Show list of installed chart */ - async showInstalledChartList(clusterSetupNamespace: string) { - this.logger.showList('Installed Charts', await this.chartManager.getInstalledCharts(clusterSetupNamespace)); - } - - /** Setup cluster with shared components */ - async setup(argv: any) { - const self = this; - - interface Context { - config: { - chartDir: string; - clusterSetupNamespace: string; - deployCertManager: boolean; - deployCertManagerCrds: boolean; - deployMinio: boolean; - deployPrometheusStack: boolean; - soloChartVersion: string; - }; - isChartInstalled: boolean; - chartPath: string; - valuesArg: string; - } - - const tasks = new Listr( - [ - { - title: 'Initialize', - task: async (ctx, task) => { - self.configManager.update(argv); - flags.disablePrompts([flags.chartDirectory]); - - await self.configManager.executePrompt(task, [ - flags.chartDirectory, - flags.clusterSetupNamespace, - flags.deployCertManager, - flags.deployCertManagerCrds, - flags.deployMinio, - flags.deployPrometheusStack, - ]); - - ctx.config = { - chartDir: self.configManager.getFlag(flags.chartDirectory) as string, - clusterSetupNamespace: self.configManager.getFlag(flags.clusterSetupNamespace) as string, - deployCertManager: self.configManager.getFlag(flags.deployCertManager) as boolean, - deployCertManagerCrds: self.configManager.getFlag(flags.deployCertManagerCrds) as boolean, - deployMinio: self.configManager.getFlag(flags.deployMinio) as boolean, - deployPrometheusStack: self.configManager.getFlag(flags.deployPrometheusStack) as boolean, - soloChartVersion: self.configManager.getFlag(flags.soloChartVersion) as string, - }; - - self.logger.debug('Prepare ctx.config', {config: ctx.config, argv}); - - ctx.isChartInstalled = await this.chartManager.isChartInstalled( - ctx.config.clusterSetupNamespace, - constants.SOLO_CLUSTER_SETUP_CHART, - ); - }, - }, - { - title: 'Prepare chart values', - task: async (ctx, _) => { - ctx.chartPath = await this.prepareChartPath( - ctx.config.chartDir, - constants.SOLO_TESTING_CHART_URL, - constants.SOLO_CLUSTER_SETUP_CHART, - ); - ctx.valuesArg = this.prepareValuesArg( - ctx.config.chartDir, - ctx.config.deployPrometheusStack, - ctx.config.deployMinio, - ctx.config.deployCertManager, - ctx.config.deployCertManagerCrds, - ); - }, - skip: ctx => ctx.isChartInstalled, - }, - { - title: `Install '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`, - task: async (ctx, _) => { - const clusterSetupNamespace = ctx.config.clusterSetupNamespace; - const version = ctx.config.soloChartVersion; - const valuesArg = ctx.valuesArg; - - try { - self.logger.debug(`Installing chart chartPath = ${ctx.chartPath}, version = ${version}`); - await self.chartManager.install( - clusterSetupNamespace, - constants.SOLO_CLUSTER_SETUP_CHART, - ctx.chartPath, - version, - valuesArg, - ); - } catch (e: Error | any) { - // if error, uninstall the chart and rethrow the error - self.logger.debug( - `Error on installing ${constants.SOLO_CLUSTER_SETUP_CHART}. attempting to rollback by uninstalling the chart`, - e, - ); - try { - await self.chartManager.uninstall(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); - } catch (ex) { - // ignore error during uninstall since we are doing the best-effort uninstall here - } - - throw e; - } - - if (argv.dev) { - await self.showInstalledChartList(clusterSetupNamespace); - } - }, - skip: ctx => ctx.isChartInstalled, - }, - ], - { - concurrent: false, - rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, - }, - ); - - try { - await tasks.run(); - } catch (e: Error | any) { - throw new SoloError('Error on cluster setup', e); - } - - return true; - } - - async reset(argv: any) { - const self = this; - const lease = await self.leaseManager.create(); - - interface Context { - config: { - clusterName: string; - clusterSetupNamespace: string; - }; - isChartInstalled: boolean; - } - - const tasks = new Listr( - [ - { - title: 'Initialize', - task: async (ctx, task) => { - if (!argv[flags.force.name]) { - const confirm = await task.prompt(ListrEnquirerPromptAdapter).run({ - type: 'toggle', - default: false, - message: 'Are you sure you would like to uninstall solo-cluster-setup chart?', - }); - - if (!confirm) { - process.exit(0); - } - } - - self.configManager.update(argv); - ctx.config = { - clusterName: self.configManager.getFlag(flags.clusterName) as string, - clusterSetupNamespace: self.configManager.getFlag(flags.clusterSetupNamespace) as string, - }; - - ctx.isChartInstalled = await this.chartManager.isChartInstalled( - ctx.config.clusterSetupNamespace, - constants.SOLO_CLUSTER_SETUP_CHART, - ); - if (!ctx.isChartInstalled) { - throw new SoloError('No chart found for the cluster'); - } - - return ListrLease.newAcquireLeaseTask(lease, task); - }, - }, - { - title: `Uninstall '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`, - task: async (ctx, _) => { - const clusterSetupNamespace = ctx.config.clusterSetupNamespace; - await self.chartManager.uninstall(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); - if (argv.dev) { - await self.showInstalledChartList(clusterSetupNamespace); - } - }, - skip: ctx => !ctx.isChartInstalled, - }, - ], - { - concurrent: false, - rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, - }, - ); - - try { - await tasks.run(); - } catch (e: Error | any) { - throw new SoloError('Error on cluster reset', e); - } finally { - await lease.release(); - } - - return true; - } - - /** Return Yargs command definition for 'cluster' command */ - getCommandDefinition(): {command: string; desc: string; builder: CommandBuilder} { - const self = this; - return { - command: 'cluster', - desc: 'Manage solo testing cluster', - builder: (yargs: any) => { - return yargs - .command({ - command: 'list', - desc: 'List all available clusters', - handler: (argv: any) => { - self.logger.debug("==== Running 'cluster list' ===", {argv}); - - try { - const r = self.showClusterList(); - self.logger.debug('==== Finished running `cluster list`===='); - - if (!r) process.exit(1); - } catch (err) { - self.logger.showUserError(err); - process.exit(1); - } - }, - }) - .command({ - command: 'info', - desc: 'Get cluster info', - handler: (argv: any) => { - self.logger.debug("==== Running 'cluster info' ===", {argv}); - try { - const r = this.getClusterInfo(); - self.logger.debug('==== Finished running `cluster info`===='); - - if (!r) process.exit(1); - } catch (err: Error | any) { - self.logger.showUserError(err); - process.exit(1); - } - }, - }) - .command({ - command: 'setup', - desc: 'Setup cluster with shared components', - builder: (y: any) => - flags.setCommandFlags( - y, - flags.chartDirectory, - flags.clusterName, - flags.clusterSetupNamespace, - flags.deployCertManager, - flags.deployCertManagerCrds, - flags.deployMinio, - flags.deployPrometheusStack, - flags.quiet, - flags.soloChartVersion, - ), - handler: (argv: any) => { - self.logger.debug("==== Running 'cluster setup' ===", {argv}); - - self - .setup(argv) - .then(r => { - self.logger.debug('==== Finished running `cluster setup`===='); - - if (!r) process.exit(1); - }) - .catch(err => { - self.logger.showUserError(err); - process.exit(1); - }); - }, - }) - .command({ - command: 'reset', - desc: 'Uninstall shared components from cluster', - builder: (y: any) => - flags.setCommandFlags(y, flags.clusterName, flags.clusterSetupNamespace, flags.force, flags.quiet), - handler: (argv: any) => { - self.logger.debug("==== Running 'cluster reset' ===", {argv}); - - self - .reset(argv) - .then(r => { - self.logger.debug('==== Finished running `cluster reset`===='); - - if (!r) process.exit(1); - }) - .catch(err => { - self.logger.showUserError(err); - process.exit(1); - }); - }, - }) - .demandCommand(1, 'Select a cluster command'); - }, - }; - } - - /** - * Prepare values arg for cluster setup command - * - * @param [chartDir] - local charts directory (default is empty) - * @param [prometheusStackEnabled] - a bool to denote whether to install prometheus stack - * @param [minioEnabled] - a bool to denote whether to install minio - * @param [certManagerEnabled] - a bool to denote whether to install cert manager - * @param [certManagerCrdsEnabled] - a bool to denote whether to install cert manager CRDs - */ - prepareValuesArg( - chartDir = flags.chartDirectory.definition.defaultValue as string, - prometheusStackEnabled = flags.deployPrometheusStack.definition.defaultValue as boolean, - minioEnabled = flags.deployMinio.definition.defaultValue as boolean, - certManagerEnabled = flags.deployCertManager.definition.defaultValue as boolean, - certManagerCrdsEnabled = flags.deployCertManagerCrds.definition.defaultValue as boolean, - ) { - let valuesArg = chartDir ? `-f ${path.join(chartDir, 'solo-cluster-setup', 'values.yaml')}` : ''; - - valuesArg += ` --set cloud.prometheusStack.enabled=${prometheusStackEnabled}`; - valuesArg += ` --set cloud.minio.enabled=${minioEnabled}`; - valuesArg += ` --set cloud.certManager.enabled=${certManagerEnabled}`; - valuesArg += ` --set cert-manager.installCRDs=${certManagerCrdsEnabled}`; - - if (certManagerEnabled && !certManagerCrdsEnabled) { - this.logger.showUser( - chalk.yellowBright('> WARNING:'), - chalk.yellow( - 'cert-manager CRDs are required for cert-manager, please enable it if you have not installed it independently.', - ), - ); - } - - return valuesArg; - } - - close(): Promise { - // no-op - return Promise.resolve(); - } -} diff --git a/src/commands/cluster/configs.ts b/src/commands/cluster/configs.ts new file mode 100644 index 000000000..f579e6670 --- /dev/null +++ b/src/commands/cluster/configs.ts @@ -0,0 +1,124 @@ +/** + * 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 {type NodeAlias} from '../../types/aliases.js'; +import {Flags as flags} from '../flags.js'; +import * as constants from '../../core/constants.js'; +import {ListrEnquirerPromptAdapter} from '@listr2/prompt-adapter-enquirer'; +import {SoloError} from '../../core/errors.js'; + +export const CONNECT_CONFIGS_NAME = 'connectConfig'; + +export const connectConfigBuilder = async function (argv, ctx, task) { + const config = this.getConfig(CONNECT_CONFIGS_NAME, argv.flags, [ + 'currentDeploymentName', + ]) as ClusterConnectConfigClass; + + // set config in the context for later tasks to use + ctx.config = config; + + return ctx.config; +}; + +export const setupConfigBuilder = async function (argv, ctx, task) { + const parent = this.parent; + const configManager = parent.getConfigManager(); + configManager.update(argv); + flags.disablePrompts([flags.chartDirectory]); + + await configManager.executePrompt(task, [ + flags.chartDirectory, + flags.clusterSetupNamespace, + flags.deployCertManager, + flags.deployCertManagerCrds, + flags.deployMinio, + flags.deployPrometheusStack, + ]); + + ctx.config = { + chartDir: configManager.getFlag(flags.chartDirectory) as string, + clusterSetupNamespace: configManager.getFlag(flags.clusterSetupNamespace) as string, + deployCertManager: configManager.getFlag(flags.deployCertManager) as boolean, + deployCertManagerCrds: configManager.getFlag(flags.deployCertManagerCrds) as boolean, + deployMinio: configManager.getFlag(flags.deployMinio) as boolean, + deployPrometheusStack: configManager.getFlag(flags.deployPrometheusStack) as boolean, + soloChartVersion: configManager.getFlag(flags.soloChartVersion) as string, + } as ClusterSetupConfigClass; + + parent.logger.debug('Prepare ctx.config', {config: ctx.config, argv}); + + ctx.isChartInstalled = await parent + .getChartManager() + .isChartInstalled(ctx.config.clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); + + return ctx.config; +}; + +export const resetConfigBuilder = async function (argv, ctx, task) { + if (!argv[flags.force.name]) { + const confirm = await task.prompt(ListrEnquirerPromptAdapter).run({ + type: 'toggle', + default: false, + message: 'Are you sure you would like to uninstall solo-cluster-setup chart?', + }); + + if (!confirm) { + process.exit(0); + } + } + + this.parent.getConfigManager().update(argv); + + ctx.config = { + clusterName: this.parent.getConfigManager().getFlag(flags.clusterName) as string, + clusterSetupNamespace: this.parent.getConfigManager().getFlag(flags.clusterSetupNamespace) as string, + } as ClusterResetConfigClass; + + ctx.isChartInstalled = await this.parent + .getChartManager() + .isChartInstalled(ctx.config.clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); + if (!ctx.isChartInstalled) { + throw new SoloError('No chart found for the cluster'); + } + + return ctx.config; +}; + +export interface ClusterConnectConfigClass { + app: string; + cacheDir: string; + devMode: boolean; + namespace: string; + nodeAlias: NodeAlias; + context: string; + clusterName: string; +} + +export interface ClusterSetupConfigClass { + chartDir: string; + clusterSetupNamespace: string; + deployCertManager: boolean; + deployCertManagerCrds: boolean; + deployMinio: boolean; + deployPrometheusStack: boolean; + soloChartVersion: string; +} + +export interface ClusterResetConfigClass { + clusterName: string; + clusterSetupNamespace: string; +} diff --git a/src/commands/context/flags.ts b/src/commands/cluster/flags.ts similarity index 57% rename from src/commands/context/flags.ts rename to src/commands/cluster/flags.ts index 2dc606063..fd318e757 100644 --- a/src/commands/context/flags.ts +++ b/src/commands/cluster/flags.ts @@ -17,6 +17,34 @@ import {Flags as flags} from '../flags.js'; +export const DEFAULT_FLAGS = { + requiredFlags: [], + requiredFlagsWithDisabledPrompt: [], + optionalFlags: [], +}; + +export const SETUP_FLAGS = { + requiredFlags: [], + requiredFlagsWithDisabledPrompt: [], + optionalFlags: [ + flags.chartDirectory, + flags.clusterName, + flags.clusterSetupNamespace, + flags.deployCertManager, + flags.deployCertManagerCrds, + flags.deployMinio, + flags.deployPrometheusStack, + flags.quiet, + flags.soloChartVersion, + ], +}; + +export const RESET_FLAGS = { + requiredFlags: [], + requiredFlagsWithDisabledPrompt: [], + optionalFlags: [flags.clusterName, flags.clusterSetupNamespace, flags.force, flags.quiet], +}; + export const USE_FLAGS = { requiredFlags: [], requiredFlagsWithDisabledPrompt: [], diff --git a/src/commands/cluster/handlers.ts b/src/commands/cluster/handlers.ts new file mode 100644 index 000000000..805df1473 --- /dev/null +++ b/src/commands/cluster/handlers.ts @@ -0,0 +1,147 @@ +/** + * 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 {type BaseCommand, type CommandHandlers} from '../base.js'; +import {type ClusterCommandTasks} from './tasks.js'; +import * as helpers from '../../core/helpers.js'; +import * as constants from '../../core/constants.js'; +import * as ContextFlags from './flags.js'; +import {RemoteConfigTasks} from '../../core/config/remote/remote_config_tasks.js'; +import type {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js'; +import {connectConfigBuilder, resetConfigBuilder, setupConfigBuilder} from './configs.js'; +import {SoloError} from '../../core/errors.js'; + +export class ClusterCommandHandlers implements CommandHandlers { + readonly parent: BaseCommand; + readonly tasks: ClusterCommandTasks; + public readonly remoteConfigManager: RemoteConfigManager; + private getConfig: any; + + constructor(parent: BaseCommand, tasks: ClusterCommandTasks, remoteConfigManager: RemoteConfigManager) { + this.parent = parent; + this.tasks = tasks; + this.remoteConfigManager = remoteConfigManager; + this.getConfig = parent.getConfig.bind(parent); + } + + async connect(argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); + + const action = this.parent.commandActionBuilder( + [ + this.tasks.initialize(argv, connectConfigBuilder.bind(this)), + this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()), + this.tasks.selectContext(argv), + RemoteConfigTasks.loadRemoteConfig.bind(this)(argv), + this.tasks.updateLocalConfig(argv), + ], + { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, + }, + 'cluster connect', + null, + ); + + await action(argv, this); + return true; + } + + async list(argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); + + const action = this.parent.commandActionBuilder( + [this.tasks.showClusterList()], + { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, + }, + 'cluster list', + null, + ); + + await action(argv, this); + return true; + } + + async info(argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); + + const action = this.parent.commandActionBuilder( + [this.tasks.getClusterInfo()], + { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, + }, + 'cluster info', + null, + ); + + await action(argv, this); + return true; + } + + async setup(argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); + + const action = this.parent.commandActionBuilder( + [ + this.tasks.initialize(argv, setupConfigBuilder.bind(this)), + this.tasks.prepareChartValues(argv), + this.tasks.installClusterChart(argv), + ], + { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, + }, + 'cluster setup', + null, + ); + + try { + await action(argv, this); + } catch (e: Error | any) { + throw new SoloError('Error on cluster setup', e); + } + + return true; + } + + async reset(argv: any) { + argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); + + const action = this.parent.commandActionBuilder( + [ + this.tasks.initialize(argv, resetConfigBuilder.bind(this)), + this.tasks.acquireNewLease(argv), + this.tasks.uninstallClusterChart(argv), + ], + { + concurrent: false, + rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, + }, + 'cluster reset', + null, + ); + + try { + await action(argv, this); + } catch (e: Error | any) { + throw new SoloError('Error on cluster reset', e); + } + return true; + } +} diff --git a/src/commands/cluster/index.ts b/src/commands/cluster/index.ts new file mode 100644 index 000000000..016224b54 --- /dev/null +++ b/src/commands/cluster/index.ts @@ -0,0 +1,108 @@ +/** + * 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 * as ContextFlags from './flags.js'; +import {YargsCommand} from '../../core/yargs_command.js'; +import {BaseCommand} from './../base.js'; +import {type Opts} from '../../types/command_types.js'; +import {ClusterCommandTasks} from './tasks.js'; +import {ClusterCommandHandlers} from './handlers.js'; +import {DEFAULT_FLAGS, RESET_FLAGS, SETUP_FLAGS} from './flags.js'; + +/** + * Defines the core functionalities of 'node' command + */ +export class ClusterCommand extends BaseCommand { + public handlers: ClusterCommandHandlers; + + constructor(opts: Opts) { + super(opts); + + this.handlers = new ClusterCommandHandlers(this, new ClusterCommandTasks(this), this.remoteConfigManager); + } + + getCommandDefinition() { + return { + command: 'cluster', + desc: 'Manage solo testing cluster', + builder: (yargs: any) => { + return yargs + .command( + new YargsCommand( + { + command: 'connect', + description: 'updates the local configuration by connecting a deployment to a k8s context', + commandDef: this, + handler: 'connect', + }, + ContextFlags.USE_FLAGS, + ), + ) + .command( + new YargsCommand( + { + command: 'list', + description: 'List all available clusters', + commandDef: this, + handler: 'list', + }, + DEFAULT_FLAGS, + ), + ) + .command( + new YargsCommand( + { + command: 'info', + description: 'Get cluster info', + commandDef: this, + handler: 'info', + }, + DEFAULT_FLAGS, + ), + ) + .command( + new YargsCommand( + { + command: 'setup', + description: 'Setup cluster with shared components', + commandDef: this, + handler: 'setup', + }, + SETUP_FLAGS, + ), + ) + .command( + new YargsCommand( + { + command: 'reset', + description: 'Uninstall shared components from cluster', + commandDef: this, + handler: 'reset', + }, + RESET_FLAGS, + ), + ) + .demandCommand(1, 'Select a context command'); + }, + }; + } + + close(): Promise { + // no-op + return Promise.resolve(); + } +} diff --git a/src/commands/context/tasks.ts b/src/commands/cluster/tasks.ts similarity index 57% rename from src/commands/context/tasks.ts rename to src/commands/cluster/tasks.ts index e8474931c..1ca458b27 100644 --- a/src/commands/context/tasks.ts +++ b/src/commands/cluster/tasks.ts @@ -20,8 +20,12 @@ import type {ListrTaskWrapper} from 'listr2'; import type {ConfigBuilder} from '../../types/aliases.js'; import {type BaseCommand} from '../base.js'; import {splitFlagInput} from '../../core/helpers.js'; +import * as constants from '../../core/constants.js'; +import path from 'path'; +import chalk from 'chalk'; +import {ListrLease} from '../../core/lease/listr_lease.js'; -export class ContextCommandTasks { +export class ClusterCommandTasks { private readonly parent: BaseCommand; constructor(parent) { @@ -108,6 +112,49 @@ export class ContextCommandTasks { } } + /** + * Prepare values arg for cluster setup command + * + * @param [chartDir] - local charts directory (default is empty) + * @param [prometheusStackEnabled] - a bool to denote whether to install prometheus stack + * @param [minioEnabled] - a bool to denote whether to install minio + * @param [certManagerEnabled] - a bool to denote whether to install cert manager + * @param [certManagerCrdsEnabled] - a bool to denote whether to install cert manager CRDs + */ + private prepareValuesArg( + chartDir = flags.chartDirectory.definition.defaultValue as string, + prometheusStackEnabled = flags.deployPrometheusStack.definition.defaultValue as boolean, + minioEnabled = flags.deployMinio.definition.defaultValue as boolean, + certManagerEnabled = flags.deployCertManager.definition.defaultValue as boolean, + certManagerCrdsEnabled = flags.deployCertManagerCrds.definition.defaultValue as boolean, + ) { + let valuesArg = chartDir ? `-f ${path.join(chartDir, 'solo-cluster-setup', 'values.yaml')}` : ''; + + valuesArg += ` --set cloud.prometheusStack.enabled=${prometheusStackEnabled}`; + valuesArg += ` --set cloud.minio.enabled=${minioEnabled}`; + valuesArg += ` --set cloud.certManager.enabled=${certManagerEnabled}`; + valuesArg += ` --set cert-manager.installCRDs=${certManagerCrdsEnabled}`; + + if (certManagerEnabled && !certManagerCrdsEnabled) { + this.parent.logger.showUser( + chalk.yellowBright('> WARNING:'), + chalk.yellow( + 'cert-manager CRDs are required for cert-manager, please enable it if you have not installed it independently.', + ), + ); + } + + return valuesArg; + } + + /** Show list of installed chart */ + private async showInstalledChartList(clusterSetupNamespace: string) { + this.parent.logger.showList( + 'Installed Charts', + await this.parent.getChartManager().getInstalledCharts(clusterSetupNamespace), + ); + } + selectContext(argv) { return new Task('Read local configuration settings', async (ctx: any, task: ListrTaskWrapper) => { this.parent.logger.info('Read local configuration settings...'); @@ -186,4 +233,102 @@ export class ContextCommandTasks { ctx.config = await configInit(argv, ctx, task); }); } + + showClusterList() { + return new Task('List all available clusters', async (ctx: any, task: ListrTaskWrapper) => { + this.parent.logger.showList('Clusters', this.parent.getK8().getClusters()); + }); + } + + getClusterInfo() { + return new Task('Get cluster info', async (ctx: any, task: ListrTaskWrapper) => { + try { + const cluster = this.parent.getK8().getKubeConfig().getCurrentCluster(); + this.parent.logger.showJSON(`Cluster Information (${cluster.name})`, cluster); + this.parent.logger.showUser('\n'); + } catch (e: Error | any) { + this.parent.logger.showUserError(e); + } + }); + } + + prepareChartValues(argv) { + return new Task( + 'Prepare chart values', + async (ctx: any, task: ListrTaskWrapper) => { + ctx.chartPath = await this.parent.prepareChartPath( + ctx.config.chartDir, + constants.SOLO_TESTING_CHART_URL, + constants.SOLO_CLUSTER_SETUP_CHART, + ); + ctx.valuesArg = this.prepareValuesArg( + ctx.config.chartDir, + ctx.config.deployPrometheusStack, + ctx.config.deployMinio, + ctx.config.deployCertManager, + ctx.config.deployCertManagerCrds, + ); + }, + ctx => ctx.isChartInstalled, + ); + } + + installClusterChart(argv) { + const parent = this.parent; + return new Task( + `Install '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`, + async (ctx: any, task: ListrTaskWrapper) => { + const clusterSetupNamespace = ctx.config.clusterSetupNamespace; + const version = ctx.config.soloChartVersion; + const valuesArg = ctx.valuesArg; + + try { + parent.logger.debug(`Installing chart chartPath = ${ctx.chartPath}, version = ${version}`); + await parent + .getChartManager() + .install(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART, ctx.chartPath, version, valuesArg); + } catch (e: Error | any) { + // if error, uninstall the chart and rethrow the error + parent.logger.debug( + `Error on installing ${constants.SOLO_CLUSTER_SETUP_CHART}. attempting to rollback by uninstalling the chart`, + e, + ); + try { + await parent.getChartManager().uninstall(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); + } catch (ex) { + // ignore error during uninstall since we are doing the best-effort uninstall here + } + + throw e; + } + + if (argv.dev) { + await this.showInstalledChartList(clusterSetupNamespace); + } + }, + ctx => ctx.isChartInstalled, + ); + } + + acquireNewLease(argv) { + return new Task('Acquire new lease', async (ctx: any, task: ListrTaskWrapper) => { + const lease = await this.parent.getLeaseManager().create(); + return ListrLease.newAcquireLeaseTask(lease, task); + }); + } + + uninstallClusterChart(argv) { + const parent = this.parent; + return new Task( + `Uninstall '${constants.SOLO_CLUSTER_SETUP_CHART}' chart`, + async (ctx: any, task: ListrTaskWrapper) => { + const clusterSetupNamespace = ctx.config.clusterSetupNamespace; + await parent.getChartManager().uninstall(clusterSetupNamespace, constants.SOLO_CLUSTER_SETUP_CHART); + if (argv.dev) { + await this.showInstalledChartList(clusterSetupNamespace); + } + }, + ctx => !ctx.isChartInstalled, + ); + } } diff --git a/src/commands/context/configs.ts b/src/commands/context/configs.ts deleted file mode 100644 index 6a60f41fe..000000000 --- a/src/commands/context/configs.ts +++ /dev/null @@ -1,41 +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. - * - */ - -import {type NodeAlias} from '../../types/aliases.js'; - -export const CONNECT_CONFIGS_NAME = 'connectConfig'; - -export const connectConfigBuilder = async function (argv, ctx, task) { - const config = this.getConfig(CONNECT_CONFIGS_NAME, argv.flags, [ - 'currentDeploymentName', - ]) as ContextConnectConfigClass; - - // set config in the context for later tasks to use - ctx.config = config; - - return ctx.config; -}; - -export interface ContextConnectConfigClass { - app: string; - cacheDir: string; - devMode: boolean; - namespace: string; - nodeAlias: NodeAlias; - context: string; - clusterName: string; -} diff --git a/src/commands/context/handlers.ts b/src/commands/context/handlers.ts deleted file mode 100644 index 3c183ca08..000000000 --- a/src/commands/context/handlers.ts +++ /dev/null @@ -1,61 +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. - * - */ -import {type BaseCommand, type CommandHandlers} from '../base.js'; -import {type ContextCommandTasks} from './tasks.js'; -import * as helpers from '../../core/helpers.js'; -import * as constants from '../../core/constants.js'; -import * as ContextFlags from './flags.js'; -import {RemoteConfigTasks} from '../../core/config/remote/remote_config_tasks.js'; -import type {RemoteConfigManager} from '../../core/config/remote/remote_config_manager.js'; -import {connectConfigBuilder} from './configs.js'; - -export class ContextCommandHandlers implements CommandHandlers { - readonly parent: BaseCommand; - readonly tasks: ContextCommandTasks; - public readonly remoteConfigManager: RemoteConfigManager; - private getConfig: any; - - constructor(parent: BaseCommand, tasks: ContextCommandTasks, remoteConfigManager: RemoteConfigManager) { - this.parent = parent; - this.tasks = tasks; - this.remoteConfigManager = remoteConfigManager; - this.getConfig = parent.getConfig.bind(parent); - } - - async connect(argv: any) { - argv = helpers.addFlagsToArgv(argv, ContextFlags.USE_FLAGS); - - const action = this.parent.commandActionBuilder( - [ - this.tasks.initialize(argv, connectConfigBuilder.bind(this)), - this.parent.getLocalConfig().promptLocalConfigTask(this.parent.getK8()), - this.tasks.selectContext(argv), - RemoteConfigTasks.loadRemoteConfig.bind(this)(argv), - this.tasks.updateLocalConfig(argv), - ], - { - concurrent: false, - rendererOptions: constants.LISTR_DEFAULT_RENDERER_OPTION, - }, - 'context use', - null, - ); - - await action(argv, this); - return true; - } -} diff --git a/src/commands/context/index.ts b/src/commands/context/index.ts deleted file mode 100644 index 54fee60ad..000000000 --- a/src/commands/context/index.ts +++ /dev/null @@ -1,63 +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. - * - */ - -import * as ContextFlags from './flags.js'; -import {YargsCommand} from '../../core/yargs_command.js'; -import {BaseCommand} from './../base.js'; -import {type Opts} from '../../types/command_types.js'; -import {ContextCommandTasks} from './tasks.js'; -import {ContextCommandHandlers} from './handlers.js'; - -/** - * Defines the core functionalities of 'node' command - */ -export class ContextCommand extends BaseCommand { - private handlers: ContextCommandHandlers; - - constructor(opts: Opts) { - super(opts); - - this.handlers = new ContextCommandHandlers(this, new ContextCommandTasks(this), this.remoteConfigManager); - } - - getCommandDefinition() { - return { - command: 'context', - desc: 'Manage local and remote configurations', - builder: (yargs: any) => { - return yargs - .command( - new YargsCommand( - { - command: 'connect', - description: 'updates the local configuration by connecting a deployment to a k8s context', - commandDef: this, - handler: 'connect', - }, - ContextFlags.USE_FLAGS, - ), - ) - .demandCommand(1, 'Select a context command'); - }, - }; - } - - close(): Promise { - // no-op - return Promise.resolve(); - } -} diff --git a/src/commands/deployment.ts b/src/commands/deployment.ts index 4e6e0c0a5..74deb0f06 100644 --- a/src/commands/deployment.ts +++ b/src/commands/deployment.ts @@ -60,7 +60,11 @@ export class DeploymentCommand extends BaseCommand { self.configManager.update(argv); self.logger.debug('Updated config with argv', {config: self.configManager.config}); - await self.configManager.executePrompt(task, DeploymentCommand.DEPLOY_FLAGS_LIST); + await self.configManager.executePrompt(task, [ + flags.contextClusterUnparsed, + flags.namespace, + flags.deploymentClusters, + ]); ctx.config = { contextClusterUnparsed: self.configManager.getFlag(flags.contextClusterUnparsed), diff --git a/src/commands/index.ts b/src/commands/index.ts index 4b717ca83..dec7a7de5 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -14,8 +14,7 @@ * limitations under the License. * */ -import {ClusterCommand} from './cluster.js'; -import {ContextCommand} from './context/index.js'; +import {ClusterCommand} from './cluster/index.js'; import {InitCommand} from './init.js'; import {MirrorNodeCommand} from './mirror_node.js'; import {NetworkCommand} from './network.js'; @@ -33,7 +32,6 @@ import {type Opts} from '../types/command_types.js'; export function Initialize(opts: Opts) { const initCmd = new InitCommand(opts); const clusterCmd = new ClusterCommand(opts); - const contextCmd = new ContextCommand(opts); const networkCommand = new NetworkCommand(opts); const nodeCmd = new NodeCommand(opts); const relayCmd = new RelayCommand(opts); @@ -45,7 +43,6 @@ export function Initialize(opts: Opts) { initCmd.getCommandDefinition(), accountCmd.getCommandDefinition(), clusterCmd.getCommandDefinition(), - contextCmd.getCommandDefinition(), networkCommand.getCommandDefinition(), nodeCmd.getCommandDefinition(), relayCmd.getCommandDefinition(), diff --git a/src/core/config/local_config.ts b/src/core/config/local_config.ts index ce0712568..9dbfb910d 100644 --- a/src/core/config/local_config.ts +++ b/src/core/config/local_config.ts @@ -15,7 +15,6 @@ * */ import {IsEmail, IsNotEmpty, IsObject, IsString, validateSync} from 'class-validator'; -import type {ListrTask, ListrTaskWrapper} from 'listr2'; import fs from 'fs'; import * as yaml from 'yaml'; import {Flags as flags} from '../../commands/flags.js'; @@ -35,6 +34,7 @@ import {type K8} from '../k8.js'; import {splitFlagInput} from '../helpers.js'; import {inject, injectable} from 'tsyringe-neo'; import {patchInject} from '../container_helper.js'; +import type {SoloListrTask, SoloListrTaskWrapper} from '../../types/index.js'; @injectable() export class LocalConfig implements LocalConfigData { @@ -162,13 +162,17 @@ export class LocalConfig implements LocalConfigData { this.logger.info(`Wrote local config to ${this.filePath}: ${yamlContent}`); } - public promptLocalConfigTask(k8: K8): ListrTask { + public promptLocalConfigTask(k8: K8): SoloListrTask { const self = this; return { title: 'Prompt local configuration', skip: this.skipPromptTask, - task: async (_: any, task: ListrTaskWrapper): Promise => { + task: async (_: any, task: SoloListrTaskWrapper): Promise => { + if (self.configFileExists) { + self.configManager.setFlag(flags.userEmailAddress, self.userEmailAddress); + } + const isQuiet = self.configManager.getFlag(flags.quiet); const contexts = self.configManager.getFlag(flags.context); const deploymentName = self.configManager.getFlag(flags.namespace); diff --git a/test/e2e/commands/cluster.test.ts b/test/e2e/commands/cluster.test.ts index b734ab8d6..d0bd6e6b3 100644 --- a/test/e2e/commands/cluster.test.ts +++ b/test/e2e/commands/cluster.test.ts @@ -67,7 +67,7 @@ describe('ClusterCommand', () => { await k8.deleteNamespace(namespace); argv[flags.clusterSetupNamespace.name] = constants.SOLO_SETUP_NAMESPACE; configManager.update(argv); - await clusterCmd.setup(argv); // restore solo-cluster-setup for other e2e tests to leverage + await clusterCmd.handlers.setup(argv); // restore solo-cluster-setup for other e2e tests to leverage do { await sleep(Duration.ofSeconds(5)); } while ( @@ -85,33 +85,33 @@ describe('ClusterCommand', () => { it('should cleanup existing deployment', async () => { if (await chartManager.isChartInstalled(constants.SOLO_SETUP_NAMESPACE, constants.SOLO_CLUSTER_SETUP_CHART)) { - expect(await clusterCmd.reset(argv)).to.be.true; + expect(await clusterCmd.handlers.reset(argv)).to.be.true; } }).timeout(Duration.ofMinutes(1).toMillis()); it('solo cluster setup should fail with invalid cluster name', async () => { argv[flags.clusterSetupNamespace.name] = 'INVALID'; configManager.update(argv); - await expect(clusterCmd.setup(argv)).to.be.rejectedWith('Error on cluster setup'); + await expect(clusterCmd.handlers.setup(argv)).to.be.rejectedWith('Error on cluster setup'); }).timeout(Duration.ofMinutes(1).toMillis()); it('solo cluster setup should work with valid args', async () => { argv[flags.clusterSetupNamespace.name] = namespace; configManager.update(argv); - expect(await clusterCmd.setup(argv)).to.be.true; + expect(await clusterCmd.handlers.setup(argv)).to.be.true; }).timeout(Duration.ofMinutes(1).toMillis()); - it('function getClusterInfo should return true', () => { - expect(clusterCmd.getClusterInfo()).to.be.ok; + it('solo cluster info should work', () => { + expect(clusterCmd.handlers.info(argv)).to.be.ok; }).timeout(Duration.ofMinutes(1).toMillis()); - it('function showClusterList should return right true', async () => { - expect(clusterCmd.showClusterList()).to.be.ok; + it('solo cluster list', async () => { + expect(clusterCmd.handlers.list(argv)).to.be.ok; }).timeout(Duration.ofMinutes(1).toMillis()); it('function showInstalledChartList should return right true', async () => { // @ts-ignore - await expect(clusterCmd.showInstalledChartList()).to.eventually.be.undefined; + await expect(clusterCmd.handlers.tasks.showInstalledChartList()).to.eventually.be.undefined; }).timeout(Duration.ofMinutes(1).toMillis()); // helm list would return an empty list if given invalid namespace @@ -120,7 +120,7 @@ describe('ClusterCommand', () => { configManager.update(argv); try { - await expect(clusterCmd.reset(argv)).to.be.rejectedWith('Error on cluster reset'); + await expect(clusterCmd.handlers.reset(argv)).to.be.rejectedWith('Error on cluster reset'); } catch (e) { clusterCmd.logger.showUserError(e); expect.fail(); @@ -130,6 +130,6 @@ describe('ClusterCommand', () => { it('solo cluster reset should work with valid args', async () => { argv[flags.clusterSetupNamespace.name] = namespace; configManager.update(argv); - expect(await clusterCmd.reset(argv)).to.be.true; + expect(await clusterCmd.handlers.reset(argv)).to.be.true; }).timeout(Duration.ofMinutes(1).toMillis()); }); diff --git a/test/e2e/commands/network.test.ts b/test/e2e/commands/network.test.ts index 176cad97e..4c5acba64 100644 --- a/test/e2e/commands/network.test.ts +++ b/test/e2e/commands/network.test.ts @@ -67,7 +67,7 @@ describe('NetworkCommand', () => { before(async () => { await initCmd.init(argv); - await clusterCmd.setup(argv); + await clusterCmd.handlers.setup(argv); fs.mkdirSync(applicationEnvParentDirectory, {recursive: true}); fs.writeFileSync(applicationEnvFilePath, applicationEnvFileContents); }); diff --git a/test/test_util.ts b/test/test_util.ts index 598b59d91..e271c4a52 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -23,7 +23,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import {Flags as flags} from '../src/commands/flags.js'; -import {ClusterCommand} from '../src/commands/cluster.js'; +import {ClusterCommand} from '../src/commands/cluster/index.js'; import {InitCommand} from '../src/commands/init.js'; import {NetworkCommand} from '../src/commands/network.js'; import {NodeCommand} from '../src/commands/node/index.js'; @@ -261,7 +261,7 @@ export function e2eTestSuite( if ( !(await chartManager.isChartInstalled(constants.SOLO_SETUP_NAMESPACE, constants.SOLO_CLUSTER_SETUP_CHART)) ) { - await clusterCmd.setup(argv); + await clusterCmd.handlers.setup(argv); } }).timeout(Duration.ofMinutes(2).toMillis()); diff --git a/test/unit/commands/cluster.test.ts b/test/unit/commands/cluster.test.ts index 6ff3afd2d..db90391f1 100644 --- a/test/unit/commands/cluster.test.ts +++ b/test/unit/commands/cluster.test.ts @@ -18,8 +18,14 @@ import sinon from 'sinon'; import {describe, it, beforeEach} from 'mocha'; import {expect} from 'chai'; -import {ClusterCommand} from '../../../src/commands/cluster.js'; -import {getDefaultArgv, HEDERA_PLATFORM_VERSION_TAG, TEST_CLUSTER} from '../../test_util.js'; +import {ClusterCommand} from '../../../src/commands/cluster/index.js'; +import { + getDefaultArgv, + getTestCacheDir, + HEDERA_PLATFORM_VERSION_TAG, + TEST_CLUSTER, + testLocalConfigData, +} from '../../test_util.js'; import {Flags as flags} from '../../../src/commands/flags.js'; import * as version from '../../../version.js'; import * as constants from '../../../src/core/constants.js'; @@ -32,6 +38,25 @@ import path from 'path'; import {container} from 'tsyringe-neo'; import {resetTestContainer} from '../../test_container.js'; import * as test from 'node:test'; +import {ClusterCommandTasks} from '../../../src/commands/cluster/tasks.js'; +import type {BaseCommand} from '../../../src/commands/base.js'; +import {LocalConfig} from '../../../src/core/config/local_config.js'; +import type {CommandFlag} from '../../../src/types/flag_types.js'; +import {K8} from '../../../src/core/k8.js'; +import {type Cluster, KubeConfig} from '@kubernetes/client-node'; +import {RemoteConfigManager} from '../../../src/core/config/remote/remote_config_manager.js'; +import {DependencyManager} from '../../../src/core/dependency_managers/index.js'; +import {PackageDownloader} from '../../../src/core/package_downloader.js'; +import {KeyManager} from '../../../src/core/key_manager.js'; +import {AccountManager} from '../../../src/core/account_manager.js'; +import {PlatformInstaller} from '../../../src/core/platform_installer.js'; +import {ProfileManager} from '../../../src/core/profile_manager.js'; +import {LeaseManager} from '../../../src/core/lease/lease_manager.js'; +import {CertificateManager} from '../../../src/core/certificate_manager.js'; +import type {Opts} from '../../../src/types/command_types.js'; +import type {ListrTaskWrapper} from 'listr2'; +import fs from 'fs'; +import {stringify} from 'yaml'; const getBaseCommandOpts = () => ({ logger: sinon.stub(), @@ -81,7 +106,7 @@ describe('ClusterCommand unit tests', () => { it('Install function is called with expected parameters', async () => { const clusterCommand = new ClusterCommand(opts); - await clusterCommand.setup(argv); + await clusterCommand.handlers.setup(argv); expect(opts.chartManager.install.args[0][0]).to.equal(constants.SOLO_SETUP_NAMESPACE); expect(opts.chartManager.install.args[0][1]).to.equal(constants.SOLO_CLUSTER_SETUP_CHART); @@ -96,11 +121,326 @@ describe('ClusterCommand unit tests', () => { argv[flags.force.name] = true; const clusterCommand = new ClusterCommand(opts); - await clusterCommand.setup(argv); + await clusterCommand.handlers.setup(argv); expect(opts.chartManager.install.args[0][2]).to.equal( path.join(ROOT_DIR, 'test-directory', constants.SOLO_CLUSTER_SETUP_CHART), ); }); }); + + describe('cluster connect', () => { + const filePath = `${getTestCacheDir('ClusterCommandTasks')}/localConfig.yaml`; + const sandbox = sinon.createSandbox(); + let namespacePromptStub: sinon.SinonStub; + let clusterNamePromptStub: sinon.SinonStub; + let contextPromptStub: sinon.SinonStub; + let tasks: ClusterCommandTasks; + let command: BaseCommand; + let loggerStub: sinon.SinonStubbedInstance; + let localConfig: LocalConfig; + + const getBaseCommandOpts = ( + sandbox: sinon.SinonSandbox, + remoteConfig: any = {}, + // @ts-ignore + stubbedFlags: Record[] = [], + ) => { + const loggerStub = sandbox.createStubInstance(SoloLogger); + const k8Stub = sandbox.createStubInstance(K8); + k8Stub.getContexts.returns([ + {cluster: 'cluster-1', user: 'user-1', name: 'context-1', namespace: 'deployment-1'}, + {cluster: 'cluster-2', user: 'user-2', name: 'context-2', namespace: 'deployment-2'}, + {cluster: 'cluster-3', user: 'user-3', name: 'context-3', namespace: 'deployment-3'}, + ]); + const kubeConfigStub = sandbox.createStubInstance(KubeConfig); + kubeConfigStub.getCurrentContext.returns('context-from-kubeConfig'); + kubeConfigStub.getCurrentCluster.returns({ + name: 'cluster-3', + caData: 'caData', + caFile: 'caFile', + server: 'server-3', + skipTLSVerify: true, + tlsServerName: 'tls-3', + } as Cluster); + + const remoteConfigManagerStub = sandbox.createStubInstance(RemoteConfigManager); + remoteConfigManagerStub.modify.callsFake(async callback => { + await callback(remoteConfig); + }); + + k8Stub.getKubeConfig.returns(kubeConfigStub); + + const configManager = sandbox.createStubInstance(ConfigManager); + + for (let i = 0; i < stubbedFlags.length; i++) { + configManager.getFlag.withArgs(stubbedFlags[i][0]).returns(stubbedFlags[i][1]); + } + + return { + logger: loggerStub, + helm: sandbox.createStubInstance(Helm), + k8: k8Stub, + chartManager: sandbox.createStubInstance(ChartManager), + configManager, + depManager: sandbox.createStubInstance(DependencyManager), + localConfig: new LocalConfig(filePath), + downloader: sandbox.createStubInstance(PackageDownloader), + keyManager: sandbox.createStubInstance(KeyManager), + accountManager: sandbox.createStubInstance(AccountManager), + platformInstaller: sandbox.createStubInstance(PlatformInstaller), + profileManager: sandbox.createStubInstance(ProfileManager), + leaseManager: sandbox.createStubInstance(LeaseManager), + certificateManager: sandbox.createStubInstance(CertificateManager), + remoteConfigManager: remoteConfigManagerStub, + } as Opts; + }; + + describe('updateLocalConfig', () => { + async function runUpdateLocalConfigTask(opts) { + command = new ClusterCommand(opts); + tasks = new ClusterCommandTasks(command); + const taskObj = tasks.updateLocalConfig({}); + await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); + return command; + } + + afterEach(async () => { + await fs.promises.unlink(filePath); + sandbox.restore(); + }); + + after(() => {}); + + beforeEach(async () => { + namespacePromptStub = sandbox.stub(flags.namespace, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('deployment-3'); + }); + }); + clusterNamePromptStub = sandbox.stub(flags.clusterName, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('cluster-3'); + }); + }); + contextPromptStub = sandbox.stub(flags.context, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('context-3'); + }); + }); + loggerStub = sandbox.createStubInstance(SoloLogger); + await fs.promises.writeFile(filePath, stringify(testLocalConfigData)); + }); + + it('should update currentDeployment with clusters from remoteConfig', async () => { + const remoteConfig = { + clusters: { + 'cluster-2': 'deployment', + }, + }; + const opts = getBaseCommandOpts(sandbox, remoteConfig, []); + command = await runUpdateLocalConfigTask(opts); // @ts-ignore + localConfig = new LocalConfig(filePath); + + expect(localConfig.currentDeploymentName).to.equal('deployment'); + expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2']); + expect(localConfig.clusterContextMapping).to.deep.equal({ + 'cluster-1': 'context-1', + 'cluster-2': 'context-2', + }); + }); + + it('should update clusterContextMapping with provided context', async () => { + const remoteConfig = { + clusters: { + 'cluster-2': 'deployment', + }, + }; + const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.context, 'provided-context']]); + command = await runUpdateLocalConfigTask(opts); // @ts-ignore + localConfig = new LocalConfig(filePath); + + expect(localConfig.currentDeploymentName).to.equal('deployment'); + expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2']); + expect(localConfig.clusterContextMapping).to.deep.equal({ + 'cluster-1': 'context-1', + 'cluster-2': 'provided-context', + }); + }); + + it('should update multiple clusterContextMappings with provided contexts', async () => { + const remoteConfig = { + clusters: { + 'cluster-2': 'deployment', + 'cluster-3': 'deployment', + 'cluster-4': 'deployment', + }, + }; + const opts = getBaseCommandOpts(sandbox, remoteConfig, [ + [flags.context, 'provided-context-2,provided-context-3,provided-context-4'], + ]); + command = await runUpdateLocalConfigTask(opts); // @ts-ignore + localConfig = new LocalConfig(filePath); + + expect(localConfig.currentDeploymentName).to.equal('deployment'); + expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'cluster-3', 'cluster-4']); + expect(localConfig.clusterContextMapping).to.deep.equal({ + 'cluster-1': 'context-1', + 'cluster-2': 'provided-context-2', + 'cluster-3': 'provided-context-3', + 'cluster-4': 'provided-context-4', + }); + }); + + it('should update multiple clusterContextMappings with default KubeConfig context if quiet=true', async () => { + const remoteConfig = { + clusters: { + 'cluster-2': 'deployment', + 'cluster-3': 'deployment', + }, + }; + const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.quiet, true]]); + command = await runUpdateLocalConfigTask(opts); // @ts-ignore + localConfig = new LocalConfig(filePath); + + expect(localConfig.currentDeploymentName).to.equal('deployment'); + expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'cluster-3']); + expect(localConfig.clusterContextMapping).to.deep.equal({ + 'cluster-1': 'context-1', + 'cluster-2': 'context-2', + 'cluster-3': 'context-from-kubeConfig', + }); + }); + + it('should update multiple clusterContextMappings with prompted context no value was provided', async () => { + const remoteConfig = { + clusters: { + 'cluster-2': 'deployment', + 'new-cluster': 'deployment', + }, + }; + const opts = getBaseCommandOpts(sandbox, remoteConfig, []); + + command = await runUpdateLocalConfigTask(opts); // @ts-ignore + localConfig = new LocalConfig(filePath); + + expect(localConfig.currentDeploymentName).to.equal('deployment'); + expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'new-cluster']); + expect(localConfig.clusterContextMapping).to.deep.equal({ + 'cluster-1': 'context-1', + 'cluster-2': 'context-2', + 'new-cluster': 'context-3', // prompted value + }); + }); + }); + + describe('selectContext', () => { + async function runSelectContextTask(opts) { + command = new ClusterCommand(opts); + tasks = new ClusterCommandTasks(command); + const taskObj = tasks.selectContext({}); + await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); + return command; + } + + afterEach(async () => { + await fs.promises.unlink(filePath); + sandbox.restore(); + }); + + beforeEach(async () => { + namespacePromptStub = sandbox.stub(flags.namespace, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('deployment-3'); + }); + }); + clusterNamePromptStub = sandbox.stub(flags.clusterName, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('cluster-3'); + }); + }); + contextPromptStub = sandbox.stub(flags.context, 'prompt').callsFake(() => { + return new Promise(resolve => { + resolve('context-3'); + }); + }); + loggerStub = sandbox.createStubInstance(SoloLogger); + await fs.promises.writeFile(filePath, stringify(testLocalConfigData)); + }); + + it('should use first provided context', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [ + [flags.context, 'provided-context-1,provided-context-2,provided-context-3'], + ]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('provided-context-1'); + }); + + it('should use local config mapping to connect to first provided cluster', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-2,cluster-3']]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); + }); + + it('should prompt for context if selected cluster is not found in local config mapping', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-3']]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); + }); + + it('should use default kubeConfig context if selected cluster is not found in local config mapping and quiet=true', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [ + [flags.clusterName, 'unknown-cluster'], + [flags.quiet, true], + ]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); + }); + + it('should use context from local config mapping for the first cluster from the selected deployment', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-2']]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); + }); + + it('should prompt for context if selected deployment is found in local config but the context is not', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-3']]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); + }); + + it('should use default context if selected deployment is found in local config but the context is not and quiet=true', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [ + [flags.namespace, 'deployment-3'], + [flags.quiet, true], + ]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); + }); + + it('should prompt for clusters and contexts if selected deployment is not found in local config', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-4']]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); + }); + + it('should use clusters and contexts from kubeConfig if selected deployment is not found in local config and quiet=true', async () => { + const opts = getBaseCommandOpts(sandbox, {}, [ + [flags.namespace, 'deployment-4'], + [flags.quiet, true], + ]); + + command = await runSelectContextTask(opts); // @ts-ignore + expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); + }); + }); + }); }); diff --git a/test/unit/commands/context.test.ts b/test/unit/commands/context.test.ts deleted file mode 100644 index ceea764dc..000000000 --- a/test/unit/commands/context.test.ts +++ /dev/null @@ -1,361 +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. - * - */ -import sinon from 'sinon'; -import {describe, it, beforeEach} from 'mocha'; -import {expect} from 'chai'; - -import {ContextCommandTasks} from '../../../src/commands/context/tasks.js'; -import {DependencyManager} from '../../../src/core/dependency_managers/index.js'; -import {LocalConfig} from '../../../src/core/config/local_config.js'; -import {PackageDownloader} from '../../../src/core/package_downloader.js'; -import {KeyManager} from '../../../src/core/key_manager.js'; -import {AccountManager} from '../../../src/core/account_manager.js'; -import {PlatformInstaller} from '../../../src/core/platform_installer.js'; -import {ProfileManager} from '../../../src/core/profile_manager.js'; -import {LeaseManager} from '../../../src/core/lease/lease_manager.js'; -import {CertificateManager} from '../../../src/core/certificate_manager.js'; -import {RemoteConfigManager} from '../../../src/core/config/remote/remote_config_manager.js'; -import {K8} from '../../../src/core/k8.js'; -import {ConfigManager} from '../../../src/core/config_manager.js'; -import {Helm} from '../../../src/core/helm.js'; -import {ChartManager} from '../../../src/core/chart_manager.js'; -import {getTestCacheDir, testLocalConfigData} from '../../test_util.js'; -import {type BaseCommand} from '../../../src/commands/base.js'; -import {Flags as flags} from '../../../src/commands/flags.js'; -import {SoloLogger} from '../../../src/core/logging.js'; -import {type Opts} from '../../../src/types/command_types.js'; -import fs from 'fs'; -import {stringify} from 'yaml'; -import {type Cluster, KubeConfig} from '@kubernetes/client-node'; -import {type ListrTaskWrapper} from 'listr2'; -import {ContextCommand} from '../../../src/commands/context/index.js'; -import {type CommandFlag} from '../../../src/types/flag_types.js'; - -describe('ContextCommandTasks unit tests', () => { - const filePath = `${getTestCacheDir('ContextCommandTasks')}/localConfig.yaml`; - const sandbox = sinon.createSandbox(); - let namespacePromptStub: sinon.SinonStub; - let clusterNamePromptStub: sinon.SinonStub; - let contextPromptStub: sinon.SinonStub; - let tasks: ContextCommandTasks; - let command: BaseCommand; - let loggerStub: sinon.SinonStubbedInstance; - let localConfig: LocalConfig; - - const getBaseCommandOpts = ( - sandbox: sinon.SinonSandbox, - remoteConfig: any = {}, - // @ts-ignore - stubbedFlags: Record[] = [], - ) => { - const loggerStub = sandbox.createStubInstance(SoloLogger); - const k8Stub = sandbox.createStubInstance(K8); - k8Stub.getContexts.returns([ - {cluster: 'cluster-1', user: 'user-1', name: 'context-1', namespace: 'deployment-1'}, - {cluster: 'cluster-2', user: 'user-2', name: 'context-2', namespace: 'deployment-2'}, - {cluster: 'cluster-3', user: 'user-3', name: 'context-3', namespace: 'deployment-3'}, - ]); - const kubeConfigStub = sandbox.createStubInstance(KubeConfig); - kubeConfigStub.getCurrentContext.returns('context-from-kubeConfig'); - kubeConfigStub.getCurrentCluster.returns({ - name: 'cluster-3', - caData: 'caData', - caFile: 'caFile', - server: 'server-3', - skipTLSVerify: true, - tlsServerName: 'tls-3', - } as Cluster); - - const remoteConfigManagerStub = sandbox.createStubInstance(RemoteConfigManager); - remoteConfigManagerStub.modify.callsFake(async callback => { - await callback(remoteConfig); - }); - - k8Stub.getKubeConfig.returns(kubeConfigStub); - - const configManager = sandbox.createStubInstance(ConfigManager); - - for (let i = 0; i < stubbedFlags.length; i++) { - configManager.getFlag.withArgs(stubbedFlags[i][0]).returns(stubbedFlags[i][1]); - } - - return { - logger: loggerStub, - helm: sandbox.createStubInstance(Helm), - k8: k8Stub, - chartManager: sandbox.createStubInstance(ChartManager), - configManager, - depManager: sandbox.createStubInstance(DependencyManager), - localConfig: new LocalConfig(filePath), - downloader: sandbox.createStubInstance(PackageDownloader), - keyManager: sandbox.createStubInstance(KeyManager), - accountManager: sandbox.createStubInstance(AccountManager), - platformInstaller: sandbox.createStubInstance(PlatformInstaller), - profileManager: sandbox.createStubInstance(ProfileManager), - leaseManager: sandbox.createStubInstance(LeaseManager), - certificateManager: sandbox.createStubInstance(CertificateManager), - remoteConfigManager: remoteConfigManagerStub, - } as Opts; - }; - - describe('updateLocalConfig', () => { - async function runUpdateLocalConfigTask(opts) { - command = new ContextCommand(opts); - tasks = new ContextCommandTasks(command); - const taskObj = tasks.updateLocalConfig({}); - await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); - return command; - } - - afterEach(async () => { - await fs.promises.unlink(filePath); - sandbox.restore(); - }); - - after(() => {}); - - beforeEach(async () => { - namespacePromptStub = sandbox.stub(flags.namespace, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('deployment-3'); - }); - }); - clusterNamePromptStub = sandbox.stub(flags.clusterName, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('cluster-3'); - }); - }); - contextPromptStub = sandbox.stub(flags.context, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('context-3'); - }); - }); - loggerStub = sandbox.createStubInstance(SoloLogger); - await fs.promises.writeFile(filePath, stringify(testLocalConfigData)); - }); - - it('should update currentDeployment with clusters from remoteConfig', async () => { - const remoteConfig = { - clusters: { - 'cluster-2': 'deployment', - }, - }; - const opts = getBaseCommandOpts(sandbox, remoteConfig, []); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore - localConfig = new LocalConfig(filePath); - - expect(localConfig.currentDeploymentName).to.equal('deployment'); - expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2']); - expect(localConfig.clusterContextMapping).to.deep.equal({ - 'cluster-1': 'context-1', - 'cluster-2': 'context-2', - }); - }); - - it('should update clusterContextMapping with provided context', async () => { - const remoteConfig = { - clusters: { - 'cluster-2': 'deployment', - }, - }; - const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.context, 'provided-context']]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore - localConfig = new LocalConfig(filePath); - - expect(localConfig.currentDeploymentName).to.equal('deployment'); - expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2']); - expect(localConfig.clusterContextMapping).to.deep.equal({ - 'cluster-1': 'context-1', - 'cluster-2': 'provided-context', - }); - }); - - it('should update multiple clusterContextMappings with provided contexts', async () => { - const remoteConfig = { - clusters: { - 'cluster-2': 'deployment', - 'cluster-3': 'deployment', - 'cluster-4': 'deployment', - }, - }; - const opts = getBaseCommandOpts(sandbox, remoteConfig, [ - [flags.context, 'provided-context-2,provided-context-3,provided-context-4'], - ]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore - localConfig = new LocalConfig(filePath); - - expect(localConfig.currentDeploymentName).to.equal('deployment'); - expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'cluster-3', 'cluster-4']); - expect(localConfig.clusterContextMapping).to.deep.equal({ - 'cluster-1': 'context-1', - 'cluster-2': 'provided-context-2', - 'cluster-3': 'provided-context-3', - 'cluster-4': 'provided-context-4', - }); - }); - - it('should update multiple clusterContextMappings with default KubeConfig context if quiet=true', async () => { - const remoteConfig = { - clusters: { - 'cluster-2': 'deployment', - 'cluster-3': 'deployment', - }, - }; - const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.quiet, true]]); - command = await runUpdateLocalConfigTask(opts); // @ts-ignore - localConfig = new LocalConfig(filePath); - - expect(localConfig.currentDeploymentName).to.equal('deployment'); - expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'cluster-3']); - expect(localConfig.clusterContextMapping).to.deep.equal({ - 'cluster-1': 'context-1', - 'cluster-2': 'context-2', - 'cluster-3': 'context-from-kubeConfig', - }); - }); - - it('should update multiple clusterContextMappings with prompted context no value was provided', async () => { - const remoteConfig = { - clusters: { - 'cluster-2': 'deployment', - 'new-cluster': 'deployment', - }, - }; - const opts = getBaseCommandOpts(sandbox, remoteConfig, []); - - command = await runUpdateLocalConfigTask(opts); // @ts-ignore - localConfig = new LocalConfig(filePath); - - expect(localConfig.currentDeploymentName).to.equal('deployment'); - expect(localConfig.getCurrentDeployment().clusters).to.deep.equal(['cluster-2', 'new-cluster']); - expect(localConfig.clusterContextMapping).to.deep.equal({ - 'cluster-1': 'context-1', - 'cluster-2': 'context-2', - 'new-cluster': 'context-3', // prompted value - }); - }); - }); - - describe('selectContext', () => { - async function runSelectContextTask(opts) { - command = new ContextCommand(opts); - tasks = new ContextCommandTasks(command); - const taskObj = tasks.selectContext({}); - await taskObj.task({config: {}}, sandbox.stub() as unknown as ListrTaskWrapper); - return command; - } - - afterEach(async () => { - await fs.promises.unlink(filePath); - sandbox.restore(); - }); - - beforeEach(async () => { - namespacePromptStub = sandbox.stub(flags.namespace, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('deployment-3'); - }); - }); - clusterNamePromptStub = sandbox.stub(flags.clusterName, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('cluster-3'); - }); - }); - contextPromptStub = sandbox.stub(flags.context, 'prompt').callsFake(() => { - return new Promise(resolve => { - resolve('context-3'); - }); - }); - loggerStub = sandbox.createStubInstance(SoloLogger); - await fs.promises.writeFile(filePath, stringify(testLocalConfigData)); - }); - - it('should use first provided context', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [ - [flags.context, 'provided-context-1,provided-context-2,provided-context-3'], - ]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('provided-context-1'); - }); - - it('should use local config mapping to connect to first provided cluster', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-2,cluster-3']]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); - }); - - it('should prompt for context if selected cluster is not found in local config mapping', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [[flags.clusterName, 'cluster-3']]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); - }); - - it('should use default kubeConfig context if selected cluster is not found in local config mapping and quiet=true', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [ - [flags.clusterName, 'unknown-cluster'], - [flags.quiet, true], - ]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); - }); - - it('should use context from local config mapping for the first cluster from the selected deployment', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-2']]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-2'); - }); - - it('should prompt for context if selected deployment is found in local config but the context is not', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-3']]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); - }); - - it('should use default context if selected deployment is found in local config but the context is not and quiet=true', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [ - [flags.namespace, 'deployment-3'], - [flags.quiet, true], - ]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); - }); - - it('should prompt for clusters and contexts if selected deployment is not found in local config', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [[flags.namespace, 'deployment-4']]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-3'); - }); - - it('should use clusters and contexts from kubeConfig if selected deployment is not found in local config and quiet=true', async () => { - const opts = getBaseCommandOpts(sandbox, {}, [ - [flags.namespace, 'deployment-4'], - [flags.quiet, true], - ]); - - command = await runSelectContextTask(opts); // @ts-ignore - expect(command.getK8().getKubeConfig().setCurrentContext).to.have.been.calledWith('context-from-kubeConfig'); - }); - }); -});