@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
package/src/ssh/pl.ts ADDED
@@ -0,0 +1,451 @@
1
+ import type * as ssh from 'ssh2';
2
+ import { SshClient } from './ssh';
3
+ import type { MiLogger } from '@milaboratories/ts-helpers';
4
+ import { sleep, notEmpty, fileExists } from '@milaboratories/ts-helpers';
5
+ import type { DownloadBinaryResult } from '../common/pl_binary_download';
6
+ import { downloadBinaryNoExtract } from '../common/pl_binary_download';
7
+ import upath from 'upath';
8
+ import * as plpath from './pl_paths';
9
+ import { getDefaultPlVersion } from '../common/pl_version';
10
+
11
+ import net from 'net';
12
+ import type { SshPlConfigGenerationResult } from '@milaboratories/pl-config';
13
+ import { generateSshPlConfigs, getFreePort } from '@milaboratories/pl-config';
14
+ import { supervisorStatus, supervisorStop as supervisorCtlShutdown, generateSupervisordConfig, supervisorCtlStart } from './supervisord';
15
+
16
+ export class SshPl {
17
+ private initState: PlatformaInitState = {};
18
+ constructor(
19
+ public readonly logger: MiLogger,
20
+ public readonly sshClient: SshClient,
21
+ private readonly username: string,
22
+ ) {}
23
+
24
+ public info() {
25
+ return {
26
+ username: this.username,
27
+ initState: this.initState,
28
+ };
29
+ }
30
+
31
+ public static async init(logger: MiLogger, config: ssh.ConnectConfig): Promise<SshPl> {
32
+ try {
33
+ const sshClient = await SshClient.init(logger, config);
34
+ return new SshPl(logger, sshClient, notEmpty(config.username));
35
+ } catch (e: unknown) {
36
+ logger.error(`Connection error in SshClient.init: ${e}`);
37
+ throw e;
38
+ }
39
+ }
40
+
41
+ public async isAlive(): Promise<boolean> {
42
+ const arch = await this.getArch();
43
+ const remoteHome = await this.getUserHomeDirectory();
44
+
45
+ try {
46
+ return await supervisorStatus(this.logger, this.sshClient, remoteHome, arch.arch);
47
+ } catch (e: unknown) {
48
+ // probably there are no supervisor on the server.
49
+ return false;
50
+ }
51
+ }
52
+
53
+ public async start() {
54
+ const arch = await this.getArch();
55
+ const remoteHome = await this.getUserHomeDirectory();
56
+
57
+ try {
58
+ await supervisorCtlStart(this.sshClient, remoteHome, arch.arch);
59
+
60
+ // We are waiting for Platforma to run to ensure that it has started.
61
+ return await this.checkIsAliveWithInterval();
62
+ } catch (e: unknown) {
63
+ const msg = `ssh.start: error occurred ${e}`
64
+ this.logger.error(msg);
65
+ throw new Error(msg);
66
+ }
67
+ }
68
+
69
+ public async stop() {
70
+ const arch = await this.getArch();
71
+ const remoteHome = await this.getUserHomeDirectory();
72
+
73
+ try {
74
+ await supervisorCtlShutdown(this.sshClient, remoteHome, arch.arch);
75
+ return await this.checkIsAliveWithInterval(undefined, undefined, false);
76
+ } catch (e: unknown) {
77
+ const msg = `ssh.stop: error occurred ${e}`
78
+ this.logger.error(msg);
79
+ throw new Error(msg)
80
+ }
81
+ }
82
+
83
+ public async platformaInit(localWorkdir: string): Promise<SshInitReturnTypes> {
84
+ const state: PlatformaInitState = { localWorkdir };
85
+
86
+ try {
87
+ state.arch = await this.getArch();
88
+ state.remoteHome = await this.getUserHomeDirectory();
89
+ state.isAlive = await this.isAlive();
90
+
91
+ if (state.isAlive) {
92
+ state.userCredentials = await this.getUserCredentials(state.remoteHome);
93
+ if (!state.userCredentials) {
94
+ throw new Error(`SshPl.platformaInit: platforma is alive but userCredentials are not found`);
95
+ }
96
+ return state.userCredentials;
97
+ }
98
+
99
+ const downloadRes = await this.downloadBinariesAndUploadToTheServer(
100
+ localWorkdir, state.remoteHome, state.arch,
101
+ );
102
+ state.binPaths = { ...downloadRes, history: undefined };
103
+ state.downloadedBinaries = downloadRes.history;
104
+
105
+ state.ports = await this.fetchPorts(state.remoteHome, state.arch);
106
+
107
+ if (!state.ports.debug.remote || !state.ports.grpc.remote || !state.ports.minioPort.remote || !state.ports.minioConsolePort.remote || !state.ports.monitoring.remote) {
108
+ throw new Error(`SshPl.platformaInit: remote ports are not defined`);
109
+ }
110
+
111
+ const config = await generateSshPlConfigs({
112
+ logger: this.logger,
113
+ workingDir: plpath.workDir(state.remoteHome),
114
+ portsMode: {
115
+ type: 'customWithMinio',
116
+ ports: {
117
+ debug: state.ports.debug.remote,
118
+ grpc: state.ports.grpc.remote,
119
+ minio: state.ports.minioPort.remote,
120
+ minioConsole: state.ports.minioConsolePort.remote,
121
+ monitoring: state.ports.monitoring.remote,
122
+
123
+ grpcLocal: state.ports.grpc.local,
124
+ minioLocal: state.ports.minioPort.local,
125
+ },
126
+ },
127
+ licenseMode: {
128
+ type: 'env',
129
+ },
130
+ });
131
+ state.generatedConfig = { ...config, filesToCreate: { skipped: 'it is too wordy' } };
132
+
133
+ for (const [filePath, content] of Object.entries(config.filesToCreate)) {
134
+ await this.sshClient.writeFileOnTheServer(filePath, content);
135
+ this.logger.info(`Created file ${filePath}`);
136
+ }
137
+
138
+ for (const dir of config.dirsToCreate) {
139
+ await this.sshClient.createRemoteDirectory(dir);
140
+ this.logger.info(`Created directory ${dir}`);
141
+ }
142
+
143
+ const supervisorConfig = generateSupervisordConfig(
144
+ config.minioConfig.storageDir,
145
+ config.minioConfig.envs,
146
+ await this.getFreePortForPlatformaOnServer(state.remoteHome, state.arch),
147
+ config.workingDir,
148
+ config.plConfig.configPath,
149
+ state.binPaths.minioRelPath,
150
+ state.binPaths.downloadedPl,
151
+ );
152
+
153
+ const writeResult = await this.sshClient.writeFileOnTheServer(plpath.supervisorConf(state.remoteHome), supervisorConfig);
154
+ if (!writeResult) {
155
+ throw new Error(`Can not write supervisord config on the server ${plpath.workDir(state.remoteHome)}`);
156
+ }
157
+
158
+ state.connectionInfo = {
159
+ plUser: config.plUser,
160
+ plPassword: config.plPassword,
161
+ ports: state.ports,
162
+ };
163
+ await this.sshClient.writeFileOnTheServer(
164
+ plpath.connectionInfo(state.remoteHome),
165
+ JSON.stringify(state.connectionInfo, undefined, 2),
166
+ );
167
+
168
+ await this.start();
169
+ state.started = true;
170
+ this.initState = state;
171
+
172
+ return {
173
+ plUser: config.plUser,
174
+ plPassword: config.plPassword,
175
+ ports: state.ports,
176
+ };
177
+ } catch (e: unknown) {
178
+ const msg = `SshPl.platformaInit: error occurred: ${e}, state: ${JSON.stringify(state)}`;
179
+ this.logger.error(msg);
180
+
181
+ throw new Error(msg);
182
+ }
183
+ }
184
+
185
+ public async downloadBinariesAndUploadToTheServer(
186
+ localWorkdir: string,
187
+ remoteHome: string,
188
+ arch: Arch,
189
+ ) {
190
+ const state: DownloadAndUntarState[] = [];
191
+ try {
192
+ const pl = await this.downloadAndUntar(
193
+ localWorkdir, remoteHome, arch,
194
+ 'pl', `pl-${getDefaultPlVersion()}`,
195
+ );
196
+ state.push(pl);
197
+
198
+ const supervisor = await this.downloadAndUntar(
199
+ localWorkdir, remoteHome, arch,
200
+ 'supervisord', plpath.supervisordDirName,
201
+ );
202
+ state.push(supervisor);
203
+
204
+ const minioPath = plpath.minioBin(remoteHome, arch.arch);
205
+ const minio = await this.downloadAndUntar(
206
+ localWorkdir, remoteHome, arch,
207
+ 'minio', plpath.minioDirName,
208
+ );
209
+ state.push(minio);
210
+ await this.sshClient.chmod(minioPath, 0o750);
211
+
212
+ return {
213
+ history: state,
214
+ minioRelPath: minioPath,
215
+ downloadedPl: plpath.platformaBin(remoteHome, arch.arch),
216
+ };
217
+ } catch (e: unknown) {
218
+ const msg = `SshPl.downloadBinariesAndUploadToServer: error ${e} occurred, state: ${JSON.stringify(state)}`;
219
+ this.logger.error(msg);
220
+ throw e;
221
+ }
222
+ }
223
+
224
+ /** We have to extract pl in the remote server,
225
+ * because Windows doesn't support symlinks
226
+ * that are found in linux pl binaries tgz archive.
227
+ * For this reason, we extract all to the remote server. */
228
+ public async downloadAndUntar(
229
+ localWorkdir: string,
230
+ remoteHome: string,
231
+ arch: Arch,
232
+ softwareName: string,
233
+ tgzName: string,
234
+ ): Promise<DownloadAndUntarState> {
235
+
236
+ const state: DownloadAndUntarState = {};
237
+ state.binBasePath = plpath.binariesDir(remoteHome);
238
+ await this.sshClient.createRemoteDirectory(state.binBasePath);
239
+ state.binBasePathCreated = true;
240
+
241
+ let downloadBinaryResult: DownloadBinaryResult | null = null;
242
+ const attempts = 5;
243
+ for (let i = 1; i <= attempts; i++) {
244
+ try {
245
+ downloadBinaryResult = await downloadBinaryNoExtract(
246
+ this.logger,
247
+ localWorkdir,
248
+ softwareName,
249
+ tgzName,
250
+ arch.arch, arch.platform,
251
+ );
252
+ break;
253
+ } catch(e: unknown) {
254
+ await sleep(300);
255
+ if (i == attempts) {
256
+ throw new Error(`downloadAndUntar: ${attempts} attempts, last error: ${e}`);
257
+ }
258
+ }
259
+ }
260
+ state.downloadResult = notEmpty(downloadBinaryResult);
261
+
262
+ state.localArchivePath = upath.resolve(state.downloadResult.archivePath);
263
+ state.remoteDir = upath.join(state.binBasePath, state.downloadResult.baseName);
264
+ state.remoteArchivePath = state.remoteDir + '.tgz';
265
+
266
+ await this.sshClient.createRemoteDirectory(state.remoteDir);
267
+ await this.sshClient.uploadFile(state.localArchivePath, state.remoteArchivePath);
268
+
269
+ const untarResult = await this.sshClient.exec(
270
+ `tar xvf ${state.remoteArchivePath} --directory=${state.remoteDir}`,
271
+ );
272
+ if (untarResult.stderr)
273
+ throw new Error(`downloadAndUntar: untar: stderr occurred: ${untarResult.stderr}, stdout: ${untarResult.stdout}`);
274
+
275
+ state.plUntarDone = true;
276
+
277
+ return state;
278
+ }
279
+
280
+ public async needDownload(remoteHome: string, arch: Arch) {
281
+ const checkPathSupervisor = plpath.supervisorBin(remoteHome, arch.arch);
282
+ const checkPathMinio = plpath.minioDir(remoteHome, arch.arch);
283
+ const checkPathPlatforma = plpath.platformaBin(remoteHome, arch.arch);
284
+
285
+ if (!await this.sshClient.checkFileExists(checkPathPlatforma)
286
+ || !await this.sshClient.checkFileExists(checkPathMinio)
287
+ || !await this.sshClient.checkFileExists(checkPathSupervisor)) {
288
+ return true;
289
+ }
290
+
291
+ return false;
292
+ }
293
+
294
+ public async checkIsAliveWithInterval(interval: number = 1000, count = 15, shouldStart = true) {
295
+ const maxMs = count * interval;
296
+
297
+ let total = 0;
298
+ let alive = await this.isAlive();
299
+ while (shouldStart ? !alive : alive) {
300
+ await sleep(interval);
301
+ total += interval;
302
+ if (total > maxMs) {
303
+ throw new Error(`isAliveWithInterval: The process did not ${shouldStart ? 'started' : 'stopped'} after ${maxMs} ms.`);
304
+ }
305
+ alive = await this.isAlive();
306
+ }
307
+ }
308
+
309
+ public async getUserCredentials(remoteHome: string): Promise<SshInitReturnTypes> {
310
+ const connectionInfo = await this.sshClient.readFile(plpath.connectionInfo(remoteHome));
311
+ return JSON.parse(connectionInfo) as SshInitReturnTypes;
312
+ }
313
+
314
+ public async fetchPorts(remoteHome: string, arch: Arch): Promise<SshPlatformaPorts> {
315
+ const ports: SshPlatformaPorts = {
316
+ grpc: {
317
+ local: await getFreePort(),
318
+ remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
319
+ },
320
+ monitoring: {
321
+ local: await getFreePort(),
322
+ remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
323
+ },
324
+ debug: {
325
+ local: await getFreePort(),
326
+ remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
327
+ },
328
+ minioPort: {
329
+ local: await getFreePort(),
330
+ remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
331
+ },
332
+ minioConsolePort: {
333
+ local: await getFreePort(),
334
+ remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
335
+ },
336
+ };
337
+
338
+ return ports;
339
+ }
340
+
341
+ public async getLocalFreePort(): Promise<number> {
342
+ return new Promise((res) => {
343
+ const srv = net.createServer();
344
+ srv.listen(0, () => {
345
+ const port = (srv.address() as net.AddressInfo).port;
346
+ srv.close((_) => res(port));
347
+ });
348
+ });
349
+ }
350
+
351
+ public async getFreePortForPlatformaOnServer(remoteHome: string, arch: Arch): Promise<number> {
352
+ const freePortBin = plpath.platformaFreePortBin(remoteHome, arch.arch);
353
+
354
+ const { stdout, stderr } = await this.sshClient.exec(`${freePortBin}`);
355
+ if (stderr) {
356
+ throw new Error(`getFreePortForPlatformaOnServer: stderr is not empty: ${stderr}, stdout: ${stdout}`);
357
+ }
358
+
359
+ return +stdout;
360
+ }
361
+
362
+ public async getArch(): Promise<Arch> {
363
+ const { stdout, stderr } = await this.sshClient.exec('uname -s && uname -m');
364
+ if (stderr)
365
+ throw new Error(`getArch: stderr is not empty: ${stderr}, stdout: ${stdout}`);
366
+
367
+ const arr = stdout.split('\n');
368
+
369
+ return {
370
+ platform: arr[0],
371
+ arch: arr[1],
372
+ };
373
+ }
374
+
375
+ public async getUserHomeDirectory() {
376
+ const { stdout, stderr } = await this.sshClient.exec('echo $HOME');
377
+
378
+ if (stderr) {
379
+ const home = `/home/${this.username}`;
380
+ console.warn(`getUserHomeDirectory: stderr is not empty: ${stderr}, stdout: ${stdout}, will get a default home: ${home}`);
381
+
382
+ return home;
383
+ }
384
+
385
+ return stdout.trim();
386
+ }
387
+ }
388
+
389
+ export type SshPlatformaPorts = {
390
+ grpc: {
391
+ local: number;
392
+ remote: number;
393
+ };
394
+ monitoring: {
395
+ local: number;
396
+ remote: number;
397
+ };
398
+ debug: {
399
+ local: number;
400
+ remote: number;
401
+ };
402
+ minioPort: {
403
+ local: number;
404
+ remote: number;
405
+ };
406
+ minioConsolePort: {
407
+ local: number;
408
+ remote: number;
409
+ };
410
+ };
411
+
412
+ type Arch = { platform: string; arch: string };
413
+
414
+ export type SshInitReturnTypes = {
415
+ plUser: string;
416
+ plPassword: string;
417
+ ports: SshPlatformaPorts;
418
+ } | null;
419
+
420
+ type BinPaths = {
421
+ history?: DownloadAndUntarState[];
422
+ minioRelPath: string;
423
+ downloadedPl: any;
424
+ };
425
+
426
+ type DownloadAndUntarState = {
427
+ binBasePath?: string;
428
+ binBasePathCreated?: boolean;
429
+ downloadResult?: DownloadBinaryResult;
430
+ attempts?: number;
431
+
432
+ localArchivePath?: string;
433
+ remoteDir?: string;
434
+ remoteArchivePath?: string;
435
+ plUploadDone?: boolean;
436
+ plUntarDone?: boolean;
437
+ };
438
+
439
+ type PlatformaInitState = {
440
+ localWorkdir?: string;
441
+ arch?: Arch;
442
+ remoteHome?: string;
443
+ isAlive?: boolean;
444
+ userCredentials?: SshInitReturnTypes;
445
+ downloadedBinaries?: DownloadAndUntarState[];
446
+ binPaths?: BinPaths;
447
+ ports?: SshPlatformaPorts;
448
+ generatedConfig?: SshPlConfigGenerationResult;
449
+ connectionInfo?: SshInitReturnTypes;
450
+ started?: boolean;
451
+ };
@@ -0,0 +1,57 @@
1
+ /** Just a lot of hardcoded paths of our current ssh deployment. */
2
+
3
+ import upath from 'upath';
4
+ import { newArch } from '../common/os_and_arch';
5
+ import { getDefaultPlVersion } from '../common/pl_version';
6
+
7
+ export const minioDirName = 'minio-2024-12-18T13-15-44Z';
8
+ export const supervisordDirName = 'supervisord-0.7.3';
9
+ export const supervisordSubDirName = 'supervisord_0.7.3_Linux_64-bit';
10
+
11
+ export function workDir(remoteHome: string) {
12
+ return upath.join(remoteHome, 'platforma_ssh');
13
+ }
14
+
15
+ export function binariesDir(remoteHome: string) {
16
+ return upath.join(remoteHome, 'platforma_ssh', 'binaries');
17
+ }
18
+
19
+ export function platformaBaseDir(remoteHome: string, arch: string) {
20
+ return upath.join(binariesDir(remoteHome), `pl-${getDefaultPlVersion()}-${newArch(arch)}`);
21
+ }
22
+
23
+ export function platformaDir(remoteHome: string, arch: string) {
24
+ return upath.join(platformaBaseDir(remoteHome, arch), 'binaries');
25
+ }
26
+
27
+ export function platformaBin(remoteHome: string, arch: string) {
28
+ return upath.join(platformaDir(remoteHome, arch), 'platforma');
29
+ }
30
+
31
+ export function platformaFreePortBin(remoteHome: string, arch: string): string {
32
+ return upath.join(platformaDir(remoteHome, arch), 'free-port');
33
+ }
34
+
35
+ export function minioDir(remoteHome: string, arch: string) {
36
+ return upath.join(binariesDir(remoteHome), `minio-2024-12-18T13-15-44Z-${newArch(arch)}`);
37
+ }
38
+
39
+ export function minioBin(remoteHome: string, arch: string) {
40
+ return upath.join(minioDir(remoteHome, arch), 'minio');
41
+ }
42
+
43
+ export function supervisorBinDir(remoteHome: string, arch: string) {
44
+ return upath.join(binariesDir(remoteHome), `supervisord-0.7.3-${newArch(arch)}`, supervisordSubDirName);
45
+ }
46
+
47
+ export function supervisorBin(remoteHome: string, arch: string): string {
48
+ return upath.join(supervisorBinDir(remoteHome, arch), 'supervisord');
49
+ }
50
+
51
+ export function supervisorConf(remoteHome: string) {
52
+ return upath.join(workDir(remoteHome), 'supervisor.conf');
53
+ }
54
+
55
+ export function connectionInfo(remoteHome: string) {
56
+ return upath.join(workDir(remoteHome), `connection.txt`);
57
+ }