@milaboratories/pl-deployments 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1 -0
  2. package/dist/common/os_and_arch.d.ts +9 -0
  3. package/dist/common/os_and_arch.d.ts.map +1 -0
  4. package/dist/common/pl_binary.d.ts +14 -0
  5. package/dist/common/pl_binary.d.ts.map +1 -0
  6. package/dist/common/pl_binary_download.d.ts +30 -0
  7. package/dist/common/pl_binary_download.d.ts.map +1 -0
  8. package/dist/common/pl_version.d.ts +2 -0
  9. package/dist/common/pl_version.d.ts.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +40 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/index.mjs +1028 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/local/options.d.ts +31 -0
  17. package/dist/local/options.d.ts.map +1 -0
  18. package/dist/local/pid.d.ts +4 -0
  19. package/dist/local/pid.d.ts.map +1 -0
  20. package/dist/local/pl.d.ts +66 -0
  21. package/dist/local/pl.d.ts.map +1 -0
  22. package/dist/local/process.d.ts +12 -0
  23. package/dist/local/process.d.ts.map +1 -0
  24. package/dist/local/trace.d.ts +10 -0
  25. package/dist/local/trace.d.ts.map +1 -0
  26. package/dist/ssh/__tests__/common-utils.d.ts +18 -0
  27. package/dist/ssh/__tests__/common-utils.d.ts.map +1 -0
  28. package/dist/ssh/pl.d.ts +101 -0
  29. package/dist/ssh/pl.d.ts.map +1 -0
  30. package/dist/ssh/pl_paths.d.ts +17 -0
  31. package/dist/ssh/pl_paths.d.ts.map +1 -0
  32. package/dist/ssh/ssh.d.ts +128 -0
  33. package/dist/ssh/ssh.d.ts.map +1 -0
  34. package/dist/ssh/supervisord.d.ts +8 -0
  35. package/dist/ssh/supervisord.d.ts.map +1 -0
  36. package/package.json +64 -0
  37. package/src/common/os_and_arch.ts +44 -0
  38. package/src/common/pl_binary.ts +39 -0
  39. package/src/common/pl_binary_download.ts +258 -0
  40. package/src/common/pl_version.ts +5 -0
  41. package/src/index.ts +4 -0
  42. package/src/local/config.test.yaml +60 -0
  43. package/src/local/options.ts +34 -0
  44. package/src/local/pid.ts +21 -0
  45. package/src/local/pl.test.ts +129 -0
  46. package/src/local/pl.ts +220 -0
  47. package/src/local/process.ts +44 -0
  48. package/src/local/trace.ts +30 -0
  49. package/src/ssh/Dockerfile +29 -0
  50. package/src/ssh/__tests__/common-utils.ts +131 -0
  51. package/src/ssh/__tests__/pl-docker.test.ts +192 -0
  52. package/src/ssh/__tests__/ssh-docker.test.ts +175 -0
  53. package/src/ssh/pl.ts +451 -0
  54. package/src/ssh/pl_paths.ts +57 -0
  55. package/src/ssh/ssh.ts +512 -0
  56. package/src/ssh/supervisord.ts +137 -0
  57. package/src/ssh/test-assets/simple-server.js +10 -0
