@milaboratories/pl-deployments 1.2.2 → 1.2.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-deployments",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "pl-version": "1.24.0",
5
5
  "description": "MiLaboratories Platforma Backend code service run wrapper",
6
6
  "types": "./dist/index.d.ts",
@@ -29,11 +29,11 @@
29
29
  "@types/jest": "^29.5.14",
30
30
  "@types/node": "~20.16.15",
31
31
  "@types/ssh2": "^1.15.1",
32
- "eslint": "^9.16.0",
32
+ "eslint": "^9.21.0",
33
33
  "jest": "^29.7.0",
34
34
  "prettier": "^3.4.1",
35
- "testcontainers": "^10.16.0",
36
- "ts-jest": "^29.2.5",
35
+ "testcontainers": "^10.18.0",
36
+ "ts-jest": "^29.2.6",
37
37
  "tsconfig-paths": "^4.2.0",
38
38
  "typescript": "~5.5.4",
39
39
  "utility-types": "^3.11.0",
@@ -47,11 +47,11 @@
47
47
  "upath": "^2.0.1",
48
48
  "ssh2": "^1.16.0",
49
49
  "tar": "^7.4.3",
50
- "undici": "~7.2.3",
50
+ "undici": "~7.4.0",
51
51
  "yaml": "^2.6.1",
52
52
  "zod": "~3.23.8",
53
- "@milaboratories/ts-helpers": "^1.1.4",
54
- "@milaboratories/pl-config": "^1.4.3"
53
+ "@milaboratories/pl-config": "^1.4.3",
54
+ "@milaboratories/ts-helpers": "^1.1.4"
55
55
  },
56
56
  "scripts": {
57
57
  "type-check": "tsc --noEmit --composite false",
@@ -153,7 +153,7 @@ export async function downloadArchive(
153
153
 
154
154
  return state;
155
155
  } catch (e: unknown) {
156
- const msg = `downloadArchive: error ${JSON.stringify(e)} occurred, state: ${JSON.stringify(state)}`;
156
+ const msg = `downloadArchive: ${JSON.stringify(e)}, state: ${JSON.stringify(state)}`;
157
157
  logger.error(msg);
158
158
  throw new Error(msg);
159
159
  }
@@ -220,7 +220,7 @@ describe('sshConnect', () => {
220
220
  });
221
221
 
222
222
  it('should timeout if the server is unreachable', async () => {
223
- await expect(SshClient.init(new ConsoleLoggerAdapter(), { ...getConnectionForSsh(testContainer), port: 3233 })).rejects.toThrow('ssh.connect: error occurred: AggregateError');
223
+ await expect(SshClient.init(new ConsoleLoggerAdapter(), { ...getConnectionForSsh(testContainer), port: 3233 })).rejects.toThrow('ssh.connect: AggregateError');
224
224
  });
225
225
  });
226
226
 
@@ -0,0 +1,26 @@
1
+ import { parseGlibcVersion } from './pl';
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ describe('parseGlibcVersion', () => {
5
+ it('correctly parses glibc version from ldd output', () => {
6
+ // Standard GNU libc outputs
7
+ expect(parseGlibcVersion('ldd (GNU libc) 2.28')).toBe(2.28);
8
+ expect(parseGlibcVersion('ldd (GNU libc) 2.39')).toBe(2.39);
9
+
10
+ // Ubuntu-style output
11
+ expect(parseGlibcVersion('ldd (Ubuntu GLIBC 2.31-0ubuntu9.9) 2.31')).toBe(2.31);
12
+
13
+ // Debian-style output
14
+ expect(parseGlibcVersion('ldd (Debian GLIBC 2.28-10) 2.28')).toBe(2.28);
15
+
16
+ // Different formatting with extra text
17
+ expect(parseGlibcVersion('ldd version 2.35, Copyright (C) 2022 Free Software Foundation, Inc.')).toBe(2.35);
18
+ });
19
+
20
+ it('throws error when glibc version cannot be parsed', () => {
21
+ // Invalid outputs
22
+ expect(() => parseGlibcVersion('ldd: command not found')).toThrow();
23
+ expect(() => parseGlibcVersion('some random output')).toThrow();
24
+ expect(() => parseGlibcVersion('')).toThrow();
25
+ });
26
+ });
package/src/ssh/pl.ts CHANGED
@@ -9,7 +9,7 @@ import * as plpath from './pl_paths';
9
9
  import { getDefaultPlVersion } from '../common/pl_version';
