@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/dist/index.js +17 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +633 -603
- package/dist/index.mjs.map +1 -1
- package/dist/ssh/pl.d.ts +4 -1
- package/dist/ssh/pl.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/common/pl_binary_download.ts +1 -1
- package/src/ssh/__tests__/ssh-docker.test.ts +1 -1
- package/src/ssh/pl.test.ts +26 -0
- package/src/ssh/pl.ts +84 -7
- package/src/ssh/ssh.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-deployments",
|
|
3
|
-
"version": "1.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.
|
|
32
|
+
"eslint": "^9.21.0",
|
|
33
33
|
"jest": "^29.7.0",
|
|
34
34
|
"prettier": "^3.4.1",
|
|
35
|
-
"testcontainers": "^10.
|
|
36
|
-
"ts-jest": "^29.2.
|
|
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.
|
|
50
|
+
"undici": "~7.4.0",
|
|
51
51
|
"yaml": "^2.6.1",
|
|
52
52
|
"zod": "~3.23.8",
|
|
53
|
-
"@milaboratories/
|
|
54
|
-
"@milaboratories/
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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}
|
|
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:
|
|
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:
|
|
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:
|
|
669
|
+
reject(new Error(`ssh.connect: ${err}`));
|
|
670
670
|
});
|
|
671
671
|
|
|
672
672
|
client.on('close', () => {
|