@@ -0,0 +1,129 @@
1
+ import { test } from 'vitest';
2
+ import type { LocalPlOptions } from './pl';
3
+ import { localPlatformaInit } from './pl';
4
+ import { ConsoleLoggerAdapter, sleep } from '@milaboratories/ts-helpers';
5
+ import * as fs from 'fs/promises';
6
+ import upath from 'upath';
7
+ import { processStop } from './process';
8
+ import * as yaml from 'yaml';
9
+ import * as os from 'os';
10
+
11
+ test(
12
+ 'should start and stop platforma of the current version with hardcoded config',
13
+ { timeout: 25000 },
14
+ async ({ expect }) => {
15
+ const logger = new ConsoleLoggerAdapter();
16
+ const config = await readTestConfig();
17
+
18
+ const dir = await prepareDirForTestConfig();
19
+
20
+ const pl = await localPlatformaInit(logger, {
21
+ workingDir: dir,
22
+ config,
23
+ closeOld: false,
24
+ });
25
+
26
+ await sleep(5000);
27
+
28
+ console.log(`Platforma: %o`, pl.debugInfo());
29
+
30
+ expect(await pl.isAlive()).toBeTruthy();
31
+ expect(pl.pid).not.toBeUndefined();
32
+
33
+ pl.stop();
34
+ await pl.waitStopped();
35
+ },
36
+ );
37
+
38
+ test(
39
+ 'should close old platforma when starting a new one if the option is set',
40
+ { timeout: 25000 },
41
+ async ({ expect }) => {
42
+ const logger = new ConsoleLoggerAdapter();
43
+
44
+ const config = await readTestConfig();
45
+ const dir = await prepareDirForTestConfig();
46
+ const options: LocalPlOptions = {
47
+ workingDir: dir,
48
+ config,
49
+ };
50
+
51
+ const oldPl = await localPlatformaInit(logger, options);
52
+ await sleep(5000);
53
+ console.log(`OldPlatforma: %o`, oldPl.debugInfo());
54
+
55
+ expect(await oldPl.isAlive()).toBeTruthy();
56
+ const newPl = await localPlatformaInit(logger, options);
57
+ expect(await oldPl.isAlive()).toBeFalsy();
58
+ await sleep(5000);
59
+
60
+ console.log(`NewPlatforma: %o`, newPl.debugInfo());
61
+
62
+ expect(await newPl.isAlive()).toBeTruthy();
63
+ expect(newPl.pid).not.toBeUndefined();
64
+ newPl.stop();
65
+ await newPl.waitStopped();
66
+ },
67
+ );
68
+
69
+ test(
70
+ 'should restart platforma if restart option was provided',
71
+ { timeout: 25000 },
72
+ async ({ expect }) => {
73
+ const logger = new ConsoleLoggerAdapter();
74
+ const config = await readTestConfig();
75
+ const dir = await prepareDirForTestConfig();
76
+
77
+ const pl = await localPlatformaInit(logger, {
78
+ workingDir: dir,
79
+ config,
80
+ closeOld: false,
81
+ onCloseAndErrorNoStop: async (pl) => await pl.start(),
82
+ });
83
+ await sleep(1000);
84
+
85
+ expect(await pl.isAlive()).toBeTruthy();
86
+ processStop(pl.pid!);
87
+ await sleep(3000);
88
+ console.log(`Platforma after first stop: %o`, pl.debugInfo());
89
+
90
+ expect(await pl.isAlive()).toBeTruthy();
91
+ processStop(pl.pid!);
92
+ await sleep(3000);
93
+ console.log(`Platforma after second stop: %o`, pl.debugInfo());
94
+
95
+ expect(await pl.isAlive()).toBeTruthy();
96
+ expect(pl.debugInfo().nRuns).toEqual(3);
97
+
98
+ pl.stop();
99
+ await pl.waitStopped();
100
+ },
101
+ );
102
+
103
+ async function readTestConfig() {
104
+ const testConfig = upath.join(__dirname, 'config.test.yaml');
105
+ const config = (await fs.readFile(testConfig)).toString();
106
+
107
+ const parsed = yaml.parse(config);
108
+ parsed.license.value = process.env.MI_LICENSE;
109
+ if ((parsed.license.value ?? '') == '') {
110
+ parsed.license.file = process.env.MI_LICENSE_FILE;
111
+ if ((parsed.license.file ?? '') == '') {
112
+ parsed.license.file = upath.join(os.homedir(), '.pl.license');
113
+ }
114
+ }
115
+
116
+ return yaml.stringify(parsed);
117
+ }
118
+
119
+ async function prepareDirForTestConfig() {
120
+ const dir = upath.join(__dirname, '..', '.test');
121
+ await fs.rm(dir, { recursive: true, force: true });
122
+ await fs.mkdir(dir);
123
+
124
+ await fs.mkdir(upath.join(dir, 'storages', 'work'), { recursive: true });
125
+ await fs.mkdir(upath.join(dir, 'storages', 'main'), { recursive: true });
126
+ await fs.mkdir(upath.join(dir, 'packages'), { recursive: true });
127
+
128
+ return dir;
129
+ }
@@ -0,0 +1,220 @@
1
+ import type {
2
+ ProcessOptions } from './process';
3
+ import {
4
+ isProcessAlive,
5
+ processStop,
6
+ processWaitStopped,
7
+ processRun,
8
+ } from './process';
9
+ import type { PlBinarySource } from '../common/pl_binary';
10
+ import { newDefaultPlBinarySource, resolveLocalPlBinaryPath } from '../common/pl_binary';
11
+ import type { MiLogger } from '@milaboratories/ts-helpers';
12
+ import { notEmpty } from '@milaboratories/ts-helpers';
13
+ import type { ChildProcess, SpawnOptions } from 'child_process';
14
+ import { filePid, readPid, writePid } from './pid';
15
+ import type { Trace } from './trace';
16
+ import { withTrace } from './trace';
17
+ import upath from 'upath';
18
+ import fsp from 'fs/promises';
19
+ import type { Required } from 'utility-types';
20
+
21
+ export const LocalConfigYaml = 'config-local.yaml';
22
+
23
+ /**
24
+ * Represents a local running pl-core,
25
+ * and has methods to start, check if it's running, stop and wait for stopping it.
26
+ * Also, a hook on pl-core closed can be provided.
27
+ */
28
+ export class LocalPl {
29
+ private instance?: ChildProcess;
30
+ public pid?: number;
31
+ private nRuns: number = 0;
32
+ private lastRunHistory: Trace = {};
33
+ private wasStopped = false;
34
+
35
+ constructor(
36
+ private readonly logger: MiLogger,
37
+ private readonly workingDir: string,
38
+ private readonly startOptions: ProcessOptions,
39
+ private readonly initialStartHistory: Trace,
40
+ private readonly onClose?: (pl: LocalPl) => Promise<void>,
41
+ private readonly onError?: (pl: LocalPl) => Promise<void>,
42
+ private readonly onCloseAndError?: (pl: LocalPl) => Promise<void>,
43
+ private readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>,
44
+ ) {}
45
+
46
+ async start() {
47
+ await withTrace(this.logger, async (trace, t) => {
48
+ this.wasStopped = false;
49
+ const instance = processRun(this.logger, this.startOptions);
50
+ instance.on('error', (e: any) => {
51
+ this.logger.error(
52
+ `error '${e}', while running platforma, started opts: ${JSON.stringify(this.debugInfo())}`,
53
+ );
54
+
55
+ // keep in mind there are no awaits here, it will be asynchronous
56
+ if (this.onError !== undefined) this.onError(this);
57
+ if (this.onCloseAndError !== undefined) this.onCloseAndError(this);
58
+ if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)
59
+ this.onCloseAndErrorNoStop(this);
60
+ });
61
+ instance.on('close', () => {
62
+ this.logger.warn(`platforma was closed, started opts: ${JSON.stringify(this.debugInfo())}`);
63
+
64
+ // keep in mind there are no awaits here, it will be asynchronous
65
+ if (this.onClose !== undefined) this.onClose(this);
66
+ if (this.onCloseAndError !== undefined) this.onCloseAndError(this);
67
+ if (this.onCloseAndErrorNoStop !== undefined && !this.wasStopped)
68
+ this.onCloseAndErrorNoStop(this);
69
+ });
70
+
71
+ trace('started', true);
72
+
73
+ const pidFile = trace('pidFile', filePid(this.workingDir));
74
+ trace('pid', notEmpty(instance.pid));
75
+ trace('pidWritten', await writePid(pidFile, notEmpty(instance.pid)));
76
+
77
+ this.nRuns++;
78
+ this.instance = instance;
79
+ this.pid = instance.pid;
80
+ this.lastRunHistory = t;
81
+ });
82
+ }
83
+
84
+ stop() {
85
+ // TODO use this.instance to stop the process
86
+ this.wasStopped = true;
87
+ processStop(notEmpty(this.pid));
88
+ }
89
+
90
+ async waitStopped() {
91
+ await processWaitStopped(notEmpty(this.pid), 15000);
92
+ }
93
+
94
+ stopped() {
95
+ return this.wasStopped;
96
+ }
97
+
98
+ async isAlive(): Promise<boolean> {
99
+ return await isProcessAlive(notEmpty(this.pid));
100
+ }
101
+
102
+ debugInfo() {
103
+ return {
104
+ lastRunHistory: this.lastRunHistory,
105
+ nRuns: this.nRuns,
106
+ pid: this.pid,
107
+ workingDir: this.workingDir,
108
+ initialStartHistory: this.initialStartHistory,
109
+ wasStopped: this.wasStopped,
110
+ };
111
+ }
112
+ }
113
+
114
+ /** Options to start a local pl-core. */
115
+ export type LocalPlOptions = {
116
+ /** From what directory start a process. */
117
+ readonly workingDir: string;
118
+ /** A string representation of yaml config. */
119
+ readonly config: string;
120
+ /** How to get a binary, download it or get an existing one (default: download latest version) */
121
+ readonly plBinary?: PlBinarySource;
122
+ /** Additional options for a process, environments, stdout, stderr etc. */
123
+ readonly spawnOptions?: SpawnOptions;
124
+ /**
125
+ * If the previous pl-core was started from the same directory,
126
+ * we can check if it's still running and then stop it before starting a new one.
127
+ * (default: true)
128
+ */
129
+ readonly closeOld?: boolean;
130
+
131
+ readonly onClose?: (pl: LocalPl) => Promise<void>;
132
+ readonly onError?: (pl: LocalPl) => Promise<void>;
133
+ readonly onCloseAndError?: (pl: LocalPl) => Promise<void>;
134
+ readonly onCloseAndErrorNoStop?: (pl: LocalPl) => Promise<void>;
135
+ };
136
+
137
+ type LocalPlOptionsFull = Required<LocalPlOptions, 'plBinary' | 'spawnOptions' | 'closeOld'>;
138
+
139
+ /**
140
+ * Starts pl-core, if the option was provided downloads a binary, reads license environments etc.
141
+ */
142
+ export async function localPlatformaInit(logger: MiLogger, _ops: LocalPlOptions): Promise<LocalPl> {
143
+ // filling-in default values
144
+ const ops = {
145
+ plBinary: newDefaultPlBinarySource(),
146
+ spawnOptions: {},
147
+ closeOld: true,
148
+ ..._ops,
149
+ } satisfies LocalPlOptionsFull;
150
+
151
+ return await withTrace(logger, async (trace, t) => {
152
+ trace('startOptions', { ...ops, config: 'too wordy' });
153
+
154
+ const workDir = upath.resolve(ops.workingDir);
155
+
156
+ if (ops.closeOld) {
157
+ trace('closeOld', await localPlatformaReadPidAndStop(logger, workDir));
158
+ }
159
+
160
+ const configPath = upath.join(workDir, LocalConfigYaml);
161
+
162
+ logger.info(`writing configuration '${configPath}'...`);
163
+ await fsp.writeFile(configPath, ops.config);
164
+
165
+ const baseBinaryPath = await resolveLocalPlBinaryPath(logger, upath.join(workDir, 'binaries'), ops.plBinary);
166
+ const binaryPath = trace('binaryPath', upath.join('binaries', baseBinaryPath));
167
+
168
+ const processOpts: ProcessOptions = {
169
+ cmd: binaryPath,
170
+ args: ['-config', configPath],
171
+ opts: {
172
+ env: { ...process.env },
173
+ cwd: workDir,
174
+ stdio: ['pipe', 'ignore', 'inherit'],
175
+ windowsHide: true, // hide a terminal on Windows
176
+ ...ops.spawnOptions,
177
+ },
178
+ };
179
+ trace('processOpts', {
180
+ cmd: processOpts.cmd,
181
+ args: processOpts.args,
182
+ cwd: processOpts.opts.cwd,
183
+ });
184
+
185
+ const pl = new LocalPl(
186
+ logger,
187
+ ops.workingDir,
188
+ processOpts,
189
+ t,
190
+ ops.onClose,
191
+ ops.onError,
192
+ ops.onCloseAndError,
193
+ ops.onCloseAndErrorNoStop,
194
+ );
195
+ await pl.start();
196
+
197
+ return pl;
198
+ });
199
+ }
200
+
201
+ /** Reads a pid of the old pl-core if it was started in the same working directory,
202
+ * and closes it. */
203
+ async function localPlatformaReadPidAndStop(
204
+ logger: MiLogger,
205
+ workingDir: string,
206
+ ): Promise<Record<string, any>> {
207
+ return await withTrace(logger, async (trace, t) => {
208
+ const file = trace('pidFilePath', filePid(workingDir));
209
+
210
+ const oldPid = trace('pid', await readPid(file));
211
+ const alive = trace('wasAlive', await isProcessAlive(oldPid));
212
+
213
+ if (oldPid !== undefined && alive) {
214
+ trace('stopped', processStop(oldPid));
215
+ trace('waitStopped', await processWaitStopped(oldPid, 10_000));
216
+ }
217
+
218
+ return t;
219
+ });
220
+ }
@@ -0,0 +1,44 @@
1
+ import type { SpawnOptions, ChildProcess } from 'child_process';
2
+ import { spawn } from 'child_process';
3
+ import type { MiLogger } from '@milaboratories/ts-helpers';
4
+ import { sleep } from '@milaboratories/ts-helpers';
5
+
6
+ export type ProcessOptions = {
7
+ cmd: string;
8
+ args: string[];
9
+ opts: SpawnOptions;
10
+ };
11
+
12
+ export function processRun(logger: MiLogger, opts: ProcessOptions): ChildProcess {
13
+ logger.info(`Running:
14
+ cmd: ${JSON.stringify([opts.cmd, ...opts.args])}
15
+ wd: ${opts.opts.cwd}`);
16
+
17
+ logger.info(' spawning child process');
18
+ return spawn(opts.cmd, opts.args, opts.opts);
19
+ }
20
+
21
+ export async function isProcessAlive(pid: number) {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (e) {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export function processStop(pid: number) {
31
+ return process.kill(pid, 'SIGINT');
32
+ }
33
+
34
+ export async function processWaitStopped(pid: number, maxMs: number) {
35
+ const sleepMs = 100;
36
+ let total = 0;
37
+ while (await isProcessAlive(pid)) {
38
+ await sleep(sleepMs);
39
+ total += sleepMs;
40
+ if (total > maxMs) {
41
+ throw new Error(`The process did not stopped after ${maxMs} ms.`);
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ import type { MiLogger } from '@milaboratories/ts-helpers';
2
+
3
+ /** Records all inputs and outputs of one's choice, so if the error happened
4
+ * one can check how it was by just printing this structure. */
5
+ export type Trace = Record<string, any>;
6
+
7
+ export function newTrace(): Trace {
8
+ return {};
9
+ }
10
+
11
+ export function trace(t: Trace, k: string, v: any) {
12
+ t[k] = v;
13
+ return v;
14
+ }
15
+
16
+ /** Creates a trace and runs a function with it. The function can record all its
17
+ * logs or traces using `trace` fn. */
18
+ export async function withTrace<T>(
19
+ logger: MiLogger,
20
+ fn: (trace: (k: string, v: any) => typeof v, t: Trace) => Promise<T>,
21
+ ): Promise<T> {
22
+ const t = newTrace();
23
+ try {
24
+ const result = await fn((k: string, v: any) => trace(t, k, v), t);
25
+ return result;
26
+ } catch (e: any) {
27
+ logger.error(`error ${e} while doing traced operation, state: ${JSON.stringify(t)}`);
28
+ throw e;
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ FROM node:20
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y openssh-server && \
5
+ mkdir /var/run/sshd
6
+
7
+ # RUN echo 'root:rootpassword' | chpasswd
8
+
9
+ RUN useradd 'pl-doctor'
10
+ RUN echo 'pl-doctor:pl-doctor-password' | chpasswd
11
+
12
+ RUN sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config && \
13
+ sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config
14
+
15
+ RUN apt-get install -y locales && \
16
+ locale-gen en_US.UTF-8
17
+ ENV LANG en_US.UTF-8
18
+
19
+ EXPOSE 22 3001
20
+
21
+ COPY ./test-assets/pub-key.pub /home/pl-doctor/.ssh/authorized_keys
22
+ COPY ./test-assets/simple-server.js /home/pl-doctor/simple-server.js
23
+
24
+ RUN chmod 755 /home/pl-doctor
25
+ RUN chown pl-doctor:pl-doctor /home/pl-doctor
26
+
27
+ RUN chmod +x /home/pl-doctor/simple-server.js
28
+
29
+ CMD ["/bin/bash", "-c", "/usr/sbin/sshd -D & node /home/pl-doctor/simple-server.js"]
@@ -0,0 +1,131 @@
1
+ import type { StartedTestContainer } from 'testcontainers';
2
+ import { GenericContainer } from 'testcontainers';
3
+ import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
4
+ import { mkdir, rm } from 'fs/promises';
5
+ import path from 'path';
6
+ import type { ConnectConfig } from 'ssh2';
7
+ import ssh from 'ssh2';
8
+ import fs from 'fs';
9
+
10
+ const SSH_PORT = [22, 3001];
11
+
12
+ const publicKeyPath = getPathForFile('pub-key.pub');
13
+ const privateKeyPath = getPathForFile('private-key.private');
14
+
15
+ export const localFileUpload = getPathForFile('test-file.txt');
16
+ export const localFileDownload = getPathForFile('test-file-download.txt');
17
+ export const downloadsFolder = path.resolve(__dirname, '..', 'test-assets', 'downloads');
18
+ export const recUpload = path.resolve(__dirname, '..', 'test-assets', 'downloads', 'rec-upload');
19
+
20
+ export async function createTestDirForRecursiveUpload() {
21
+ const pathBase = path.resolve(__dirname, '..', 'test-assets', 'downloads', 'rec-upload', 'sub-1');
22
+ const path2 = path.resolve(__dirname, '..', 'test-assets', 'downloads', 'rec-upload', 'sub-1', 'sub-1-1');
23
+
24
+ await mkdir(pathBase, { recursive: true });
25
+ await mkdir(path2, { recursive: true });
26
+
27
+ for (let i = 0; i < 19; i++) {
28
+ const path2 = path.resolve(__dirname, '..', 'test-assets', 'downloads', 'rec-upload', 'sub-1', `sub-1-${i}`);
29
+ await mkdir(path2, { recursive: true });
30
+
31
+ for (let i = 0; i < 3; i++) {
32
+ writeFileSync(path.resolve(path2, `test-${i}.txt`), `test-${i}`);
33
+ }
34
+ }
35
+
36
+ for (let i = 1; i < 100; i++) {
37
+ writeFileSync(path.resolve(pathBase, `test-${i}.txt`), `test-${i}`);
38
+ }
39
+ writeFileSync(path.resolve(pathBase, `test.txt`), `test-1`);
40
+ writeFileSync(path.resolve(path2, 'test-5.txt'), 'test-5');
41
+ }
42
+
43
+ export function getPathForFile(fileName: string) {
44
+ return path.resolve(__dirname, '..', 'test-assets', fileName);
45
+ }
46
+
47
+ export function generateKeys() {
48
+ const keys = ssh.utils.generateKeyPairSync('ecdsa', { bits: 256, comment: 'node.js rules!' });
49
+ if (!existsSync(publicKeyPath) || !existsSync(privateKeyPath)) {
50
+ writeFileSync(publicKeyPath, keys.public);
51
+ writeFileSync(privateKeyPath, keys.private);
52
+ }
53
+ }
54
+
55
+ export function initPrivateKey(): string {
56
+ generateKeys();
57
+ return readFileSync(privateKeyPath, { encoding: 'utf-8' });
58
+ }
59
+
60
+ export async function initContainer(name: string): Promise<StartedTestContainer> {
61
+ await createTestDirForRecursiveUpload();
62
+
63
+ const fromCacheContainer = await new GenericContainer(`pl-ssh-test-container-${name}:1.0.0`)
64
+ .withExposedPorts(...SSH_PORT)
65
+ .withReuse()
66
+ .withName(`pl-ssh-test-${name}`)
67
+ .start()
68
+ .catch((err) => console.log('No worries, creating a new container'));
69
+
70
+ if (!fromCacheContainer) {
71
+ generateKeys();
72
+ const container1 = await GenericContainer.fromDockerfile(path.resolve(__dirname, '..'))
73
+ .withCache(true)
74
+ .build(`pl-ssh-test-container-${name}:1.0.0`, { deleteOnExit: false });
75
+
76
+ return container1.withExposedPorts(...SSH_PORT).withReuse().start();
77
+ }
78
+
79
+ return fromCacheContainer;
80
+ }
81
+
82
+ export function getContainerHostAndPort(container: StartedTestContainer) {
83
+ return {
84
+ port: container.getMappedPort(22),
85
+ host: container.getHost(),
86
+ };
87
+ }
88
+
89
+ function logToFile(message: string) {
90
+ const logFileName = 'log.txt';
91
+ const logFilePath = path.join(__dirname, logFileName);
92
+
93
+ const timestamp = new Date().toISOString();
94
+ const logMessage = `[${timestamp}] ${message}\n`;
95
+
96
+ fs.appendFile(logFilePath, logMessage, (err) => {
97
+ if (err) {
98
+ console.error('Error writing to log file:', err);
99
+ }
100
+ });
101
+ }
102
+
103
+ export function getConnectionForSsh(container: StartedTestContainer, debug: boolean = false): ConnectConfig {
104
+ const hostData = getContainerHostAndPort(container);
105
+ const privateKey = initPrivateKey();
106
+ const config = {
107
+ host: hostData.host,
108
+ port: hostData.port,
109
+ username: 'pl-doctor',
110
+ privateKey: privateKey,
111
+ debug: debug ? logToFile : undefined,
112
+ };
113
+ logToFile(JSON.stringify(config, null, 4));
114
+ return config;
115
+ }
116
+
117
+ export async function cleanUp(container: StartedTestContainer) {
118
+ await container.stop();
119
+
120
+ if (existsSync(localFileUpload)) {
121
+ unlinkSync(localFileUpload);
122
+ }
123
+
124
+ if (existsSync(localFileDownload)) {
125
+ unlinkSync(localFileDownload);
126
+ }
127
+
128
+ if (existsSync(recUpload)) {
129
+ await rm(recUpload, { recursive: true });
130
+ }
131
+ }