10
10
 
11
11
  import net from 'net';
12
- import type { PlLicenseMode, SshPlConfigGenerationResult } from '@milaboratories/pl-config';
12
+ import type { PlConfig, PlLicenseMode, SshPlConfigGenerationResult } from '@milaboratories/pl-config';
13
13
  import { generateSshPlConfigs, getFreePort } from '@milaboratories/pl-config';
14
14
  import type { SupervisorStatus } from './supervisord';
15
15
  import { supervisorStatus, supervisorStop as supervisorCtlShutdown, generateSupervisordConfig, supervisorCtlStart } from './supervisord';
@@ -17,6 +17,8 @@ import type { ConnectionInfo, SshPlPorts } from './connection_info';
17
17
  import { newConnectionInfo, parseConnectionInfo, stringifyConnectionInfo } from './connection_info';
18
18
  import type { PlBinarySourceDownload } from '../common/pl_binary';
19
19
 
20
+ const minRequiredGlibcVersion = 2.28;
21
+
20
22
  export class SshPl {
21
23
  private initState: PlatformaInitState = {};
22
24
  constructor(
@@ -67,7 +69,7 @@ export class SshPl {
67
69
  return await this.checkIsAliveWithInterval();
68
70
  }
69
71
  } catch (e: unknown) {
70
- const msg = `SshPl.start: error occurred ${e}`;
72
+ const msg = `SshPl.start: ${e}`;
71
73
  this.logger.error(msg);
72
74
  throw new Error(msg);
73
75
  }
@@ -85,7 +87,7 @@ export class SshPl {
85
87
  return await this.checkIsAliveWithInterval(undefined, undefined, false);
86
88
  }
87
89
  } catch (e: unknown) {
88
- const msg = `PlSsh.stop: error occurred ${e}`;
90
+ const msg = `PlSsh.stop: ${e}`;
89
91
  this.logger.error(msg);
90
92
  throw new Error(msg);
91
93
  }
