@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
237 lines • 12.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { after, before, describe, it } from 'mocha';
import { expect } from 'chai';
import each from 'mocha-each';
import fs from 'node:fs';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { v4 as uuid4 } from 'uuid';
import { SoloError } from '../../../../src/core/errors/solo-error.js';
import * as constants from '../../../../src/core/constants.js';
import { Flags as flags } from '../../../../src/commands/flags.js';
import crypto from 'node:crypto';
import { PodName } from '../../../../src/integration/kube/resources/pod/pod-name.js';
import { Duration } from '../../../../src/core/time/duration.js';
import { container } from 'tsyringe-neo';
import { NamespaceName } from '../../../../src/types/namespace/namespace-name.js';
import { PodReference } from '../../../../src/integration/kube/resources/pod/pod-reference.js';
import { ContainerName } from '../../../../src/integration/kube/resources/container/container-name.js';
import { ContainerReference } from '../../../../src/integration/kube/resources/container/container-reference.js';
import { ServiceReference } from '../../../../src/integration/kube/resources/service/service-reference.js';
import { ServiceName } from '../../../../src/integration/kube/resources/service/service-name.js';
import { InjectTokens } from '../../../../src/core/dependency-injection/inject-tokens.js';
import { Argv } from '../../../helpers/argv-wrapper.js';
import { PathEx } from '../../../../src/business/utils/path-ex.js';
import { SoloPinoLogger } from '../../../../src/core/logging/solo-pino-logger.js';
const defaultTimeout = Duration.ofMinutes(2).toMillis();
const TEST_POD_IMAGE = process.env.K8_E2E_TEST_IMAGE ?? 'registry.k8s.io/e2e-test-images/busybox:1.29';
async function createPod(podReference, containerName, podLabelValue, k8Factory, image) {
await k8Factory
.default()
.pods()
.create(podReference, { app: podLabelValue }, containerName, image, ['/bin/sh', '-c', 'sleep 7200'], ['/bin/sh', '-c', 'exit 0']);
}
describe('K8', () => {
const testLogger = new SoloPinoLogger('debug', true);
const configManager = container.resolve(InjectTokens.ConfigManager);
const k8Factory = container.resolve(InjectTokens.K8Factory);
const testNamespace = NamespaceName.of('k8-e2e');
const argv = Argv.initializeEmpty();
const podName = PodName.of(`test-pod-${uuid4()}`);
const podReference = PodReference.of(testNamespace, podName);
const containerName = ContainerName.of('alpine');
const podLabelValue = `test-${uuid4()}`;
const serviceName = `test-service-${uuid4()}`;
before(async function () {
this.timeout(defaultTimeout);
try {
argv.setArg(flags.namespace, testNamespace.name);
configManager.update(argv.build());
if (!(await k8Factory.default().namespaces().has(testNamespace))) {
await k8Factory.default().namespaces().create(testNamespace);
}
await createPod(podReference, containerName, podLabelValue, k8Factory, TEST_POD_IMAGE);
const serviceReference = ServiceReference.of(testNamespace, ServiceName.of(serviceName));
await k8Factory.default().services().create(serviceReference, { app: 'svc-test' }, 80, 80);
// wait 10 seconds for the pod up and running
await new Promise(resolve => setTimeout(resolve, 10_000));
}
catch (error) {
console.log(`${error}, ${error.stack}`);
throw error;
}
});
after(async function () {
this.timeout(defaultTimeout);
try {
await k8Factory.default().pods().readByReference(PodReference.of(testNamespace, podName)).killPod();
argv.setArg(flags.namespace, constants.SOLO_SETUP_NAMESPACE.name);
configManager.update(argv.build());
}
catch (error) {
console.log(error);
throw error;
}
});
it('should be able to list clusters', async () => {
const clusters = k8Factory.default().clusters().list();
expect(clusters).not.to.have.lengthOf(0);
}).timeout(defaultTimeout);
it('should be able to list namespaces', async () => {
const namespaces = await k8Factory.default().namespaces().list();
expect(namespaces).not.to.have.lengthOf(0);
const match = namespaces.filter(n => n.name === constants.DEFAULT_NAMESPACE.name);
expect(match).to.have.lengthOf(1);
}).timeout(defaultTimeout);
it('should be able to list context names', () => {
const contexts = k8Factory.default().contexts().list();
expect(contexts).not.to.have.lengthOf(0);
}).timeout(defaultTimeout);
it('should be able to create and delete a namespaces', async () => {
const name = uuid4();
expect(await k8Factory.default().namespaces().create(NamespaceName.of(name))).to.be.true;
expect(await k8Factory.default().namespaces().delete(NamespaceName.of(name))).to.be.true;
}).timeout(defaultTimeout);
it('should be able to run wait for pod', async () => {
const labels = [`app=${podLabelValue}`];
const pods = await k8Factory
.default()
.pods()
.waitForRunningPhase(testNamespace, labels, 30, constants.PODS_RUNNING_DELAY);
expect(pods).to.have.lengthOf(1);
}).timeout(defaultTimeout);
it('should be able to run wait for pod ready', async () => {
const labels = [`app=${podLabelValue}`];
const pods = await k8Factory.default().pods().waitForReadyStatus(testNamespace, labels, 100);
expect(pods).to.have.lengthOf(1);
}).timeout(defaultTimeout);
it('should be able to check if a path is directory inside a container', async () => {
const pods = await k8Factory
.default()
.pods()
.list(testNamespace, [`app=${podLabelValue}`]);
expect(await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(pods[0].podReference, containerName))
.hasDir('/tmp')).to.be.true;
}).timeout(defaultTimeout);
const testCases = ['test/data/pem/keys/a-private-node0.pem', 'test/data/build-v0.54.0-alpha.4.zip'];
each(testCases).describe('test copyTo and copyFrom', localFilePath => {
it('should be able to copy a file to and from a container', async () => {
const pods = await k8Factory
.default()
.pods()
.waitForReadyStatus(testNamespace, [`app=${podLabelValue}`], 20);
expect(pods).to.have.lengthOf(1);
const localTemporaryDirectory = fs.mkdtempSync(PathEx.join(os.tmpdir(), 'k8-test'));
const remoteTemporaryDirectory = '/tmp';
const fileName = path.basename(localFilePath);
const remoteFilePath = `${remoteTemporaryDirectory}/${fileName}`;
const originalFileData = fs.readFileSync(localFilePath);
const originalFileHash = crypto.createHash('sha384').update(originalFileData).digest('hex');
const originalStat = fs.statSync(localFilePath);
// upload the file
expect(await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(podReference, containerName))
.copyTo(localFilePath, remoteTemporaryDirectory)).to.be.true;
// download the same file
expect(await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(podReference, containerName))
.copyFrom(remoteFilePath, localTemporaryDirectory)).to.be.true;
const downloadedFilePath = PathEx.joinWithRealPath(localTemporaryDirectory, fileName);
const downloadedFileData = fs.readFileSync(downloadedFilePath);
const downloadedFileHash = crypto.createHash('sha384').update(downloadedFileData).digest('hex');
const downloadedStat = fs.statSync(downloadedFilePath);
expect(downloadedStat.size, 'downloaded file size should match original file size').to.equal(originalStat.size);
expect(downloadedFileHash, 'downloaded file hash should match original file hash').to.equal(originalFileHash);
// rm file inside the container
await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(podReference, containerName))
.execContainer(['rm', '-f', remoteFilePath]);
fs.rmSync(localTemporaryDirectory, { recursive: true });
}).timeout(defaultTimeout);
});
it('should be able to port forward gossip port', done => {
const localPort = +constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT;
try {
const podReference = PodReference.of(testNamespace, podName);
k8Factory
.default()
.pods()
.readByReference(podReference)
.portForward(localPort, +constants.HEDERA_NODE_INTERNAL_GOSSIP_PORT)
.then(async (server) => {
// sleep for 5 seconds to allow the port forward to start
await new Promise(resolve => setTimeout(resolve, 5000));
expect(server).not.to.be.null;
// client
const s = new net.Socket();
s.on('ready', async () => {
s.destroy();
await k8Factory.default().pods().readByReference(podReference).stopPortForward(server);
done();
});
s.on('error', async (error) => {
s.destroy();
await k8Factory.default().pods().readByReference(podReference).stopPortForward(server);
done(new SoloError(`could not connect to local port '${localPort}': ${error.message}`, error));
});
s.connect(localPort);
});
}
catch (error) {
testLogger.showUserError(error);
expect.fail();
}
// TODO enhance this test to do something with the port, this pod isn't even running, but it is still passing
}).timeout(defaultTimeout);
it('should be able to cat a file inside the container', async () => {
const pods = await k8Factory
.default()
.pods()
.list(testNamespace, [`app=${podLabelValue}`]);
const podName = pods[0].podReference.name;
const output = await k8Factory
.default()
.containers()
.readByRef(ContainerReference.of(PodReference.of(testNamespace, podName), containerName))
.execContainer(['cat', '/etc/hostname']);
expect(output.indexOf(podName.name)).to.equal(0);
}).timeout(defaultTimeout);
it('should be able to list persistent volume claims', async () => {
const pvcReference = PodReference.of(testNamespace, PodName.of(`test-pvc-${uuid4()}`));
try {
await k8Factory.default().pvcs().create(pvcReference, { storage: '50Mi' }, ['ReadWriteOnce']);
const pvcs = await k8Factory.default().pvcs().list(testNamespace);
expect(pvcs).to.have.length.greaterThan(0);
}
catch (error) {
console.error(error);
throw error;
}
finally {
await k8Factory.default().pvcs().delete(pvcReference);
}
}).timeout(defaultTimeout);
it('should be able to kill a pod', async () => {
const podName = PodName.of(`test-pod-${uuid4()}`);
const podReference = PodReference.of(testNamespace, podName);
const podLabelValue = `test-${uuid4()}`;
await createPod(podReference, containerName, podLabelValue, k8Factory, TEST_POD_IMAGE);
await k8Factory.default().pods().readByReference(podReference).killPod();
const newPods = await k8Factory
.default()
.pods()
.list(testNamespace, [`app=${podLabelValue}`]);
expect(newPods).to.have.lengthOf(0);
});
});
//# sourceMappingURL=k8-end-to-end.test.js.map