@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
421 lines • 21.5 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { HelmExecutionException } from '../../../../../src/integration/helm/helm-execution-exception.js';
import { Chart } from '../../../../../src/integration/helm/model/chart.js';
import { Repository } from '../../../../../src/integration/helm/model/repository.js';
import { DefaultHelmClientBuilder } from '../../../../../src/integration/helm/impl/default-helm-client-builder.js';
import { UpgradeChartOptionsBuilder } from '../../../../../src/integration/helm/model/upgrade/upgrade-chart-options-builder.js';
import { exec as execCallback } from 'node:child_process';
import { promisify } from 'node:util';
import { InstallChartOptionsBuilder } from '../../../../../src/integration/helm/model/install/install-chart-options-builder.js';
import { UnInstallChartOptionsBuilder } from '../../../../../src/integration/helm/model/install/un-install-chart-options-builder.js';
import { TestChartOptionsBuilder } from '../../../../../src/integration/helm/model/test/test-chart-options-builder.js';
import { InjectTokens } from '../../../../../src/core/dependency-injection/inject-tokens.js';
import { container } from 'tsyringe-neo';
import { Duration } from '../../../../../src/core/time/duration.js';
import { AddRepoOptionsBuilder } from '../../../../../src/integration/helm/model/add/add-repo-options-builder.js';
import * as constants from '../../../../../src/core/constants.js';
import path from 'node:path';
import { resetForTest } from '../../../../test-container.js';
import { getTemporaryDirectory } from '../../../../test-utility.js';
import { SemanticVersion } from '../../../../../src/business/utils/semantic-version.js';
const exec = promisify(execCallback);
describe('HelmClient Tests', () => {
const TEST_CHARTS_DIR = '/Users/jeffrey/solo-charts/charts/solo-deployment';
const NONEXISTENT_CHARTS_DIR = 'test/unit/core/helm/nonexistent-charts';
const HAPROXYTECH_REPOSITORY = new Repository('haproxytech', 'https://haproxytech.github.io/helm-charts');
const HAPROXY_CHART = new Chart('haproxy', 'haproxytech');
const HAPROXY_RELEASE_NAME = 'haproxy-release';
const INCUBATOR_REPOSITORY = new Repository('incubator', 'https://charts.helm.sh/incubator');
const JETSTACK_REPOSITORY = new Repository('jetstack', 'https://charts.jetstack.io');
const NAMESPACE = 'helm-client-test-ns';
const INSTALL_TIMEOUT = 30;
let helmClient;
before(async function () {
this.timeout(Duration.ofMinutes(3).toMillis());
resetForTest();
const helmDependencyManager = container.resolve(InjectTokens.HelmDependencyManager);
const kubectlDependencyManager = container.resolve(InjectTokens.KubectlDependencyManager);
try {
await helmDependencyManager.install(getTemporaryDirectory());
await kubectlDependencyManager.install(getTemporaryDirectory());
console.log(`Creating namespace ${NAMESPACE}...`);
await exec(`kubectl create namespace ${NAMESPACE}`, {
env: { ...process.env, PATH: `${constants.SOLO_HOME_DIR}/bin${path.delimiter}${process.env.PATH}` },
});
console.log(`Namespace ${NAMESPACE} created successfully`);
// Initialize helm client
helmClient = await new DefaultHelmClientBuilder()
.defaultNamespace(NAMESPACE)
.workingDirectory(process.cwd())
.build();
expect(helmClient).to.not.be.null;
}
catch (error) {
console.error('Error during setup:', error);
throw error;
}
});
after(async function () {
this.timeout(Duration.ofMinutes(2).toMillis()); // 2 minutes timeout for cleanup
try {
console.log(`Deleting namespace ${NAMESPACE}...`);
await exec(`kubectl delete namespace ${NAMESPACE}`, {
env: { ...process.env, PATH: `${constants.SOLO_HOME_DIR}/bin${path.delimiter}${process.env.PATH}` },
});
console.log(`Namespace ${NAMESPACE} deleted successfully`);
}
catch (error) {
console.error('Error during cleanup:', error);
// Don't throw the error during cleanup to not mask test failures
}
});
const removeRepoIfPresent = async (client, repo) => {
const repositories = await client.listRepositories();
if (repositories.some((r) => r.name === repo.name)) {
await client.removeRepository(repo);
}
};
const addRepoIfMissing = async (client, repo) => {
const repositories = await client.listRepositories();
if (!repositories.some((r) => r.name === repo.name)) {
await client.addRepository(repo);
}
};
it('Version Command Executes Successfully', async () => {
const helmVersion = await helmClient.version();
expect(helmVersion).to.not.be.null;
expect(helmVersion).to.not.equal(SemanticVersion.ZERO);
expect(helmVersion.major).to.be.greaterThanOrEqual(3);
expect(helmVersion.minor).to.be.greaterThanOrEqual(12);
expect(helmVersion.patch).to.not.be.lessThan(0);
});
it('Repository List Executes Successfully', async () => {
const repositories = await helmClient.listRepositories();
expect(repositories).to.not.be.null;
});
it('Repository Add Executes Successfully', async () => {
const originalRepoList = await helmClient.listRepositories();
const originalRepoListSize = originalRepoList.length;
await removeRepoIfPresent(helmClient, INCUBATOR_REPOSITORY);
try {
// Basic add
await expect(helmClient.addRepository(INCUBATOR_REPOSITORY)).to.not.be.rejected;
let repositories = await helmClient.listRepositories();
expect(repositories).to.not.be.null.and.to.not.be.empty;
expect(repositories).to.deep.include(INCUBATOR_REPOSITORY);
expect(repositories).to.have.lengthOf(originalRepoListSize + 1);
// Remove again for clean test
await expect(helmClient.removeRepository(INCUBATOR_REPOSITORY)).to.not.be.rejected;
// Add with forceUpdate = true
const optionsTrue = new AddRepoOptionsBuilder().forceUpdate(true).build();
await expect(helmClient.addRepository(INCUBATOR_REPOSITORY, optionsTrue)).to.not.be.rejected;
repositories = await helmClient.listRepositories();
expect(repositories).to.deep.include(INCUBATOR_REPOSITORY);
// Remove again
await expect(helmClient.removeRepository(INCUBATOR_REPOSITORY)).to.not.be.rejected;
// Add with forceUpdate = false (should be same as default)
const optionsFalse = new AddRepoOptionsBuilder().forceUpdate(false).build();
await expect(helmClient.addRepository(INCUBATOR_REPOSITORY, optionsFalse)).to.not.be.rejected;
repositories = await helmClient.listRepositories();
expect(repositories).to.deep.include(INCUBATOR_REPOSITORY);
}
finally {
await expect(helmClient.removeRepository(INCUBATOR_REPOSITORY)).to.not.be.rejected;
const repositories = await helmClient.listRepositories();
expect(repositories).to.not.be.null;
expect(repositories).to.have.lengthOf(originalRepoListSize);
}
});
it('Repository Remove Executes With Error', async () => {
await removeRepoIfPresent(helmClient, JETSTACK_REPOSITORY);
const repositories = await helmClient.listRepositories();
const existingRepoCount = repositories.length;
const expectedMessage = existingRepoCount === 0
? 'Error: no repositories configured'
: `Error: no repo named "${JETSTACK_REPOSITORY.name}" found`;
await expect(helmClient.removeRepository(JETSTACK_REPOSITORY))
.to.be.rejectedWith(HelmExecutionException)
.that.eventually.has.property('message')
.that.contain(expectedMessage);
});
it('Install Chart Executes Successfully', async function () {
this.timeout(INSTALL_TIMEOUT * 1000);
await addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY);
try {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
const options = InstallChartOptionsBuilder.builder().namespace(NAMESPACE).build();
const release = await helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options);
// Verify the returned release object
expect(release).to.not.be.null;
expect(release.name).to.equal(HAPROXY_RELEASE_NAME);
expect(release.info.description).to.equal('Install complete');
expect(release.info.status).to.equal('deployed');
// Verify the release through helm list command using namespace
const specificNamespaceReleaseItems = await helmClient.listReleases(false, NAMESPACE);
expect(specificNamespaceReleaseItems).to.not.be.null.and.to.not.be.empty;
const specificNamespaceReleaseItem = specificNamespaceReleaseItems.find((item) => item.name === HAPROXY_RELEASE_NAME);
expect(specificNamespaceReleaseItem).to.not.be.null;
expect(specificNamespaceReleaseItem?.name).to.equal(HAPROXY_RELEASE_NAME);
expect(specificNamespaceReleaseItem?.namespace).to.equal(NAMESPACE);
expect(specificNamespaceReleaseItem?.status).to.equal('deployed');
// Verify with default client and all namespaces
const defaultHelmClient = await new DefaultHelmClientBuilder().build();
const releaseItems = await defaultHelmClient.listReleases(true);
expect(releaseItems).to.not.be.null.and.to.not.be.empty;
const releaseItem = releaseItems.find((item) => item.name === HAPROXY_RELEASE_NAME);
expect(releaseItem).to.not.be.null;
expect(releaseItem?.name).to.equal(HAPROXY_RELEASE_NAME);
expect(releaseItem?.namespace).to.equal(NAMESPACE);
expect(releaseItem?.status).to.equal('deployed');
}
finally {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
}
});
it('List Releases with Kube Context', async () => {
await addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY);
try {
// Install a test chart first
const options = InstallChartOptionsBuilder.builder().namespace(NAMESPACE).build();
await helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options);
// List releases with specific kube context
const k8 = container.resolve(InjectTokens.K8Factory).default();
const releaseItems = await helmClient.listReleases(false, NAMESPACE, k8.contexts().readCurrent());
expect(releaseItems).to.not.be.null.and.to.not.be.empty;
const releaseItem = releaseItems.find((item) => item.name === HAPROXY_RELEASE_NAME);
expect(releaseItem).to.not.be.null;
expect(releaseItem?.name).to.equal(HAPROXY_RELEASE_NAME);
expect(releaseItem?.namespace).to.equal(NAMESPACE);
expect(releaseItem?.status).to.equal('deployed');
}
finally {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
}
});
it('Helm Test subcommand with options', async () => {
await addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY);
const options = TestChartOptionsBuilder.builder()
.timeout('60s')
.filter('haproxy')
.namespace(NAMESPACE)
.build();
try {
const helmOptions = InstallChartOptionsBuilder.builder().namespace(NAMESPACE).build();
await helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, helmOptions);
await helmClient.testChart(HAPROXY_RELEASE_NAME, options);
}
finally {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
}
});
const testChartInstallWithCleanup = async (options) => {
try {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
const release = await helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, options);
// Verify the returned release object
expect(release).to.not.be.null;
expect(release.name).to.equal(HAPROXY_RELEASE_NAME);
expect(release.info.description).to.equal('Install complete');
expect(release.info.status).to.equal('deployed');
// Verify the release through helm list command using namespace
const specificNamespaceReleaseItems = await helmClient.listReleases(false, NAMESPACE);
expect(specificNamespaceReleaseItems).to.not.be.null.and.to.not.be.empty;
const specificNamespaceReleaseItem = specificNamespaceReleaseItems.find((item) => item.name === HAPROXY_RELEASE_NAME);
expect(specificNamespaceReleaseItem).to.not.be.null;
expect(specificNamespaceReleaseItem?.name).to.equal(HAPROXY_RELEASE_NAME);
expect(specificNamespaceReleaseItem?.namespace).to.equal(NAMESPACE);
expect(specificNamespaceReleaseItem?.status).to.equal('deployed');
// Verify with default client and all namespaces
const defaultHelmClient = await new DefaultHelmClientBuilder().build();
const releaseItems = await defaultHelmClient.listReleases(true);
expect(releaseItems).to.not.be.null.and.to.not.be.empty;
const releaseItem = releaseItems.find((item) => item.name === HAPROXY_RELEASE_NAME);
expect(releaseItem).to.not.be.null;
expect(releaseItem?.name).to.equal(HAPROXY_RELEASE_NAME);
expect(releaseItem?.namespace).to.equal(NAMESPACE);
expect(releaseItem?.status).to.equal('deployed');
}
finally {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
}
};
it('Test Helm upgrade subcommand', async () => {
try {
await addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY);
// First install the chart
const helmOptions = InstallChartOptionsBuilder.builder().namespace(NAMESPACE).build();
await helmClient.installChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, helmOptions);
// Then try to upgrade it
await expect(helmClient.upgradeChart(HAPROXY_RELEASE_NAME, HAPROXY_CHART, UpgradeChartOptionsBuilder.builder().namespace(NAMESPACE).build())).to.not.be.rejected;
}
finally {
try {
await helmClient.uninstallChart(HAPROXY_RELEASE_NAME, UnInstallChartOptionsBuilder.builder().namespace(NAMESPACE).build());
}
catch {
// Suppress uninstall errors
}
}
});
// Skipped d in our unit tests due to lack of signed charts in the repo
it.skip('Test Helm dependency update subcommand', async () => {
await expect(helmClient.dependencyUpdate(TEST_CHARTS_DIR)).to.not.be.rejected;
});
// Skipped d in our unit tests due to lack of signed charts in the repo
it.skip('Test Helm dependency build subcommand failure', async () => {
await expect(helmClient.dependencyUpdate(NONEXISTENT_CHARTS_DIR))
.to.be.rejectedWith(HelmExecutionException)
.that.eventually.has.property('message')
.that.contain('Error: could not find Chart.yaml');
});
const getChartInstallOptionsTestParameters = () => {
return [
{
name: 'Atomic Chart Installation Executes Successfully',
options: InstallChartOptionsBuilder.builder().atomic(true).createNamespace(true).namespace(NAMESPACE).build(),
},
{
name: 'Install Chart with Combination of Options Executes Successfully',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.dependencyUpdate(true)
.description('Test install chart with options')
.enableDNS(true)
.force(true)
.skipCrds(true)
.timeout('3m0s')
.username('username')
.password('password')
.version('1.18.0')
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Dependency Updates',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.dependencyUpdate(true)
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Description',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.namespace(NAMESPACE)
.description('Test install chart with options')
.build(),
},
{
name: 'Install Chart with DNS Enabled',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.enableDNS(true)
.namespace(NAMESPACE)
.build(),
},
{
name: 'Forced Chart Installation',
options: InstallChartOptionsBuilder.builder().createNamespace(true).force(true).namespace(NAMESPACE).build(),
},
{
name: 'Install Chart with Password',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.password('password')
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart From Repository',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.repo(HAPROXYTECH_REPOSITORY.url)
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart Skipping CRDs',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.skipCrds(true)
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Timeout',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.timeout('60s')
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Username',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.username('username')
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Specific Version',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.version('1.18.0')
.namespace(NAMESPACE)
.build(),
},
{
name: 'Install Chart with Wait',
options: InstallChartOptionsBuilder.builder()
.createNamespace(true)
.waitFor(true)
.namespace(NAMESPACE)
.build(),
},
];
};
describe('Parameterized Chart Installation with Options Executes Successfully', function () {
this.timeout(INSTALL_TIMEOUT * 1000);
for (const parameters of getChartInstallOptionsTestParameters()) {
it(parameters.name, async () => {
await addRepoIfMissing(helmClient, HAPROXYTECH_REPOSITORY);
await testChartInstallWithCleanup(parameters.options);
});
}
});
});
//# sourceMappingURL=helm-client.test.js.map