UNPKG

@hashgraph/solo

Version:

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

300 lines 18.1 kB
// SPDX-License-Identifier: Apache-2.0 import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import fs from 'node:fs'; import * as yaml from 'yaml'; import { Flags as flags } from '../../../src/commands/flags.js'; import { ProfileManager } from '../../../src/core/profile-manager.js'; import { getTemporaryDirectory, getTestCacheDirectory } from '../../test-utility.js'; import * as version from '../../../version.js'; import { container } from 'tsyringe-neo'; import { resetForTest } from '../../test-container.js'; import { Templates } from '../../../src/core/templates.js'; import { NamespaceName } from '../../../src/types/namespace/namespace-name.js'; import { InjectTokens } from '../../../src/core/dependency-injection/inject-tokens.js'; import { KubeConfig } from '@kubernetes/client-node'; import sinon from 'sinon'; import { PathEx } from '../../../src/business/utils/path-ex.js'; import * as constants from '../../../src/core/constants.js'; import { Address } from '../../../src/business/address/address.js'; function invokeExtractSavedEndpoint(manager, consensusNode, nodeSequence) { const extractSavedEndpoint = manager.extractSavedEndpoint; return extractSavedEndpoint.call(manager, consensusNode, nodeSequence); } describe('ProfileManager', () => { let temporaryDirectory, configManager, profileManager, cacheDirectory; const namespace = NamespaceName.of('test-namespace'); const deploymentName = 'deployment'; const kubeConfig = new KubeConfig(); kubeConfig.loadFromDefault(); const consensusNodes = [ { name: 'node1', nodeId: 1, namespace: namespace.name, cluster: kubeConfig.getCurrentCluster().name, context: kubeConfig.getCurrentContext(), dnsBaseDomain: 'cluster.local', dnsConsensusNodePattern: 'network-{nodeAlias}-svc.{namespace}.svc', fullyQualifiedDomainName: 'network-node1-svc.test-namespace.svc.cluster.local', blockNodeMap: [], externalBlockNodeMap: [], }, { name: 'node2', nodeId: 2, namespace: namespace.name, cluster: kubeConfig.getCurrentCluster().name, context: kubeConfig.getCurrentContext(), dnsBaseDomain: 'cluster.local', dnsConsensusNodePattern: 'network-{nodeAlias}-svc.{namespace}.svc', fullyQualifiedDomainName: 'network-node2-svc.test-namespace.svc.cluster.local', blockNodeMap: [], externalBlockNodeMap: [], }, { name: 'node3', nodeId: 3, namespace: namespace.name, cluster: kubeConfig.getCurrentCluster().name, context: kubeConfig.getCurrentContext(), dnsBaseDomain: 'cluster.local', dnsConsensusNodePattern: 'network-{nodeAlias}-svc.{namespace}.svc', fullyQualifiedDomainName: 'network-node3-svc.test-namespace.svc.cluster.local', blockNodeMap: [], externalBlockNodeMap: [], }, ]; let stagingDirectory = ''; before(async () => { resetForTest(namespace.name); temporaryDirectory = getTemporaryDirectory(); configManager = container.resolve(InjectTokens.ConfigManager); profileManager = new ProfileManager(undefined, undefined, temporaryDirectory); configManager.setFlag(flags.nodeAliasesUnparsed, 'node1,node2,node4'); configManager.setFlag(flags.cacheDir, getTestCacheDirectory('ProfileManager')); configManager.setFlag(flags.releaseTag, version.HEDERA_PLATFORM_VERSION); cacheDirectory = configManager.getFlag(flags.cacheDir); configManager.setFlag(flags.apiPermissionProperties, flags.apiPermissionProperties.definition.defaultValue); configManager.setFlag(flags.applicationEnv, flags.applicationEnv.definition.defaultValue); configManager.setFlag(flags.applicationProperties, flags.applicationProperties.definition.defaultValue); configManager.setFlag(flags.bootstrapProperties, flags.bootstrapProperties.definition.defaultValue); configManager.setFlag(flags.log4j2Xml, flags.log4j2Xml.definition.defaultValue); configManager.setFlag(flags.settingTxt, flags.settingTxt.definition.defaultValue); stagingDirectory = Templates.renderStagingDir(configManager.getFlag(flags.cacheDir), configManager.getFlag(flags.releaseTag)); if (!fs.existsSync(stagingDirectory)) { fs.mkdirSync(stagingDirectory, { recursive: true }); } // @ts-expect-error - TS2339: to mock profileManager.remoteConfig.getConsensusNodes = sinon.stub().returns(consensusNodes); // @ts-expect-error - TS2339: to mock profileManager.remoteConfig.configuration = { // @ts-expect-error - TS2339: to mock state: {}, versions: { // @ts-expect-error - TS2339: to mock consensusNode: version.HEDERA_PLATFORM_VERSION, }, }; // @ts-expect-error - TS2339: to mock profileManager.updateApplicationPropertiesForBlockNode = sinon.stub(); const localConfig = container.resolve(InjectTokens.LocalConfigRuntimeState); await localConfig.load(); }); after(() => { fs.rmSync(temporaryDirectory, { recursive: true }); }); describe('determine chart values', () => { it('should determine Solo chart values', async () => { configManager.setFlag(flags.namespace, 'test-namespace'); const resources = ['templates']; for (const directoryName of resources) { const sourceDirectory = PathEx.joinWithRealPath(PathEx.join('resources'), directoryName); if (!fs.existsSync(sourceDirectory)) { continue; } const destinationDirectory = PathEx.resolve(PathEx.join(cacheDirectory, directoryName)); if (!fs.existsSync(destinationDirectory)) { fs.mkdirSync(destinationDirectory, { recursive: true }); } fs.cpSync(sourceDirectory, destinationDirectory, { recursive: true }); } const applicationPropertiesFile = PathEx.join(cacheDirectory, 'templates', constants.APPLICATION_PROPERTIES); const valuesFileMapping = await profileManager.prepareValuesForSoloChart(consensusNodes, deploymentName, applicationPropertiesFile); const valuesFile = Object.values(valuesFileMapping)[0]; expect(valuesFile).not.to.be.null; expect(fs.existsSync(valuesFile)).to.be.ok; // validate the yaml const valuesYaml = yaml.parse(fs.readFileSync(valuesFile, 'utf8')); expect(valuesYaml.hedera.nodes.length).to.equal(3); }); it('prepareValuesForSoloChart should set the value of a key to the contents of a file', async () => { configManager.setFlag(flags.namespace, 'test-namespace'); const file = PathEx.join(temporaryDirectory, 'application.env'); const fileContents = '# row 1\n# row 2\n# row 3'; fs.writeFileSync(file, fileContents); configManager.setFlag(flags.applicationEnv, file); const destinationFile = PathEx.join(stagingDirectory, 'templates', 'application.env'); const applicationPropertiesFile = PathEx.join(stagingDirectory, 'templates', constants.APPLICATION_PROPERTIES); fs.cpSync(file, destinationFile, { force: true }); const cachedValuesFileMapping = await profileManager.prepareValuesForSoloChart(consensusNodes, deploymentName, applicationPropertiesFile); const cachedValuesFile = Object.values(cachedValuesFileMapping)[0]; const valuesYaml = yaml.parse(fs.readFileSync(cachedValuesFile, 'utf8')); expect(valuesYaml.hedera.configMaps.applicationEnv).to.equal(fileContents); }); }); describe('prepareConfigText', () => { it('should write and return the path to the config.txt file', async () => { const destinationPath = PathEx.join(temporaryDirectory, 'staging'); fs.mkdirSync(destinationPath, { recursive: true }); }); }); describe('saved endpoint extraction', () => { afterEach(() => { sinon.restore(); }); it('reuses saved domainName endpoint from network.json', async () => { const savedDomainName = 'network-node1-svc.test-namespace.svc.cluster.local'; const networkJsonContent = JSON.stringify({ nodeMetadata: [{ rosterEntry: { gossipEndpoint: [{ port: 50_211, domainName: savedDomainName }] } }], }); const getK8Stub = sinon.stub().returns({ pods: () => ({ list: async () => [{ podReference: {} }], }), containers: () => ({ readByRef: () => ({ execContainer: async () => networkJsonContent, }), }), }); sinon .stub(profileManager.k8Factory, 'getK8') .callsFake(getK8Stub); const savedAddress = await invokeExtractSavedEndpoint(profileManager, consensusNodes[0], 0); expect(savedAddress).to.not.be.undefined; expect(savedAddress?.hostString()).to.equal(savedDomainName); expect(savedAddress?.port).to.equal(50_211); }); it('decodes saved ipAddressV4 and validates it against the expected node service', async () => { const savedIpAddress = '10.1.2.3'; const encodedIpAddress = Buffer.from([10, 1, 2, 3]).toString('base64'); const networkJsonContent = JSON.stringify({ nodeMetadata: [{ rosterEntry: { gossipEndpoint: [{ port: 50_211, ipAddressV4: encodedIpAddress }] } }], }); const serviceReadStub = sinon.stub().resolves({ spec: { clusterIP: savedIpAddress } }); const getK8Stub = sinon.stub().returns({ pods: () => ({ list: async () => [{ podReference: {} }], }), containers: () => ({ readByRef: () => ({ execContainer: async () => networkJsonContent, }), }), services: () => ({ read: serviceReadStub, }), }); sinon .stub(profileManager.k8Factory, 'getK8') .callsFake(getK8Stub); const savedAddress = await invokeExtractSavedEndpoint(profileManager, consensusNodes[0], 0); expect(savedAddress).to.not.be.undefined; expect(savedAddress?.hostString()).to.equal(savedIpAddress); expect(serviceReadStub.calledOnce).to.equal(true); expect(serviceReadStub.firstCall.args[1]).to.equal('network-node1-svc'); }); it('falls back to current external address when saved endpoint is not reusable', async () => { const destinationPath = PathEx.join(temporaryDirectory, 'config-fallback'); fs.mkdirSync(destinationPath, { recursive: true }); const extractSavedEndpointStub = sinon .stub(profileManager, 'extractSavedEndpoint') .resolves(); const externalAddressStub = sinon .stub(Address, 'getExternalAddress') .resolves(new Address(50_211, 'fallback-node1.test')); const nodeAccountMap = new Map([[consensusNodes[0].name, '0.0.3']]); const configTxtPath = await profileManager.prepareConfigTxt(nodeAccountMap, [consensusNodes[0]], destinationPath, version.HEDERA_PLATFORM_VERSION, constants.HEDERA_APP_NAME, constants.HEDERA_CHAIN_ID, false); expect(extractSavedEndpointStub.calledOnce).to.equal(true); expect(externalAddressStub.calledOnce).to.equal(true); expect(externalAddressStub.firstCall.args[3]).to.equal(false); expect(fs.readFileSync(configTxtPath, 'utf8')).to.contain('fallback-node1.test, 50211'); }); }); describe('chainId updates', () => { it('should update contracts.chainId in application.properties', async () => { const applicationPropertiesPath = PathEx.join(temporaryDirectory, constants.APPLICATION_PROPERTIES); fs.writeFileSync(applicationPropertiesPath, ['hedera.realm=0', 'contracts.chainId=295', 'hedera.shard=0'].join('\n') + '\n', 'utf8'); // @ts-expect-error to access private method await profileManager.updateApplicationPropertiesWithChainId(applicationPropertiesPath, '296'); const updated = fs.readFileSync(applicationPropertiesPath, 'utf8'); expect(updated).to.contain('contracts.chainId=296'); expect(updated).not.to.contain('contracts.chainId=295'); }); it('should update contracts.chainId in bootstrap.properties', async () => { const bootstrapPropertiesPath = PathEx.join(temporaryDirectory, 'bootstrap.properties'); fs.writeFileSync(bootstrapPropertiesPath, ['foo=bar', 'contracts.chainId=295', 'baz=qux'].join('\n') + '\n', 'utf8'); // @ts-expect-error to access private method await profileManager.updateBoostrapPropertiesWithChainId(bootstrapPropertiesPath, '296'); const updated = fs.readFileSync(bootstrapPropertiesPath, 'utf8'); expect(updated).to.contain('contracts.chainId=296'); expect(updated).not.to.contain('contracts.chainId=295'); }); it('prepareStagingDirectory should update chainId in staged application.properties and bootstrap.properties', async () => { const yamlRoot = {}; const nodeAliases = ['node1', 'node2', 'node3']; const sourceDirectory = PathEx.join(temporaryDirectory, 'source-files'); fs.mkdirSync(sourceDirectory, { recursive: true }); const applicationPropertiesSourcePath = PathEx.join(sourceDirectory, constants.APPLICATION_PROPERTIES); const bootstrapPropertiesSourcePath = PathEx.join(sourceDirectory, 'bootstrap.properties'); // eslint-disable-next-line unicorn/prevent-abbreviations const applicationEnvSourcePath = PathEx.join(sourceDirectory, 'application.env'); const apiPermissionSourcePath = PathEx.join(sourceDirectory, 'api-permission.properties'); // eslint-disable-next-line unicorn/prevent-abbreviations const log4j2SourcePath = PathEx.join(sourceDirectory, 'log4j2.xml'); const settingsSourcePath = PathEx.join(sourceDirectory, 'settings.txt'); fs.writeFileSync(applicationPropertiesSourcePath, ['hedera.realm=0', 'hedera.shard=0', 'contracts.chainId=295'].join('\n') + '\n', 'utf8'); fs.writeFileSync(bootstrapPropertiesSourcePath, ['contracts.chainId=295', 'some.other.value=true'].join('\n') + '\n', 'utf8'); fs.writeFileSync(applicationEnvSourcePath, 'ENV_ONE=value1\n', 'utf8'); fs.writeFileSync(apiPermissionSourcePath, 'dummy.permission=true\n', 'utf8'); fs.writeFileSync(log4j2SourcePath, '<Configuration />\n', 'utf8'); fs.writeFileSync(settingsSourcePath, 'swirld, 123\n', 'utf8'); configManager.setFlag(flags.applicationProperties, applicationPropertiesSourcePath); configManager.setFlag(flags.bootstrapProperties, bootstrapPropertiesSourcePath); configManager.setFlag(flags.applicationEnv, applicationEnvSourcePath); configManager.setFlag(flags.apiPermissionProperties, apiPermissionSourcePath); configManager.setFlag(flags.log4j2Xml, log4j2SourcePath); configManager.setFlag(flags.settingTxt, settingsSourcePath); configManager.setFlag(flags.chainId, '296'); // @ts-expect-error to access private property sinon.stub(profileManager.accountManager, 'getNodeAccountMap').returns(new Map([ ['node1', '0.0.3'], ['node2', '0.0.4'], ['node3', '0.0.5'], ])); // @ts-expect-error to access private property sinon.stub(profileManager.localConfig.configuration, 'realmForDeployment').returns(0); // @ts-expect-error to access private property sinon.stub(profileManager.localConfig.configuration, 'shardForDeployment').returns(0); await profileManager.prepareStagingDirectory(consensusNodes, nodeAliases, yamlRoot, deploymentName, applicationPropertiesSourcePath, { cacheDir: cacheDirectory, releaseTag: version.HEDERA_PLATFORM_VERSION, appName: 'HederaNode.jar', chainId: '296', }); const stagedApplicationPropertiesPath = PathEx.join(stagingDirectory, 'templates', constants.APPLICATION_PROPERTIES); const stagedBootstrapPropertiesPath = PathEx.join(stagingDirectory, 'templates', 'bootstrap.properties'); const stagedApplicationProperties = fs.readFileSync(stagedApplicationPropertiesPath, 'utf8'); const stagedBootstrapProperties = fs.readFileSync(stagedBootstrapPropertiesPath, 'utf8'); expect(stagedApplicationProperties).to.contain('contracts.chainId=296'); expect(stagedApplicationProperties).not.to.contain('contracts.chainId=295'); expect(stagedBootstrapProperties).to.contain('contracts.chainId=296'); expect(stagedBootstrapProperties).not.to.contain('contracts.chainId=295'); expect(yamlRoot.hedera.configMaps.applicationProperties).to.contain('contracts.chainId=296'); expect(yamlRoot.hedera.configMaps.bootstrapProperties).to.contain('contracts.chainId=296'); sinon.restore(); }); }); }); //# sourceMappingURL=profile-manager.test.js.map