@milaboratories/pl-deployments 1.1.10 → 1.1.12

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.
@@ -0,0 +1,62 @@
1
+ /** We store all info about the connection on the server,
2
+ * so that another client could read the file and connect from another machine. */
3
+ import { z } from 'zod';
4
+
5
+ //
6
+ // Types
7
+ //
8
+
9
+ export const PortPair = z.object({
10
+ local: z.number(),
11
+ remote: z.number(),
12
+ });
13
+ /** The pair of ports for forwarding. */
14
+ export type PortPair = z.infer<typeof PortPair>;
15
+
16
+ export const SshPlPorts = z.object({
17
+ grpc: PortPair,
18
+ monitoring: PortPair,
19
+ debug: PortPair,
20
+ minioPort: PortPair,
21
+ minioConsolePort: PortPair,
22
+ });
23
+ /** All info about ports that are forwarded. */
24
+ export type SshPlPorts = z.infer<typeof SshPlPorts>;
25
+
26
+ export const ConnectionInfo = z.object({
27
+ plUser: z.string(),
28
+ plPassword: z.string(),
29
+ ports: SshPlPorts,
30
+
31
+ // It's false by default because it was added later,
32
+ // and in some deployments there won't be useGlobalAccess flag in the file.
33
+ useGlobalAccess: z.boolean().default(false),
34
+ });
35
+ /** The content of the file that holds all the info about the connection on the remote server. */
36
+ export type ConnectionInfo = z.infer<typeof ConnectionInfo>;
37
+
38
+ //
39
+ // Funcs
40
+ //
41
+
42
+ export function newConnectionInfo(
43
+ plUser: string,
44
+ plPassword: string,
45
+ ports: SshPlPorts,
46
+ useGlobalAccess: boolean,
47
+ ): ConnectionInfo {
48
+ return {
49
+ plUser,
50
+ plPassword,
51
+ ports,
52
+ useGlobalAccess,
53
+ };
54
+ }
55
+
56
+ export function parseConnectionInfo(content: string): ConnectionInfo {
57
+ return ConnectionInfo.parse(JSON.parse(content));
58
+ }
59
+
60
+ export function stringifyConnectionInfo(conn: ConnectionInfo): string {
61
+ return JSON.stringify(conn, undefined, 2);
62
+ }
package/src/ssh/pl.ts CHANGED
@@ -11,7 +11,10 @@ import { getDefaultPlVersion } from '../common/pl_version';
11
11
  import net from 'net';
12
12
  import type { PlLicenseMode, SshPlConfigGenerationResult } from '@milaboratories/pl-config';
13
13
  import { generateSshPlConfigs, getFreePort } from '@milaboratories/pl-config';
14
+ import type { SupervisorStatus } from './supervisord';
14
15
  import { supervisorStatus, supervisorStop as supervisorCtlShutdown, generateSupervisordConfig, supervisorCtlStart } from './supervisord';
16
+ import type { ConnectionInfo, SshPlPorts } from './connection_info';
17
+ import { newConnectionInfo, parseConnectionInfo, stringifyConnectionInfo } from './connection_info';
15
18
 