@@ -115,6 +117,8 @@ export class SshPl {
115
117
  public async platformaInit(options: SshPlConfig): Promise<ConnectionInfo> {
116
118
  const state: PlatformaInitState = { localWorkdir: options.localWorkdir };
117
119
 
120
+ const { onProgress } = options;
121
+
118
122
  try {
119
123
  // merge options with default ops.
120
124
  const ops: SshPlConfig = {
@@ -122,10 +126,22 @@ export class SshPl {
122
126
  ...options,
123
127
  };
124
128
  state.plBinaryOps = ops.plBinary;
129
+
130
+ await onProgress?.('Detecting server architecture...');
125
131
  state.arch = await this.getArch();
132
+ await onProgress?.('Server architecture detected.');
133
+
134
+ await onProgress?.('Fetching user home directory...');
126
135
  state.remoteHome = await this.getUserHomeDirectory();
136
+ await onProgress?.('User home directory retrieved.');
137
+
138
+ await onProgress?.('Checking platform status...');
127
139
  state.alive = await this.isAlive();
128
140
 
141
+ if (state.alive.allAlive) {
142
+ await onProgress?.('All required services are running.');
143
+ }
144
+
129
145
  if (state.alive.allAlive) {
130
146
  state.userCredentials = await this.getUserCredentials(state.remoteHome);
131
147
  if (!state.userCredentials) {
@@ -136,15 +152,26 @@ export class SshPl {
136
152
  state.needRestart = !(sameGA && samePlVersion);
137
153
  this.logger.info(`SshPl.platformaInit: need restart? ${state.needRestart}`);
138
154
 
139
- if (!state.needRestart)
155
+ if (!state.needRestart) {
156
+ await onProgress?.('Server setup completed.');
140
157
  return state.userCredentials;
158
+ }
141
159
 
160
+ await onProgress?.('Stopping services...');
142
161
  await this.stop();
143
162
  }
144
163
 
164
+ await onProgress?.('Downloading and uploading required binaries...');
165
+
166
+ const glibcVersion = await getGlibcVersion(this.logger, this.sshClient);
167
+ if (glibcVersion < minRequiredGlibcVersion)
168
+ throw new Error(`glibc version ${glibcVersion} is too old. Version ${minRequiredGlibcVersion} or higher is required for Platforma.`);
169
+
145
170
  const downloadRes = await this.downloadBinariesAndUploadToTheServer(
146
171
  ops.localWorkdir, ops.plBinary!, state.remoteHome, state.arch,
147
172
  );
173
+ await onProgress?.('All required binaries have been downloaded and uploaded.');
174
+
148
175
  state.binPaths = { ...downloadRes, history: undefined };
149
176
  state.downloadedBinaries = downloadRes.history;
150
177
 
@@ -154,6 +181,7 @@ export class SshPl {
154
181
  throw new Error(`SshPl.platformaInit: remote ports are not defined`);
155
182
  }
156
183
 
184
+ await onProgress?.('Generating server configuration...');
157
185
  const config = await generateSshPlConfigs({
158
186
  logger: this.logger,
159
187
  workingDir: plpath.workDir(state.remoteHome),
@@ -172,9 +200,13 @@ export class SshPl {
172
200
  },
173
201
  licenseMode: ops.license,
174
202
  useGlobalAccess: notEmpty(ops.useGlobalAccess),
203
+ plConfigPostprocessing: ops.plConfigPostprocessing,
175
204
  });
176
205
  state.generatedConfig = { ...config, filesToCreate: { skipped: 'it is too wordy' } };
177
206
 
207
+ await onProgress?.('Server configuration generated.');
208
+
209
+ await onProgress?.('Generating folder structure...');
178
210
  for (const [filePath, content] of Object.entries(config.filesToCreate)) {
179
211
  await this.sshClient.writeFileOnTheServer(filePath, content);
180
212
  this.logger.info(`Created file ${filePath}`);
@@ -184,7 +216,9 @@ export class SshPl {
184
216
  await this.sshClient.ensureRemoteDirCreated(dir);
185
217
  this.logger.info(`Created directory ${dir}`);
186
218
  }
219
+ await onProgress?.('Folder structure created.');
187
220
 
221
+ await onProgress?.('Writing supervisord configuration...');
188
222
  const supervisorConfig = generateSupervisordConfig(
189
223
  config.minioConfig.storageDir,
190
224
  config.minioConfig.envs,
@@ -199,7 +233,9 @@ export class SshPl {
199
233
  if (!writeResult) {
200
234
  throw new Error(`Can not write supervisord config on the server ${plpath.workDir(state.remoteHome)}`);
201
235
  }
236
+ await onProgress?.('Supervisord configuration written.');
202
237
 
238
+ await onProgress?.('Saving connection information...');
203
239
  state.connectionInfo = newConnectionInfo(
204
240
  config.plUser,
205
241
  config.plPassword,
@@ -211,14 +247,18 @@ export class SshPl {
211
247
  plpath.connectionInfo(state.remoteHome),
212
248
  stringifyConnectionInfo(state.connectionInfo),
213
249
  );
250
+ await onProgress?.('Connection information saved.');
214
251
 
252
+ await onProgress?.('Starting Platforma on the server...');
215
253
  await this.start();
216
254
  state.started = true;
217
255
  this.initState = state;
218
256
 
257
+ await onProgress?.('Platforma has been started successfully.');
258
+
219
259
  return state.connectionInfo;
220
260
  } catch (e: unknown) {
221
- const msg = `SshPl.platformaInit: error occurred: ${e}, state: ${JSON.stringify(state)}`;
261
+ const msg = `SshPl.platformaInit: ${e}, state: ${JSON.stringify(state)}`;
222
262
  this.logger.error(msg);
223
263
 
224
264
  throw new Error(msg);
@@ -259,7 +299,7 @@ export class SshPl {
259
299
  downloadedPl: plpath.platformaBin(remoteHome, arch.arch),
260
300
  };
261
301
  } catch (e: unknown) {
262
- const msg = `SshPl.downloadBinariesAndUploadToServer: error ${e} occurred, state: ${JSON.stringify(state)}`;
302
+ const msg = `SshPl.downloadBinariesAndUploadToServer: ${e}, state: ${JSON.stringify(state)}`;
263
303
  this.logger.error(msg);
264
304
  throw e;
265
305
  }
@@ -312,13 +352,19 @@ export class SshPl {
312
352
  await this.sshClient.uploadFile(state.localArchivePath, state.remoteArchivePath);
313
353
  state.uploadDone = true;
314
354
 
355
+ try {
356
+ await this.sshClient.exec('hash tar');
357
+ } catch (_) {
358
+ throw new Error(`tar is not installed on the server. Please install it before running Platforma.`);
359
+ }
360
+
315
361
  // TODO: Create a proper archive to avoid xattr warnings
316
362
  const untarResult = await this.sshClient.exec(
317
363
  `tar --warning=no-all -xvf ${state.remoteArchivePath} --directory=${state.remoteDir}`,
318
364
  );
319
365
 
320
366
  if (untarResult.stderr)
321
- throw Error(`downloadAndUntar: untar: stderr occurred: ${untarResult.stderr}, stdout: ${untarResult.stdout}`);
367
+ throw new Error(`downloadAndUntar: untar: stderr occurred: ${untarResult.stderr}, stdout: ${untarResult.stdout}`);
322
368
 
323
369
  state.untarDone = true;
324
370
 
@@ -441,6 +487,10 @@ export type SshPlConfig = {
441
487
  license: PlLicenseMode;
442
488
  useGlobalAccess?: boolean;
443
489
  plBinary?: PlBinarySourceDownload;
490
+
491
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
492
+ onProgress?: (...args: any) => Promise<any>;
493
+ plConfigPostprocessing?: (config: PlConfig) => PlConfig;
444
494
  };
445
495
 
446
496
  const defaultSshPlConfig: Pick<
@@ -489,3 +539,30 @@ type PlatformaInitState = {
489
539
  connectionInfo?: ConnectionInfo;
490
540
  started?: boolean;
491
541
  };
542
+
543
+ /**
544
+ * Gets the glibc version on the remote system
545
+ * @returns The glibc version as a number
546
+ * @throws Error if version cannot be determined
547
+ */
548
+ async function getGlibcVersion(logger: MiLogger, sshClient: SshClient): Promise <number> {
549
+ try {
550
+ const { stdout, stderr } = await sshClient.exec('ldd --version | head -n 1');
551
+ if (stderr) {
552
+ throw new Error(`Failed to check glibc version: ${stderr}`);
553
+ }
554
+ return parseGlibcVersion(stdout);
555
+ } catch (e: unknown) {
556
+ logger.error(`glibc version check failed: ${e}`);
557
+ throw e;
558
+ }
559
+ }
560
+
561
+ export function parseGlibcVersion(output: string): number {
562
+ const versionMatch = output.match(/\d+\.\d+/);
563
+ if (!versionMatch) {
564
+ throw new Error(`Could not parse glibc version from: ${output}`);
565
+ }
566
+
567
+ return parseFloat(versionMatch[0]);
568
+ }
package/src/ssh/ssh.ts CHANGED
@@ -79,7 +79,7 @@ export class SshClient {
79
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
80
  this.client.exec(command, (err: any, stream: ClientChannel) => {
81
81
  if (err) {
82
- return reject(`ssh.exec: ${command}, error occurred: ${err}`);
82
+ return reject(`ssh.exec: ${command}: ${err}`);
83
83
  }
84
84
 
85
85
  let stdout = '';
@@ -89,7 +89,7 @@ export class SshClient {
89
89
  if (code === 0) {
90
90
  resolve({ stdout, stderr });
91
91
  } else {
92
- reject(new Error(`Command ${command} exited with code ${code}`));
92
+ reject(new Error(`Command ${command} exited with code ${code}, stdout: ${stdout}, stderr: ${stderr}`));
93
93
  }
94
94
  }).on('data', (data: ArrayBuffer) => {
95
95
  stdout += data.toString();
@@ -419,7 +419,7 @@ export class SshClient {
419
419
  return new Promise((resolve, reject) => {
420
420
  sftp.readFile(remotePath, (err, buffer) => {
421
421
  if (err) {
422
- return reject(new Error(`ssh.readFile: err occurred ${err}`));
422
+ return reject(new Error(`ssh.readFile: ${err}`));
423
423
  }
424
424
  resolve(buffer.toString());
425
425
  });
@@ -495,7 +495,7 @@ export class SshClient {
495
495
  resolve(undefined);
496
496
  })
497
497
  .catch((err) => {
498
- const msg = `uploadFileUsingExistingSftp: error ${err} occurred`;
498
+ const msg = `uploadFileUsingExistingSftp: ${err}`;
499
499
  this.logger.error(msg);
500
500
  reject(new Error(msg));
501
501
  });
@@ -666,7 +666,7 @@ async function connect(
666
666
 
667
667
  client.on('error', (err: unknown) => {
668
668
  onError(err);
669
- reject(new Error(`ssh.connect: error occurred: ${err}`));
669
+ reject(new Error(`ssh.connect: ${err}`));
670
670
  });
671
671
 
672
672
  client.on('close', () => {