@milaboratories/pl-deployments 1.1.4 → 1.1.6
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 +13 -13
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +491 -457
- package/dist/index.mjs.map +1 -1
- package/dist/ssh/pl.d.ts +1 -0
- package/dist/ssh/pl.d.ts.map +1 -1
- package/dist/ssh/ssh.d.ts +6 -3
- package/dist/ssh/ssh.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/ssh/__tests__/common-utils.ts +1 -1
- package/src/ssh/pl.ts +8 -2
- package/src/ssh/ssh.ts +140 -55
|
@@ -65,7 +65,7 @@ export async function initContainer(name: string): Promise<StartedTestContainer>
|
|
|
65
65
|
.withReuse()
|
|
66
66
|
.withName(`pl-ssh-test-${name}`)
|
|
67
67
|
.start()
|
|
68
|
-
.catch((err) => console.log('No worries, creating a new container'));
|
|
68
|
+
.catch((err: any) => console.log('No worries, creating a new container'));
|
|
69
69
|
|
|
70
70
|
if (!fromCacheContainer) {
|
|
71
71
|
generateKeys();
|
package/src/ssh/pl.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as ssh from 'ssh2';
|
|
2
2
|
import { SshClient } from './ssh';
|
|
3
3
|
import type { MiLogger } from '@milaboratories/ts-helpers';
|
|
4
|
-
import { sleep, notEmpty
|
|
4
|
+
import { sleep, notEmpty } from '@milaboratories/ts-helpers';
|
|
5
5
|
import type { DownloadBinaryResult } from '../common/pl_binary_download';
|
|
6
6
|
import { downloadBinaryNoExtract } from '../common/pl_binary_download';
|
|
7
7
|
import upath from 'upath';
|
|
@@ -19,7 +19,7 @@ export class SshPl {
|
|
|
19
19
|
public readonly logger: MiLogger,
|
|
20
20
|
public readonly sshClient: SshClient,
|
|
21
21
|
private readonly username: string,
|
|
22
|
-
) {}
|
|
22
|
+
) { }
|
|
23
23
|
|
|
24
24
|
public info() {
|
|
25
25
|
return {
|
|
@@ -38,6 +38,10 @@ export class SshPl {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
public cleanUp() {
|
|
42
|
+
this.sshClient.close();
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
public async isAlive(): Promise<boolean> {
|
|
42
46
|
const arch = await this.getArch();
|
|
43
47
|
const remoteHome = await this.getUserHomeDirectory();
|
|
@@ -89,6 +93,8 @@ export class SshPl {
|
|
|
89
93
|
this.logger.info(`pl.reset: Deleting Platforma workDir ${workDir} on the server`);
|
|
90
94
|
await this.sshClient.deleteFolder(plpath.workDir(workDir));
|
|
91
95
|
|
|
96
|
+
this.cleanUp();
|
|
97
|
+
|
|
92
98
|
return true;
|
|
93
99
|
}
|
|
94
100
|
|
package/src/ssh/ssh.ts
CHANGED
|
@@ -5,7 +5,8 @@ import dns from 'dns';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import { readFile } from 'fs/promises';
|
|
7
7
|
import upath from 'upath';
|
|
8
|
-
import type
|
|
8
|
+
import { RetryablePromise, type MiLogger } from '@milaboratories/ts-helpers';
|
|
9
|
+
import { randomBytes } from 'crypto';
|
|
9
10
|
|
|
10
11
|
const defaultConfig: ConnectConfig = {
|
|
11
12
|
keepaliveInterval: 60000,
|
|
@@ -22,6 +23,7 @@ export type SshDirContent = {
|
|
|
22
23
|
export class SshClient {
|
|
23
24
|
private config?: ConnectConfig;
|
|
24
25
|
public homeDir?: string;
|
|
26
|
+
private forwardedServers: net.Server[] = [];
|
|
25
27
|
|
|
26
28
|
constructor(
|
|
27
29
|
private readonly logger: MiLogger,
|
|
@@ -45,6 +47,10 @@ export class SshClient {
|
|
|
45
47
|
return client;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
public getForwardedServers() {
|
|
51
|
+
return this.forwardedServers;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
public getFullHostName() {
|
|
49
55
|
return `${this.config?.host}:${this.config?.port}`;
|
|
50
56
|
}
|
|
@@ -60,16 +66,7 @@ export class SshClient {
|
|
|
60
66
|
*/
|
|
61
67
|
public async connect(config: ConnectConfig) {
|
|
62
68
|
this.config = config;
|
|
63
|
-
return
|
|
64
|
-
this.client.on('ready', () => {
|
|
65
|
-
resolve(undefined);
|
|
66
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
-
}).on('error', (err: any) => {
|
|
68
|
-
reject(new Error(`ssh.connect: error occurred: ${err}`));
|
|
69
|
-
}).on('timeout', () => {
|
|
70
|
-
reject(new Error(`timeout was occurred while waiting for SSH connection.`));
|
|
71
|
-
}).connect(config);
|
|
72
|
-
});
|
|
69
|
+
return await connect(this.client, config, () => {}, () => {});
|
|
73
70
|
}
|
|
74
71
|
|
|
75
72
|
/**
|
|
@@ -154,62 +151,112 @@ export class SshClient {
|
|
|
154
151
|
* @returns { server: net.Server } A promise resolving with the created server instance.
|
|
155
152
|
*/
|
|
156
153
|
public async forwardPort(ports: { remotePort: number; localPort: number; localHost?: string }, config?: ConnectConfig): Promise<{ server: net.Server }> {
|
|
154
|
+
const log = `ssh.forward:${ports.localPort}:${ports.remotePort}.id_${randomBytes(1).toString('hex')}`;
|
|
157
155
|
config = config ?? this.config;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
(err, stream) => {
|
|
170
|
-
if (err) {
|
|
171
|
-
console.error('Error opening SSH channel:', err.message);
|
|
172
|
-
localSocket.end();
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
localSocket.pipe(stream);
|
|
176
|
-
stream.pipe(localSocket);
|
|
177
|
-
localSocket.resume();
|
|
178
|
-
},
|
|
179
|
-
);
|
|
156
|
+
|
|
157
|
+
// we make this thing persistent so that if the connection
|
|
158
|
+
// drops (it happened in the past because of lots of errors and forwardOut opened channels),
|
|
159
|
+
// we'll recreate it here.
|
|
160
|
+
const persistentClient = new RetryablePromise((p: RetryablePromise<Client>) => {
|
|
161
|
+
return new Promise<Client>((resolve, reject) => {
|
|
162
|
+
const client = new Client();
|
|
163
|
+
|
|
164
|
+
client.on('ready', () => {
|
|
165
|
+
this.logger.info(`${log}.client.ready`);
|
|
166
|
+
resolve(client);
|
|
180
167
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
168
|
+
|
|
169
|
+
client.on('error', (err) => {
|
|
170
|
+
this.logger.info(`${log}.client.error: ${err}`);
|
|
171
|
+
p.reset();
|
|
172
|
+
reject(err);
|
|
184
173
|
});
|
|
185
174
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
reject(new Error(`ssh.forwardPort: server error: ${err}`));
|
|
175
|
+
client.on('close', () => {
|
|
176
|
+
this.logger.info(`${log}.client.closed`);
|
|
177
|
+
p.reset();
|
|
190
178
|
});
|
|
191
179
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
180
|
+
client.connect(config!);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await persistentClient.ensure(); // warm up a connection
|
|
185
|
+
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
const server = net.createServer({ pauseOnConnect: true }, async (localSocket) => {
|
|
188
|
+
const sockLog = `${log}.sock_${randomBytes(1).toString('hex')}`;
|
|
189
|
+
// this.logger.info(`${sockLog}.localSocket: start connection`);
|
|
190
|
+
let conn: Client;
|
|
191
|
+
try {
|
|
192
|
+
conn = await persistentClient.ensure();
|
|
193
|
+
} catch (e: unknown) {
|
|
194
|
+
this.logger.info(`${sockLog}.persistentClient.catch: ${e}`);
|
|
195
|
+
localSocket.end();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let stream: ClientChannel;
|
|
200
|
+
try {
|
|
201
|
+
stream = await forwardOut(this.logger, conn, '127.0.0.1', 0, '127.0.0.1', ports.remotePort);
|
|
202
|
+
} catch (e: unknown) {
|
|
203
|
+
this.logger.error(`${sockLog}.forwardOut.err: ${e}`);
|
|
204
|
+
localSocket.end();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
localSocket.pipe(stream);
|
|
209
|
+
stream.pipe(localSocket);
|
|
210
|
+
localSocket.resume();
|
|
211
|
+
// this.logger.info(`${sockLog}.forwardOut: connected`);
|
|
212
|
+
|
|
213
|
+
stream.on('error', (err: unknown) => {
|
|
214
|
+
this.logger.error(`${sockLog}.stream.error: ${err}`);
|
|
215
|
+
localSocket.end();
|
|
216
|
+
stream.end();
|
|
198
217
|
});
|
|
218
|
+
stream.on('close', () => {
|
|
219
|
+
// this.logger.info(`${sockLog}.stream.close: closed`);
|
|
220
|
+
localSocket.end();
|
|
221
|
+
stream.end();
|
|
222
|
+
});
|
|
223
|
+
localSocket.on('close', () => {
|
|
224
|
+
this.logger.info(`${sockLog}.localSocket: closed`);
|
|
225
|
+
localSocket.end();
|
|
226
|
+
stream.end();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
server.listen(ports.localPort, '127.0.0.1', () => {
|
|
231
|
+
this.logger.info(`${log}.server: started listening`);
|
|
232
|
+
this.forwardedServers.push(server);
|
|
233
|
+
resolve({ server });
|
|
199
234
|
});
|
|
200
235
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
server
|
|
204
|
-
reject(`ssh.forwardPort: conn.err: ${err}`);
|
|
236
|
+
server.on('error', (err) => {
|
|
237
|
+
server.close();
|
|
238
|
+
reject(new Error(`${log}.server: error: ${JSON.stringify(err)}`));
|
|
205
239
|
});
|
|
206
240
|
|
|
207
|
-
|
|
208
|
-
this.logger.info(
|
|
241
|
+
server.on('close', () => {
|
|
242
|
+
this.logger.info(`${log}.server: closed ${JSON.stringify(ports)}`);
|
|
243
|
+
this.forwardedServers = this.forwardedServers.filter((s) => s !== server);
|
|
209
244
|
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
210
247
|
|
|
211
|
-
|
|
248
|
+
public closeForwardedPorts(): void {
|
|
249
|
+
this.logger.info('[SSH] Closing all forwarded ports...');
|
|
250
|
+
this.forwardedServers.forEach((server) => {
|
|
251
|
+
const rawAddress = server.address();
|
|
252
|
+
if (rawAddress && typeof rawAddress !== 'string') {
|
|
253
|
+
const address: net.AddressInfo = rawAddress;
|
|
254
|
+
this.logger.info(`[SSH] Closing port forward for server ${address.address}:${address.port}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
server.close();
|
|
212
258
|
});
|
|
259
|
+
this.forwardedServers = [];
|
|
213
260
|
}
|
|
214
261
|
|
|
215
262
|
/**
|
|
@@ -440,7 +487,7 @@ export class SshClient {
|
|
|
440
487
|
|
|
441
488
|
public uploadFileUsingExistingSftp(sftp: SFTPWrapper, localPath: string, remotePath: string, mode: number = 0o660) {
|
|
442
489
|
return new Promise((resolve, reject) => {
|
|
443
|
-
readFile(localPath).then(async (result) => {
|
|
490
|
+
readFile(localPath).then(async (result: Buffer) => {
|
|
444
491
|
this.writeFile(sftp, remotePath, result, mode)
|
|
445
492
|
.then(() => {
|
|
446
493
|
resolve(undefined);
|
|
@@ -590,11 +637,49 @@ export class SshClient {
|
|
|
590
637
|
}
|
|
591
638
|
|
|
592
639
|
/**
|
|
593
|
-
* Closes the SSH client connection.
|
|
640
|
+
* Closes the SSH client connection and forwarded ports.
|
|
594
641
|
*/
|
|
595
642
|
public close(): void {
|
|
643
|
+
this.closeForwardedPorts();
|
|
596
644
|
this.client.end();
|
|
597
645
|
}
|
|
598
646
|
}
|
|
599
647
|
|
|
600
648
|
export type SshExecResult = { stdout: string; stderr: string };
|
|
649
|
+
|
|
650
|
+
async function connect(
|
|
651
|
+
client: Client,
|
|
652
|
+
config: ConnectConfig,
|
|
653
|
+
onError: (e: unknown) => void,
|
|
654
|
+
onClose: () => void,
|
|
655
|
+
): Promise<Client> {
|
|
656
|
+
return new Promise((resolve, reject) => {
|
|
657
|
+
client.on('ready', () => {
|
|
658
|
+
resolve(client);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
client.on('error', (err: unknown) => {
|
|
662
|
+
onError(err);
|
|
663
|
+
reject(new Error(`ssh.connect: error occurred: ${err}`));
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
client.on('close', () => {
|
|
667
|
+
onClose();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
client.connect(config);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function forwardOut(logger: MiLogger, conn: Client, localHost: string, localPort: number, remoteHost: string, remotePort: number): Promise<ClientChannel> {
|
|
675
|
+
return new Promise((resolve, reject) => {
|
|
676
|
+
conn.forwardOut(localHost, localPort, remoteHost, remotePort, (err, stream) => {
|
|
677
|
+
if (err) {
|
|
678
|
+
logger.error(`forwardOut.error: ${err}`);
|
|
679
|
+
return reject(err);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return resolve(stream);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|