@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
233 lines • 13.9 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { expect } from 'chai';
import { after, before, describe, it } from 'mocha';
import each from 'mocha-each';
import fs from 'node:fs';
import path from 'node:path';
import { KubectlDependencyManager } from '../../../../../src/core/dependency-managers/index.js';
import { getTestCacheDirectory, getTemporaryDirectory } from '../../../../test-utility.js';
import * as version from '../../../../../version.js';
import { PathEx } from '../../../../../src/business/utils/path-ex.js';
import sinon from 'sinon';
import { platform } from 'node:process';
import { OperatingSystem } from '../../../../../src/business/utils/operating-system.js';
import { container } from 'tsyringe-neo';
import { InjectTokens } from '../../../../../src/core/dependency-injection/inject-tokens.js';
import * as constants from '../../../../../src/core/constants.js';
import { resetForTest } from '../../../../test-container.js';
const mockVersionOutputValid = 'Client Version: v1.33.3\nKustomize Version: v5.6.0';
const mockVersionOutputMissingClient = 'Something Else: v1.10.0\nKustomize Version: v5.6.0';
const mockVersionOutputInvalid = 'invalid output';
describe('KubectlDependencyManager', () => {
const originalPlatform = platform;
const originalInstallationDirectory = container.resolve(InjectTokens.KubectlInstallationDirectory);
const temporaryDirectory = PathEx.join(getTemporaryDirectory(), 'bin');
const localInstallationDirectory = temporaryDirectory;
let sandbox;
before(() => {
resetForTest();
fs.mkdirSync(temporaryDirectory);
sandbox = sinon.createSandbox();
});
after(() => {
if (fs.existsSync(temporaryDirectory)) {
fs.rmSync(temporaryDirectory, { recursive: true });
}
});
afterEach(() => {
resetForTest();
sandbox.restore();
});
it('should return kubectl version', () => {
const kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, undefined, undefined);
expect(kubectlDependencyManager.getRequiredVersion()).to.equal(version.KUBECTL_VERSION);
});
it('should be able to check when kubectl not installed', () => {
const kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, undefined, undefined);
expect(kubectlDependencyManager.isInstalledLocally()).not.to.be.ok;
});
it('should be able to check when kubectl is installed', async () => {
const kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, undefined, undefined);
// Create the local executable file for testing
const localPath = PathEx.join(localInstallationDirectory, constants.KUBECTL);
fs.writeFileSync(localPath, '');
expect(kubectlDependencyManager.isInstalledLocally()).to.be.ok;
});
describe('when kubectl is installed globally', () => {
let kubectlDependencyManager;
let runStub;
let existsSyncStub;
let downloaderFetchPackageSpy;
let packageDownloader;
let cpSyncStub;
let chmodSyncStub;
let rmSyncStub;
beforeEach(() => {
packageDownloader = container.resolve(InjectTokens.PackageDownloader);
downloaderFetchPackageSpy = sandbox.spy(packageDownloader, 'fetchPackage');
kubectlDependencyManager = new KubectlDependencyManager(undefined, temporaryDirectory, process.arch, undefined);
kubectlDependencyManager.uninstallLocal();
runStub = sandbox.stub(kubectlDependencyManager, 'run');
cpSyncStub = sandbox.stub(fs, 'cpSync').returns();
chmodSyncStub = sandbox.stub(fs, 'chmodSync').returns();
existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true);
rmSyncStub = sandbox.stub(fs, 'rmSync').returns();
});
afterEach(() => {
container.register(InjectTokens.OsPlatform, { useValue: originalPlatform });
container.register(InjectTokens.KubectlInstallationDirectory, { useValue: originalInstallationDirectory });
sandbox.restore();
resetForTest();
});
it('should prefer the global installation if it meets the requirements', async () => {
const fakeGlobalBinDirectory = '/test-solo-global-bin';
const fakeGlobalKubectlPath = `${fakeGlobalBinDirectory}/kubectl`;
const originalPath = process.env.PATH ?? '';
process.env.PATH = `${fakeGlobalBinDirectory}${path.delimiter}${originalPath}`;
sandbox.stub(fs, 'accessSync').callsFake((filePath) => {
if (String(filePath) === fakeGlobalKubectlPath) {
return;
}
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});
runStub.withArgs(`"${fakeGlobalKubectlPath}" version --client`).resolves(mockVersionOutputValid.split('\n'));
existsSyncStub.withArgs(`${localInstallationDirectory}/kubectl`).returns(false);
try {
// @ts-expect-error TS2341: Property isInstalledGloballyAndMeetsRequirements is private
const result = await kubectlDependencyManager.isInstalledGloballyAndMeetsRequirements();
expect(result).to.be.true;
expect(await kubectlDependencyManager.install(getTestCacheDirectory())).to.be.true;
// Should return global path since it meets requirements
expect(await kubectlDependencyManager.getExecutable()).to.equal(constants.KUBECTL);
}
finally {
process.env.PATH = originalPath;
}
});
it('should install kubectl locally if the global installation does not meet the requirements', async () => {
// Stub accessSync so the native PATH scan finds no global kubectl installation.
sandbox.stub(fs, 'accessSync').throws(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
expect(await kubectlDependencyManager.install(getTestCacheDirectory())).to.be.true;
expect(fs.existsSync(PathEx.join(localInstallationDirectory, constants.KUBECTL))).to.be.ok;
expect(await kubectlDependencyManager.getExecutable()).to.equal(constants.KUBECTL);
});
it('should be able to use local installation on repeated calls without reinstalling given global installation does not meet requirements', async () => {
// Restore all beforeEach stubs so real fs/run operations work for the download.
runStub.restore();
existsSyncStub.restore();
cpSyncStub.restore();
chmodSyncStub.restore();
rmSyncStub.restore();
// Stub accessSync so the native PATH scan finds no global kubectl installation.
sandbox.stub(fs, 'accessSync').throws(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
const kubectlInstallationDirectory = getTemporaryDirectory();
container.register(InjectTokens.KubectlInstallationDirectory, { useValue: kubectlInstallationDirectory });
kubectlDependencyManager = new KubectlDependencyManager(undefined, undefined, process.arch, undefined);
expect(await kubectlDependencyManager.install(getTemporaryDirectory())).to.be.true;
expect(downloaderFetchPackageSpy.calledOnce).to.be.true;
expect(fs.existsSync(PathEx.join(kubectlInstallationDirectory, constants.KUBECTL))).to.be.ok;
// Call install again, should not trigger installation since local installation meets requirements
downloaderFetchPackageSpy.resetHistory();
expect(downloaderFetchPackageSpy.notCalled).to.be.true;
expect(await kubectlDependencyManager.install(getTemporaryDirectory())).to.be.true;
expect(downloaderFetchPackageSpy.notCalled).to.be.true;
});
});
describe('Kubectl Installation Tests', () => {
afterEach(() => {
container.register(InjectTokens.OsPlatform, { useValue: originalPlatform });
sandbox.restore();
});
each([
[OperatingSystem.OS_LINUX, 'x64'],
[OperatingSystem.OS_LINUX, 'amd64'],
[OperatingSystem.OS_WIN32, 'amd64'],
]).it('should be able to install kubectl base on %s and %s', async (osPlatform, osArch) => {
container.register(InjectTokens.OsPlatform, { useValue: osPlatform });
const kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, osArch, undefined);
if (fs.existsSync(temporaryDirectory)) {
fs.rmSync(temporaryDirectory, { recursive: true });
}
kubectlDependencyManager.uninstallLocal();
expect(kubectlDependencyManager.isInstalledLocally()).not.to.be.ok;
// Stub accessSync so the native PATH scan finds no global kubectl installation.
sandbox.stub(fs, 'accessSync').throws(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
expect(await kubectlDependencyManager.install(getTestCacheDirectory())).to.be.true;
expect(kubectlDependencyManager.isInstalledLocally()).to.be.ok;
fs.rmSync(temporaryDirectory, { recursive: true });
});
});
describe('KubectlDependencyManager system methods', () => {
let kubectlDependencyManager;
beforeEach(() => {
kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, process.arch, undefined);
});
afterEach(() => {
container.register(InjectTokens.OsPlatform, { useValue: originalPlatform });
container.register(InjectTokens.KubectlInstallationDirectory, { useValue: originalInstallationDirectory });
sandbox.restore();
resetForTest();
});
it('getVersion should succeed with valid version output', async () => {
sandbox.stub(kubectlDependencyManager, 'run').resolves(mockVersionOutputValid.split('\n'));
expect(await kubectlDependencyManager.getVersion('/usr/local/bin/kubectl')).to.equal('1.33.3');
});
it('getVersion should handle error if kubectl version fails', async () => {
sandbox.stub(kubectlDependencyManager, 'run').rejects(new Error('Command failed'));
try {
await kubectlDependencyManager.getVersion('/usr/local/bin/kubectl');
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message).to.include('Failed to check kubectl version');
}
});
it('getVersion should handle invalid output', async () => {
sandbox.stub(kubectlDependencyManager, 'run').resolves(mockVersionOutputInvalid.split('\n'));
try {
await kubectlDependencyManager.getVersion('/usr/local/bin/kubectl');
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message).to.include('Failed to get kubectl version');
}
});
it('getVersion should handle missing client version in output', async () => {
sandbox.stub(kubectlDependencyManager, 'run').resolves(mockVersionOutputMissingClient.split('\n'));
try {
await kubectlDependencyManager.getVersion('/usr/local/bin/kubectl');
expect.fail('Should have thrown an error');
}
catch (error) {
expect(error).to.be.instanceOf(Error);
expect(error.message).to.include('Failed to get kubectl version');
}
});
it('processDownloadedPackage should handle platform-specific executable names', async () => {
container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_LINUX });
// First test with non-Windows platform
let kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, 'amd64', undefined);
// @ts-expect-error TS2341: Property processDownloadedPackage is private
const linuxResult = await kubectlDependencyManager.processDownloadedPackage('/tmp/kubectl', '/tmp');
expect(linuxResult).to.contain('/tmp/kubectl');
// Now test with Windows platform
container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_WIN32 });
kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, 'amd64', undefined);
// @ts-expect-error TS2341: Property processDownloadedPackage is private
const windowsResult = await kubectlDependencyManager.processDownloadedPackage('/tmp/kubectl.exe');
expect(windowsResult).to.contain('/tmp/kubectl.exe');
});
it('getArtifactName should generate correct URL format based on platform/arch', () => {
container.register(InjectTokens.OsPlatform, { useValue: OperatingSystem.OS_LINUX });
const kubectlDependencyManager = new KubectlDependencyManager(undefined, localInstallationDirectory, 'amd64', '1.25.0');
// @ts-expect-error TS2341: Property getArtifactName is private
const artifactName = kubectlDependencyManager.getArtifactName();
expect(artifactName).to.include('1.25.0');
expect(artifactName).to.include(OperatingSystem.OS_LINUX);
expect(artifactName).to.include('amd64');
});
});
});
//# sourceMappingURL=kubectl-dependency-manager.test.js.map