UNPKG

@hashgraph/solo

Version:

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

546 lines 30 kB
/** * SPDX-License-Identifier: Apache-2.0 */ import sinon from 'sinon'; import { beforeEach, describe, it } from 'mocha'; import { expect } from 'chai'; 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'; import { ConfigManager } from '../../../src/core/config_manager.js'; import { SoloLogger } from '../../../src/core/logging.js'; import { ChartManager } from '../../../src/core/chart_manager.js'; import { Helm } from '../../../src/core/helm.js'; import { ROOT_DIR } from '../../../src/core/constants.js'; import path from 'path'; import { container } from 'tsyringe-neo'; import { resetForTest } from '../../test_container.js'; import { ClusterCommandTasks } from '../../../src/commands/cluster/tasks.js'; import { LocalConfig } from '../../../src/core/config/local_config.js'; import { K8Client } from '../../../src/core/kube/k8_client/k8_client.js'; import { K8ClientFactory } from '../../../src/core/kube/k8_client/k8_client_factory.js'; import { 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 fs from 'fs'; import { stringify } from 'yaml'; import { ErrorMessages } from '../../../src/core/error_messages.js'; import { NamespaceName } from '../../../src/core/kube/resources/namespace/namespace_name.js'; import { ClusterChecks } from '../../../src/core/cluster_checks.js'; import { K8ClientClusters } from '../../../src/core/kube/k8_client/resources/cluster/k8_client_clusters.js'; import { K8ClientContexts } from '../../../src/core/kube/k8_client/resources/context/k8_client_contexts.js'; import { InjectTokens } from '../../../src/core/dependency_injection/inject_tokens.js'; const getBaseCommandOpts = (context) => { const opts = { logger: sandbox.createStubInstance(SoloLogger), helm: sandbox.createStubInstance(Helm), k8Factory: sandbox.createStubInstance(K8ClientFactory), chartManager: sandbox.createStubInstance(ChartManager), configManager: sandbox.createStubInstance(ConfigManager), depManager: sandbox.createStubInstance(DependencyManager), localConfig: sandbox.createStubInstance(LocalConfig), }; opts.k8Factory.default.returns(new K8Client(context)); return opts; }; const testName = 'cluster-cmd-unit'; const namespace = NamespaceName.of(testName); const argv = getDefaultArgv(namespace); const sandbox = sinon.createSandbox(); argv[flags.namespace.name] = namespace.name; argv[flags.deployment.name] = `${namespace.name}-deployment`; argv[flags.releaseTag.name] = HEDERA_PLATFORM_VERSION_TAG; argv[flags.nodeAliasesUnparsed.name] = 'node1'; argv[flags.generateGossipKeys.name] = true; argv[flags.generateTlsKeys.name] = true; argv[flags.clusterRef.name] = TEST_CLUSTER; argv[flags.soloChartVersion.name] = version.SOLO_CHART_VERSION; argv[flags.force.name] = true; argv[flags.clusterSetupNamespace.name] = constants.SOLO_SETUP_NAMESPACE.name; describe('ClusterCommand unit tests', () => { before(() => { resetForTest(namespace.name); }); describe('Chart Install Function is called correctly', () => { let opts; afterEach(() => { sandbox.restore(); }); beforeEach(() => { const k8Client = new K8Client(undefined); const context = k8Client.contexts().readCurrent(); opts = getBaseCommandOpts(context); opts.logger = container.resolve(InjectTokens.SoloLogger); opts.helm = container.resolve(InjectTokens.Helm); opts.chartManager = container.resolve(InjectTokens.ChartManager); opts.helm.dependency = sandbox.stub(); opts.chartManager.isChartInstalled = sandbox.stub().returns(false); opts.chartManager.install = sandbox.stub().returns(true); opts.configManager = container.resolve(InjectTokens.ConfigManager); opts.remoteConfigManager = sandbox.stub(); opts.remoteConfigManager.currentCluster = 'solo-e2e'; opts.localConfig.clusterRefs = { 'solo-e2e': 'context-1' }; }); it('Install function is called with expected parameters', async () => { const clusterCommand = new ClusterCommand(opts); await clusterCommand.handlers.setup(argv); expect(opts.chartManager.install.args[0][0].name).to.equal(constants.SOLO_SETUP_NAMESPACE.name); expect(opts.chartManager.install.args[0][1]).to.equal(constants.SOLO_CLUSTER_SETUP_CHART); expect(opts.chartManager.install.args[0][2]).to.equal(constants.SOLO_TESTING_CHART_URL + '/' + constants.SOLO_CLUSTER_SETUP_CHART); expect(opts.chartManager.install.args[0][3]).to.equal(version.SOLO_CHART_VERSION); }); it('Should use local chart directory', async () => { argv[flags.chartDirectory.name] = 'test-directory'; argv[flags.force.name] = true; const clusterCommand = new ClusterCommand(opts); 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; let clusterNamePromptStub; let deploymentPromptStub; let contextPromptStub; let tasks; let command; let loggerStub; let k8FactoryStub; let remoteConfigManagerStub; let localConfig; const defaultRemoteConfig = { metadata: { namespace: 'solo-e2e', }, clusters: {}, }; const getBaseCommandOpts = (sandbox, remoteConfig = {}, // @ts-expect-error - TS2344: Type CommandFlag does not satisfy the constraint string | number | symbol stubbedFlags = [], opts = { testContextConnectionError: false, }) => { const loggerStub = sandbox.createStubInstance(SoloLogger); k8FactoryStub = sandbox.createStubInstance(K8ClientFactory); const k8Stub = sandbox.createStubInstance(K8Client); k8FactoryStub.default.returns(k8Stub); const k8ContextsStub = sandbox.createStubInstance(K8ClientContexts); k8ContextsStub.list.returns(['context-1', 'context-2', 'context-3']); k8Stub.contexts.returns(k8ContextsStub); const clusterChecksStub = sandbox.createStubInstance(ClusterChecks); clusterChecksStub.isMinioInstalled.returns(new Promise(() => true)); clusterChecksStub.isPrometheusInstalled.returns(new Promise(() => true)); clusterChecksStub.isCertManagerInstalled.returns(new Promise(() => true)); if (opts.testContextConnectionError) { k8ContextsStub.testContextConnection.resolves(false); } else { k8ContextsStub.testContextConnection.resolves(true); } const kubeConfigClusterObject = { name: 'cluster-3', caData: 'caData', caFile: 'caFile', server: 'server-3', skipTLSVerify: true, tlsServerName: 'tls-3', }; const kubeConfigStub = sandbox.createStubInstance(KubeConfig); kubeConfigStub.getCurrentContext.returns('context-from-kubeConfig'); kubeConfigStub.getCurrentCluster.returns(kubeConfigClusterObject); remoteConfigManagerStub = sandbox.createStubInstance(RemoteConfigManager); remoteConfigManagerStub.modify.callsFake(async (callback) => { await callback(remoteConfig); }); remoteConfigManagerStub.get.resolves(remoteConfig); const k8ClustersStub = sandbox.createStubInstance(K8ClientClusters); k8ClustersStub.readCurrent.returns(kubeConfigClusterObject.name); k8Stub.clusters.returns(k8ClustersStub); k8ContextsStub.readCurrent.returns('context-from-kubeConfig'); 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), k8Factory: k8FactoryStub, 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, }; }; describe('updateLocalConfig', () => { async function runUpdateLocalConfigTask(opts) { command = new ClusterCommand(opts); tasks = new ClusterCommandTasks(command, opts.k8Factory); // @ts-expect-error - TS2554: Expected 0 arguments, but got 1. const taskObj = tasks.updateLocalConfig({}); await taskObj.task({ config: {} }, sandbox.stub()); 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'); }); }); deploymentPromptStub = sandbox.stub(flags.deployment, 'prompt').callsFake(() => { return new Promise(resolve => { resolve('deployment-3'); }); }); clusterNamePromptStub = sandbox.stub(flags.clusterRef, '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 = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-2': 'solo-e2e', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.clusterRefs).to.deep.equal({ 'cluster-1': 'context-1', 'cluster-2': 'context-2', }); }); xit('should update clusterRefs with provided context', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-2': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.context, 'provided-context']]); command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.clusterRefs).to.deep.equal({ 'cluster-1': 'context-1', 'cluster-2': 'provided-context', }); }); xit('should update multiple clusterRefss with provided contexts', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { 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); localConfig = new LocalConfig(filePath); expect(localConfig.clusterRefs).to.deep.equal({ 'cluster-1': 'context-1', 'cluster-2': 'provided-context-2', 'cluster-3': 'provided-context-3', 'cluster-4': 'provided-context-4', }); }); xit('should update multiple clusterRefss with default KubeConfig context if quiet=true', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-2': 'deployment', 'cluster-3': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, [[flags.quiet, true]]); command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.clusterRefs).to.deep.equal({ 'cluster-1': 'context-1', 'cluster-2': 'context-2', 'cluster-3': 'context-from-kubeConfig', }); }); xit('should update multiple clusterRefss with prompted context no value was provided', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-2': 'deployment', 'new-cluster': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); command = await runUpdateLocalConfigTask(opts); localConfig = new LocalConfig(filePath); expect(localConfig.clusterRefs).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, opts.k8Factory); // @ts-expect-error - TS2554: Expected 0 arguments, but got 1 const taskObj = tasks.selectContext({}); await taskObj.task({ config: {} }, sandbox.stub()); 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.clusterRef, '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.getK8Factory().default().contexts().updateCurrent).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.clusterRef, 'cluster-2,cluster-3']]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.clusterRef, 'cluster-3']]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.clusterRef, 'unknown-cluster'], [flags.quiet, true], ]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.deployment, 'deployment-2']]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.deployment, 'deployment-3']]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.deployment, 'deployment-3'], [flags.quiet, true], ]); command = await runSelectContextTask(opts); // @ts-ignore expect(command.getK8Factory().default().contexts().updateCurrent).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.deployment, 'deployment-4']]); command = await runSelectContextTask(opts); expect(command.getK8Factory().default().contexts().updateCurrent).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.deployment, 'deployment-4'], [flags.quiet, true], ]); command = await runSelectContextTask(opts); expect(command.getK8Factory().default().contexts().updateCurrent).to.have.been.calledWith('context-from-kubeConfig'); }); it('throws error when context is invalid', async () => { const opts = getBaseCommandOpts(sandbox, {}, [[flags.context, 'invalid-context']], { testContextConnectionError: true, }); try { await runSelectContextTask(opts); expect(true).to.be.false; } catch (e) { expect(e.message).to.eq(ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER('invalid-context')); } }); }); describe('readClustersFromRemoteConfig', () => { let taskStub; async function runReadClustersFromRemoteConfigTask(opts) { command = new ClusterCommand(opts); tasks = new ClusterCommandTasks(command, k8FactoryStub); const taskObj = tasks.readClustersFromRemoteConfig({}); taskStub = sandbox.stub(); taskStub.newListr = sandbox.stub(); await taskObj.task({ config: {} }, taskStub); return command; } async function runSubTasks(subTasks) { const stubs = []; for (const subTask of subTasks) { const subTaskStub = sandbox.stub(); subTaskStub.newListr = sandbox.stub(); await subTask.task({ config: {} }, subTaskStub); stubs.push(subTaskStub); } return stubs; } afterEach(async () => { await fs.promises.unlink(filePath); sandbox.restore(); }); beforeEach(async () => { contextPromptStub = sandbox.stub(flags.context, 'prompt').callsFake(() => { return new Promise(resolve => { resolve('prompted-context'); }); }); loggerStub = sandbox.createStubInstance(SoloLogger); await fs.promises.writeFile(filePath, stringify(testLocalConfigData)); }); it('should load RemoteConfig when there is only 1 cluster', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-3': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); command = await runReadClustersFromRemoteConfigTask(opts); expect(taskStub.newListr).calledWith([]); }); it('should test other clusters and pull their respective RemoteConfigs', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-2': 'deployment', 'cluster-3': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); command = await runReadClustersFromRemoteConfigTask(opts); expect(taskStub.newListr).calledOnce; const subTasks = taskStub.newListr.firstCall.firstArg; expect(subTasks.length).to.eq(2); await runSubTasks(subTasks); expect(contextPromptStub).not.called; expect(command.getK8Factory().default().contexts().updateCurrent).to.have.been.calledWith('context-2'); expect(command.getK8Factory().default().contexts().testContextConnection).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledWith('context-2'); }); it('should prompt for context when reading unknown cluster', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-3': 'deployment', 'cluster-4': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); command = await runReadClustersFromRemoteConfigTask(opts); expect(taskStub.newListr).calledOnce; const subTasks = taskStub.newListr.firstCall.firstArg; expect(subTasks.length).to.eq(2); await runSubTasks(subTasks); expect(contextPromptStub).calledOnce; expect(command.getK8Factory().default().contexts().updateCurrent).to.have.been.calledWith('prompted-context'); expect(command.getK8Factory().default().contexts().testContextConnection).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledWith('prompted-context'); }); it('should throw error for invalid prompted context', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-3': 'deployment', 'cluster-4': 'deployment', }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, [], { testContextConnectionError: true }); command = await runReadClustersFromRemoteConfigTask(opts); expect(taskStub.newListr).calledOnce; const subTasks = taskStub.newListr.firstCall.firstArg; expect(subTasks.length).to.eq(2); try { await runSubTasks(subTasks); expect(true).to.be.false; } catch (e) { expect(e.message).to.eq(ErrorMessages.INVALID_CONTEXT_FOR_CLUSTER_DETAILED('prompted-context', 'cluster-4')); expect(contextPromptStub).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledWith('prompted-context'); } }); it('should throw error when remoteConfigs do not match', async () => { const remoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-3': 'deployment', 'cluster-4': 'deployment', }, }); const mismatchedRemoteConfig = Object.assign({}, defaultRemoteConfig, { clusters: { 'cluster-3': 'deployment' }, }); const opts = getBaseCommandOpts(sandbox, remoteConfig, []); remoteConfigManagerStub.get.onCall(0).resolves(remoteConfig); remoteConfigManagerStub.get.onCall(1).resolves(mismatchedRemoteConfig); command = await runReadClustersFromRemoteConfigTask(opts); expect(taskStub.newListr).calledOnce; const subTasks = taskStub.newListr.firstCall.firstArg; expect(subTasks.length).to.eq(2); try { await runSubTasks(subTasks); expect(true).to.be.false; } catch (e) { expect(e.message).to.eq(ErrorMessages.REMOTE_CONFIGS_DO_NOT_MATCH('cluster-3', 'cluster-4')); expect(contextPromptStub).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledOnce; expect(command.getK8Factory().default().contexts().testContextConnection).calledWith('prompted-context'); } }); }); }); }); //# sourceMappingURL=cluster.test.js.map