16
19
  export class SshPl {
17
20
  private initState: PlatformaInitState = {};
@@ -42,49 +45,60 @@ export class SshPl {
42
45
  this.sshClient.close();
43
46
  }
44
47
 
45
- public async isAlive(): Promise<boolean> {
48
+ /** Provides an info if the platforma and minio are running along with the debug info. */
49
+ public async isAlive(): Promise<SupervisorStatus> {
46
50
  const arch = await this.getArch();
47
51
  const remoteHome = await this.getUserHomeDirectory();
48
-
49
- try {
50
- return await supervisorStatus(this.logger, this.sshClient, remoteHome, arch.arch);
51
- } catch (e: unknown) {
52
- // probably there are no supervisor on the server.
53
- return false;
54
- }
52
+ return await supervisorStatus(this.logger, this.sshClient, remoteHome, arch.arch);
55
53
  }
56
54
 
55
+ /** Starts all the services on the server.
56
+ * Idempotent semantic: we could call it several times. */
57
57
  public async start() {
58
58
  const arch = await this.getArch();
59
59
  const remoteHome = await this.getUserHomeDirectory();
60
60
 
61
61
  try {
62
- await supervisorCtlStart(this.sshClient, remoteHome, arch.arch);
62
+ if (!(await this.isAlive()).allAlive) {
63
+ await supervisorCtlStart(this.sshClient, remoteHome, arch.arch);
63
64
 
64
- // We are waiting for Platforma to run to ensure that it has started.
65
- return await this.checkIsAliveWithInterval();
65
+ // We are waiting for Platforma to run to ensure that it has started.
66
+ return await this.checkIsAliveWithInterval();
67
+ }
66
68
  } catch (e: unknown) {
67
- const msg = `ssh.start: error occurred ${e}`;
69
+ const msg = `SshPl.start: error occurred ${e}`;
68
70
  this.logger.error(msg);
69
71
  throw new Error(msg);
70
72
  }
71
73
  }
72
74
 
75
+ /** Stops all the services on the server.
76
+ * Idempotent semantic: we could call it several times. */
73
77
  public async stop() {
74
78
  const arch = await this.getArch();
75
79
  const remoteHome = await this.getUserHomeDirectory();
76
80
 
77
81
  try {
78
- await supervisorCtlShutdown(this.sshClient, remoteHome, arch.arch);
79
- return await this.checkIsAliveWithInterval(undefined, undefined, false);
82
+ if ((await this.isAlive()).allAlive) {
83
+ await supervisorCtlShutdown(this.sshClient, remoteHome, arch.arch);
84
+ return await this.checkIsAliveWithInterval(undefined, undefined, false);
85
+ }
80
86
  } catch (e: unknown) {
81
- const msg = `ssh.stop: error occurred ${e}`;
87
+ const msg = `PlSsh.stop: error occurred ${e}`;
82
88
  this.logger.error(msg);
83
89
  throw new Error(msg);
84
90
  }
85
91
  }
86
92
 
93
+ /** Stops the services, deletes a directory with the state and closes SSH connection. */
87
94
  public async reset(): Promise<boolean> {
95
+ await this.stopAndClean();
96
+ this.cleanUp();
97
+ return true;
98
+ }
99
+
100
+ /** Stops platforma and deletes its state. */
101
+ public async stopAndClean(): Promise<void> {
88
102
  const workDir = await this.getUserHomeDirectory();
89
103
 
90
104
  this.logger.info(`pl.reset: Stop Platforma on the server`);
@@ -92,26 +106,35 @@ export class SshPl {
92
106
 
93
107
  this.logger.info(`pl.reset: Deleting Platforma workDir ${workDir} on the server`);
94
108
  await this.sshClient.deleteFolder(plpath.workDir(workDir));
95
-
96
- this.cleanUp();
97
-
98
- return true;
99
109
  }
100
110
 
101
- public async platformaInit(ops: SshPlConfig): Promise<SshInitReturnTypes> {
102
- const state: PlatformaInitState = { localWorkdir: ops.localWorkdir };
111
+ /** Downloads binaries and untar them on the server,
112
+ * generates all the configs, creates necessary dirs,
113
+ * and finally starts all the services. */
114
+ public async platformaInit(options: SshPlConfig): Promise<ConnectionInfo> {
115
+ const state: PlatformaInitState = { localWorkdir: options.localWorkdir };
103
116
 
104
117
  try {
118
+ // merge options with default ops.
119
+ const ops: SshPlConfig = {
120
+ ...defaultSshPlConfig,
121
+ ...options,
122
+ };
105
123
  state.arch = await this.getArch();
106
124
  state.remoteHome = await this.getUserHomeDirectory();
107
- state.isAlive = await this.isAlive();
125
+ state.alive = await this.isAlive();
108
126
 
109
- if (state.isAlive) {
127
+ if (state.alive.allAlive) {
110
128
  state.userCredentials = await this.getUserCredentials(state.remoteHome);
111
129
  if (!state.userCredentials) {
112
130
  throw new Error(`SshPl.platformaInit: platforma is alive but userCredentials are not found`);
113
131
  }
114
- return state.userCredentials;
132
+ const needRestart = state.userCredentials.useGlobalAccess != ops.useGlobalAccess;
133
+ if (!needRestart)
134
+ return state.userCredentials;
135
+
136
+ // make sure that we won't be in a broken state.
137
+ await this.stopAndClean();
115
138
  }
116
139
 
117
140
  const downloadRes = await this.downloadBinariesAndUploadToTheServer(
@@ -143,6 +166,7 @@ export class SshPl {
143
166
  },
144
167
  },
145
168
  licenseMode: ops.license,
169
+ useGlobalAccess: notEmpty(ops.useGlobalAccess),
146
170
  });
147
171
  state.generatedConfig = { ...config, filesToCreate: { skipped: 'it is too wordy' } };
148
172
 
@@ -171,25 +195,22 @@ export class SshPl {
171
195
  throw new Error(`Can not write supervisord config on the server ${plpath.workDir(state.remoteHome)}`);
172
196
  }
173
197
 
174
- state.connectionInfo = {
175
- plUser: config.plUser,
176
- plPassword: config.plPassword,
177
- ports: state.ports,
178
- };
198
+ state.connectionInfo = newConnectionInfo(
199
+ config.plUser,
200
+ config.plPassword,
201
+ state.ports,
202
+ notEmpty(ops.useGlobalAccess),
203
+ );
179
204
  await this.sshClient.writeFileOnTheServer(
180
205
  plpath.connectionInfo(state.remoteHome),
181
- JSON.stringify(state.connectionInfo, undefined, 2),
206
+ stringifyConnectionInfo(state.connectionInfo),
182
207
  );
183
208
 
184
209
  await this.start();
185
210
  state.started = true;
186
211
  this.initState = state;
187
212
 
188
- return {
189
- plUser: config.plUser,
190
- plPassword: config.plPassword,
191
- ports: state.ports,
192
- };
213
+ return state.connectionInfo;
193
214
  } catch (e: unknown) {
194
215
  const msg = `SshPl.platformaInit: error occurred: ${e}, state: ${JSON.stringify(state)}`;
195
216
  this.logger.error(msg);
@@ -239,8 +260,10 @@ export class SshPl {
239
260
 
240
261
  /** We have to extract pl in the remote server,
241
262
  * because Windows doesn't support symlinks
242
- * that are found in linux pl binaries tgz archive.
243
- * For this reason, we extract all to the remote server. */
263
+ * that are found in Linux pl binaries tgz archive.
264
+ * For this reason, we extract all to the remote server.
265
+ * It requires `tar` to be installed on the server
266
+ * (it's not installed for Rocky Linux for example). */
244
267
  public async downloadAndUntar(
245
268
  localWorkdir: string,
246
269
  remoteHome: string,
@@ -308,28 +331,28 @@ export class SshPl {
308
331
  return false;
309
332
  }
310
333
 
311
- public async checkIsAliveWithInterval(interval: number = 1000, count = 15, shouldStart = true) {
334
+ public async checkIsAliveWithInterval(interval: number = 1000, count = 15, shouldStart = true): Promise<void> {
312
335
  const maxMs = count * interval;
313
336
 
314
337
  let total = 0;
315
338
  let alive = await this.isAlive();
316
- while (shouldStart ? !alive : alive) {
339
+ while (shouldStart ? !alive.allAlive : alive.allAlive) {
317
340
  await sleep(interval);
318
341
  total += interval;
319
342
  if (total > maxMs) {
320
- throw new Error(`isAliveWithInterval: The process did not ${shouldStart ? 'started' : 'stopped'} after ${maxMs} ms.`);
343
+ throw new Error(`isAliveWithInterval: The process did not ${shouldStart ? 'started' : 'stopped'} after ${maxMs} ms. Live status: ${JSON.stringify(alive)}`);
321
344
  }
322
345
  alive = await this.isAlive();
323
346
  }
324
347
  }
325
348
 
326
- public async getUserCredentials(remoteHome: string): Promise<SshInitReturnTypes> {
349
+ public async getUserCredentials(remoteHome: string): Promise<ConnectionInfo> {
327
350
  const connectionInfo = await this.sshClient.readFile(plpath.connectionInfo(remoteHome));
328
- return JSON.parse(connectionInfo) as SshInitReturnTypes;
351
+ return parseConnectionInfo(connectionInfo);
329
352
  }
330
353
 
331
- public async fetchPorts(remoteHome: string, arch: Arch): Promise<SshPlatformaPorts> {
332
- const ports: SshPlatformaPorts = {
354
+ public async fetchPorts(remoteHome: string, arch: Arch): Promise<SshPlPorts> {
355
+ const ports: SshPlPorts = {
333
356
  grpc: {
334
357
  local: await getFreePort(),
335
358
  remote: await this.getFreePortForPlatformaOnServer(remoteHome, arch),
@@ -403,41 +426,20 @@ export class SshPl {
403
426
  }
404
427
  }
405
428
 
406
- export type SshPlatformaPorts = {
407
- grpc: {
408
- local: number;
409
- remote: number;
410
- };
411
- monitoring: {
412
- local: number;
413
- remote: number;
414
- };
415
- debug: {
416
- local: number;
417
- remote: number;
418
- };
419
- minioPort: {
420
- local: number;
421
- remote: number;
422
- };
423
- minioConsolePort: {
424
- local: number;
425
- remote: number;
426
- };
427
- };
428
-
429
429
  type Arch = { platform: string; arch: string };
430
430
 
431
431
  export type SshPlConfig = {
432
432
  localWorkdir: string;
433
433
  license: PlLicenseMode;
434
+ useGlobalAccess?: boolean;
434
435
  };
435
436
 
436
- export type SshInitReturnTypes = {
437
- plUser: string;
438
- plPassword: string;
439
- ports: SshPlatformaPorts;
440
- } | null;
437
+ const defaultSshPlConfig: Pick<
438
+ SshPlConfig,
439
+ 'useGlobalAccess'
440
+ > = {
441
+ useGlobalAccess: false,
442
+ };
441
443
 
442
444
  type BinPaths = {
443
445
  history?: DownloadAndUntarState[];
@@ -462,12 +464,12 @@ type PlatformaInitState = {
462
464
  localWorkdir?: string;
463
465
  arch?: Arch;
464
466
  remoteHome?: string;
465
- isAlive?: boolean;
466
- userCredentials?: SshInitReturnTypes;
467
+ alive?: SupervisorStatus;
468
+ userCredentials?: ConnectionInfo;
467
469
  downloadedBinaries?: DownloadAndUntarState[];
468
470
  binPaths?: BinPaths;
469
- ports?: SshPlatformaPorts;
471
+ ports?: SshPlPorts;
470
472
  generatedConfig?: SshPlConfigGenerationResult;
471
- connectionInfo?: SshInitReturnTypes;
473
+ connectionInfo?: ConnectionInfo;
472
474
  started?: boolean;
473
475
  };
@@ -9,11 +9,11 @@ export const supervisordDirName = 'supervisord-0.7.3';
9
9
  export const supervisordSubDirName = 'supervisord_0.7.3_Linux_64-bit';
10
10
 
11
11
  export function workDir(remoteHome: string) {
12
- return upath.join(remoteHome, 'platforma_ssh');
12
+ return upath.join(remoteHome, '.platforma_ssh');
13
13
  }
14
14
 
15
15
  export function binariesDir(remoteHome: string) {
16
- return upath.join(remoteHome, 'platforma_ssh', 'binaries');
16
+ return upath.join(workDir(remoteHome), 'binaries');
17
17
  }
18
18
 
19
19
  export function platformaBaseDir(remoteHome: string, arch: string) {
@@ -28,6 +28,10 @@ export function platformaBin(remoteHome: string, arch: string) {
28
28
  return upath.join(platformaDir(remoteHome, arch), 'platforma');
29
29
  }
30
30
 
31
+ export function platformaConf(remoteHome: string): string {
32
+ return upath.join(workDir(remoteHome), 'config.yaml');
33
+ }
34
+
31
35
  export function platformaFreePortBin(remoteHome: string, arch: string): string {
32
36
  return upath.join(platformaDir(remoteHome, arch), 'free-port');
33
37
  }
package/src/ssh/ssh.ts CHANGED
@@ -314,12 +314,6 @@ export class SshClient {
314
314
  });
315
315
  }
316
316
 
317
- delay(delay: number): Promise<void> {
318
- return new Promise((res, rej) => {
319
- setTimeout(() => res(), delay);
320
- });
321
- }
322
-
323
317
  public async withSftp<R>(callback: (sftp: SFTPWrapper) => Promise<R>): Promise<R> {
324
318
  return new Promise((resolve, reject) => {
325
319
  this.client.sftp((err, sftp) => {
@@ -441,9 +435,9 @@ export class SshClient {
441
435
  async checkFileExists(remotePath: string) {
442
436
  return this.withSftp(async (sftp) => {
443
437
  return new Promise((resolve, reject) => {
444
- sftp.stat(remotePath, (err, stats) => {
438
+ sftp.stat(remotePath, (err: Error | undefined, stats) => {
445
439
  if (err) {
446
- if ((err as Error & { code: number }).code === 2) {
440
+ if ((err as unknown as { code?: number })?.code === 2) {
447
441
  return resolve(false);
448
442
  }
449
443
  return reject(new Error(`ssh.checkFileExists: err ${err}`));
@@ -540,8 +534,12 @@ export class SshClient {
540
534
  public async uploadDirectory(localDir: string, remoteDir: string, mode: number = 0o660): Promise<void> {
541
535
  return new Promise((resolve, reject) => {
542
536
  this.withSftp(async (sftp: SFTPWrapper) => {
543
- await this.__uploadDirectory(sftp, localDir, remoteDir, mode);
544
- resolve();
537
+ try {
538
+ await this.__uploadDirectory(sftp, localDir, remoteDir, mode);
539
+ resolve();
540
+ } catch (e: unknown) {
541
+ reject(new Error(`ssh.uploadDirectory: ${e}`));
542
+ }
545
543
  });
546
544
  });
547
545
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { MiLogger } from '@milaboratories/ts-helpers';
4
4
  import * as plpath from './pl_paths';
5
- import type { SshClient } from './ssh';
5
+ import type { SshClient, SshExecResult } from './ssh';
6
6
  import { randomBytes } from 'crypto';
7
7
 
8
8
  export async function supervisorCtlStart(
@@ -27,31 +27,45 @@ export async function supervisorStop(
27
27
  }
28
28
  }
29
29
 
30
- type SupervisorStatus = {
31
- platforma: boolean;
32
- minio: boolean;
30
+ /** Provides a simple true/false response got from supervisord status
31
+ * along with a debug info that could be showed in error logs (raw response from the command, parsed response etc). */
32
+ export type SupervisorStatus = {
33
+ platforma?: boolean;
34
+ minio?: boolean;
35
+ allAlive: boolean; // true when both pl and minio are alive.
36
+ rawResult?: SshExecResult;
37
+ execError?: string;
33
38
  };
34
39
 
35
40
  export async function supervisorStatus(
36
41
  logger: MiLogger,
37
42
  sshClient: SshClient,
38
43
  remoteHome: string, arch: string,
39
- ): Promise<boolean> {
40
- const result = await supervisorExec(sshClient, remoteHome, arch, 'ctl status');
44
+ ): Promise<SupervisorStatus> {
45
+ let result: SshExecResult;
46
+ try {
47
+ result = await supervisorExec(sshClient, remoteHome, arch, 'ctl status');
48
+ } catch (e: unknown) {
49
+ return { execError: String(e), allAlive: false };
50
+ }
41
51
 
42
52
  if (result.stderr) {
43
53
  logger.info(`supervisord ctl status: stderr occurred: ${result.stderr}, stdout: ${result.stdout}`);
44
54
 
45
- return false;
55
+ return { rawResult: result, allAlive: false };
46
56
  }
47
57
 
58
+ const platforma = isProgramRunning(result.stdout, 'platforma');
59
+ const minio = isProgramRunning(result.stdout, 'minio');
48
60
  const status: SupervisorStatus = {
49
- platforma: isProgramRunning(result.stdout, 'platforma'),
50
- minio: isProgramRunning(result.stdout, 'minio'),
61
+ rawResult: result,
62
+ platforma,
63
+ minio,
64
+ allAlive: platforma && minio,
51
65
  };
52
66
 
53
- if (status.platforma && status.minio) {
54
- return true;
67
+ if (status.allAlive) {
68
+ return status;
55
69
  }
56
70
 
57
71
  if (!status.minio) {
@@ -62,7 +76,7 @@ export async function supervisorStatus(
62
76
  logger.warn('Platforma is not running on the server');
63
77
  }
64
78
 
65
- return false;
79
+ return status;
66
80
  }
67
81
 
68
82
  export function generateSupervisordConfig(