@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.
- package/README.md +1 -0
- package/dist/common/os_and_arch.d.ts +9 -0
- package/dist/common/os_and_arch.d.ts.map +1 -0
- package/dist/common/pl_binary.d.ts +14 -0
- package/dist/common/pl_binary.d.ts.map +1 -0
- package/dist/common/pl_binary_download.d.ts +30 -0
- package/dist/common/pl_binary_download.d.ts.map +1 -0
- package/dist/common/pl_version.d.ts +2 -0
- package/dist/common/pl_version.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1028 -0
- package/dist/index.mjs.map +1 -0
- package/dist/local/options.d.ts +31 -0
- package/dist/local/options.d.ts.map +1 -0
- package/dist/local/pid.d.ts +4 -0
- package/dist/local/pid.d.ts.map +1 -0
- package/dist/local/pl.d.ts +66 -0
- package/dist/local/pl.d.ts.map +1 -0
- package/dist/local/process.d.ts +12 -0
- package/dist/local/process.d.ts.map +1 -0
- package/dist/local/trace.d.ts +10 -0
- package/dist/local/trace.d.ts.map +1 -0
- package/dist/ssh/__tests__/common-utils.d.ts +18 -0
- package/dist/ssh/__tests__/common-utils.d.ts.map +1 -0
- package/dist/ssh/pl.d.ts +101 -0
- package/dist/ssh/pl.d.ts.map +1 -0
- package/dist/ssh/pl_paths.d.ts +17 -0
- package/dist/ssh/pl_paths.d.ts.map +1 -0
- package/dist/ssh/ssh.d.ts +128 -0
- package/dist/ssh/ssh.d.ts.map +1 -0
- package/dist/ssh/supervisord.d.ts +8 -0
- package/dist/ssh/supervisord.d.ts.map +1 -0
- package/package.json +64 -0
- package/src/common/os_and_arch.ts +44 -0
- package/src/common/pl_binary.ts +39 -0
- package/src/common/pl_binary_download.ts +258 -0
- package/src/common/pl_version.ts +5 -0
- package/src/index.ts +4 -0
- package/src/local/config.test.yaml +60 -0
- package/src/local/options.ts +34 -0
- package/src/local/pid.ts +21 -0
- package/src/local/pl.test.ts +129 -0
- package/src/local/pl.ts +220 -0
- package/src/local/process.ts +44 -0
- package/src/local/trace.ts +30 -0
- package/src/ssh/Dockerfile +29 -0
- package/src/ssh/__tests__/common-utils.ts +131 -0
- package/src/ssh/__tests__/pl-docker.test.ts +192 -0
- package/src/ssh/__tests__/ssh-docker.test.ts +175 -0
- package/src/ssh/pl.ts +451 -0
- package/src/ssh/pl_paths.ts +57 -0
- package/src/ssh/ssh.ts +512 -0
- package/src/ssh/supervisord.ts +137 -0
- package/src/ssh/test-assets/simple-server.js +10 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { ConnectConfig, SFTPWrapper, Client } from 'ssh2';
|
|
2
|
+
import { default as net } from 'net';
|
|
3
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
4
|
+
export type SshAuthMethods = 'publickey' | 'password';
|
|
5
|
+
export type SshAuthMethodsResult = SshAuthMethods[];
|
|
6
|
+
export declare class SshClient {
|
|
7
|
+
private readonly logger;
|
|
8
|
+
private readonly client;
|
|
9
|
+
private config?;
|
|
10
|
+
homeDir?: string;
|
|
11
|
+
constructor(logger: MiLogger, client: Client);
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the SshClient and establishes a connection using the provided configuration.
|
|
14
|
+
* @param config - The connection configuration object for the SSH client.
|
|
15
|
+
* @returns A new instance of SshClient with an active connection.
|
|
16
|
+
*/
|
|
17
|
+
static init(logger: MiLogger, config: ConnectConfig): Promise<SshClient>;
|
|
18
|
+
/**
|
|
19
|
+
* Connects to the SSH server using the specified configuration.
|
|
20
|
+
* @param config - The connection configuration object for the SSH client.
|
|
21
|
+
* @returns A promise that resolves when the connection is established or rejects on error.
|
|
22
|
+
*/
|
|
23
|
+
connect(config: ConnectConfig): Promise<unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Executes a command on the SSH server.
|
|
26
|
+
* @param command - The command to execute on the remote server.
|
|
27
|
+
* @returns A promise resolving with the command's stdout and stderr outputs.
|
|
28
|
+
*/
|
|
29
|
+
exec(command: string): Promise<SshExecResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Retrieves the supported authentication methods for a given host and port.
|
|
32
|
+
* @param host - The hostname or IP address of the server.
|
|
33
|
+
* @param port - The port number to connect to on the server.
|
|
34
|
+
* @returns 'publickey' | 'password'[] A promise resolving with a list of supported authentication methods.
|
|
35
|
+
*/
|
|
36
|
+
static getAuthTypes(host: string, port: number): Promise<SshAuthMethodsResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Extracts authentication methods from debug logs.
|
|
39
|
+
* @param log - The debug log output containing authentication information.
|
|
40
|
+
* @returns An array of extracted authentication methods.
|
|
41
|
+
*/
|
|
42
|
+
private static extractAuthMethods;
|
|
43
|
+
/**
|
|
44
|
+
* Sets up port forwarding between a remote port on the SSH server and a local port.
|
|
45
|
+
* A new connection is used for this operation instead of an existing one.
|
|
46
|
+
* @param ports - An object specifying the remote and local port configuration.
|
|
47
|
+
* @param config - Optional connection configuration for the SSH client.
|
|
48
|
+
* @returns { server: net.Server } A promise resolving with the created server instance.
|
|
49
|
+
*/
|
|
50
|
+
forwardPort(ports: {
|
|
51
|
+
remotePort: number;
|
|
52
|
+
localPort: number;
|
|
53
|
+
localHost?: string;
|
|
54
|
+
}, config?: ConnectConfig): Promise<{
|
|
55
|
+
server: net.Server;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Checks if a specified host is available by performing a DNS lookup.
|
|
59
|
+
* @param hostname - The hostname or IP address to check.
|
|
60
|
+
* @returns A promise resolving with `true` if the host is reachable, otherwise `false`.
|
|
61
|
+
*/
|
|
62
|
+
static checkHostAvailability(hostname: string): Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* Determines whether a private key requires a passphrase for use.
|
|
65
|
+
* @param privateKey - The private key content to check.
|
|
66
|
+
* @returns A promise resolving with `true` if a passphrase is required, otherwise `false`.
|
|
67
|
+
*/
|
|
68
|
+
static isPassphraseRequiredForKey(privateKey: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Uploads a local file to a remote server via SFTP.
|
|
71
|
+
* This function creates new SFTP connection
|
|
72
|
+
* @param localPath - The local file path.
|
|
73
|
+
* @param remotePath - The remote file path on the server.
|
|
74
|
+
* @returns A promise resolving with `true` if the file was successfully uploaded.
|
|
75
|
+
*/
|
|
76
|
+
uploadFile(localPath: string, remotePath: string): Promise<boolean>;
|
|
77
|
+
delay(delay: number): Promise<void>;
|
|
78
|
+
withSftp<R>(callback: (sftp: SFTPWrapper) => Promise<R>): Promise<R>;
|
|
79
|
+
writeFileOnTheServer(remotePath: string, data: string | Buffer, mode?: number): Promise<boolean>;
|
|
80
|
+
readFile(remotePath: string): Promise<string>;
|
|
81
|
+
chmod(path: string, mode: number): Promise<unknown>;
|
|
82
|
+
checkFileExists(remotePath: string): Promise<unknown>;
|
|
83
|
+
checkPathExists(remotePath: string): Promise<{
|
|
84
|
+
exists: boolean;
|
|
85
|
+
isFile: boolean;
|
|
86
|
+
isDirectory: boolean;
|
|
87
|
+
}>;
|
|
88
|
+
private writeFile;
|
|
89
|
+
uploadFileUsingExistingSftp(sftp: SFTPWrapper, localPath: string, remotePath: string, mode?: number): Promise<unknown>;
|
|
90
|
+
private __uploadDirectory;
|
|
91
|
+
/**
|
|
92
|
+
* Uploads a local directory and its contents (including subdirectories) to the remote server via SFTP.
|
|
93
|
+
* @param localDir - The path to the local directory to upload.
|
|
94
|
+
* @param remoteDir - The path to the remote directory on the server.
|
|
95
|
+
* @returns A promise that resolves when the directory and its contents are uploaded.
|
|
96
|
+
*/
|
|
97
|
+
uploadDirectory(localDir: string, remoteDir: string, mode?: number): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Ensures that a remote directory and all its parent directories exist.
|
|
100
|
+
* @param sftp - The SFTP wrapper.
|
|
101
|
+
* @param remotePath - The path to the remote directory.
|
|
102
|
+
* @returns A promise that resolves when the directory is created.
|
|
103
|
+
*/
|
|
104
|
+
private __createRemoteDirectory;
|
|
105
|
+
/**
|
|
106
|
+
* Ensures that a remote directory and all its parent directories exist.
|
|
107
|
+
* @param sftp - The SFTP wrapper.
|
|
108
|
+
* @param remotePath - The path to the remote directory.
|
|
109
|
+
* @returns A promise that resolves when the directory is created.
|
|
110
|
+
*/
|
|
111
|
+
createRemoteDirectory(remotePath: string, mode?: number): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Downloads a file from the remote server to a local path via SFTP.
|
|
114
|
+
* @param remotePath - The remote file path on the server.
|
|
115
|
+
* @param localPath - The local file path to save the file.
|
|
116
|
+
* @returns A promise resolving with `true` if the file was successfully downloaded.
|
|
117
|
+
*/
|
|
118
|
+
downloadFile(remotePath: string, localPath: string): Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* Closes the SSH client connection.
|
|
121
|
+
*/
|
|
122
|
+
close(): void;
|
|
123
|
+
}
|
|
124
|
+
export type SshExecResult = {
|
|
125
|
+
stdout: string;
|
|
126
|
+
stderr: string;
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=ssh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../../src/ssh/ssh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MiLogger } from '@milaboratories/ts-helpers';
|
|
2
|
+
import { SshClient, SshExecResult } from './ssh';
|
|
3
|
+
export declare function supervisorCtlStart(sshClient: SshClient, remoteHome: string, arch: string): Promise<void>;
|
|
4
|
+
export declare function supervisorStop(sshClient: SshClient, remoteHome: string, arch: string): Promise<void>;
|
|
5
|
+
export declare function supervisorStatus(logger: MiLogger, sshClient: SshClient, remoteHome: string, arch: string): Promise<boolean>;
|
|
6
|
+
export declare function generateSupervisordConfig(minioStorageDir: string, minioEnvs: Record<string, string>, supervisorRemotePort: number, remoteWorkDir: string, platformaConfigPath: string, minioPath: string, plPath: string): string;
|
|
7
|
+
export declare function supervisorExec(sshClient: SshClient, remoteHome: string, arch: string, command: string): Promise< SshExecResult>;
|
|
8
|
+
//# sourceMappingURL=supervisord.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisord.d.ts","sourceRoot":"","sources":["../../src/ssh/supervisord.ts"],"names":[],"mappings":"AAAA,yDAAyD;AAEzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAE3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,iBAOjC;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,iBAOjC;AAOD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,QAAQ,EAChB,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAC/B,OAAO,CAAC,OAAO,CAAC,CA2BlB;AAED,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,oBAAoB,EAAE,MAAM,EAC5B,aAAa,EAAE,MAAM,EACrB,mBAAmB,EAAE,MAAM,EAE3B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,UAoCf;AAED,wBAAsB,cAAc,CAClC,SAAS,EAAE,SAAS,EACpB,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAChC,OAAO,EAAE,MAAM,0CAOhB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@milaboratories/pl-deployments",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"pl-version": "1.18.3",
|
|
5
|
+
"description": "MiLaboratories Platforma Backend code service run wrapper",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"./dist/**/*",
|
|
18
|
+
"./src/**/*",
|
|
19
|
+
"README.md",
|
|
20
|
+
"bin/run.cmd",
|
|
21
|
+
"bin/run.js",
|
|
22
|
+
"assets",
|
|
23
|
+
"postinstall.js"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/decompress": "^4.2.7",
|
|
29
|
+
"@types/jest": "^29.5.14",
|
|
30
|
+
"@types/node": "~20.16.15",
|
|
31
|
+
"@types/ssh2": "^1.15.1",
|
|
32
|
+
"eslint": "^9.16.0",
|
|
33
|
+
"jest": "^29.7.0",
|
|
34
|
+
"prettier": "^3.4.1",
|
|
35
|
+
"testcontainers": "^10.16.0",
|
|
36
|
+
"ts-jest": "^29.2.5",
|
|
37
|
+
"tsconfig-paths": "^4.2.0",
|
|
38
|
+
"typescript": "~5.5.4",
|
|
39
|
+
"utility-types": "^3.11.0",
|
|
40
|
+
"vite": "^5.4.11",
|
|
41
|
+
"vitest": "^2.1.8",
|
|
42
|
+
"@milaboratories/platforma-build-configs": "1.0.2",
|
|
43
|
+
"@milaboratories/eslint-config": "^1.0.1"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"decompress": "^4.2.1",
|
|
47
|
+
"upath": "^2.0.1",
|
|
48
|
+
"ssh2": "^1.16.0",
|
|
49
|
+
"tar": "^7.4.3",
|
|
50
|
+
"undici": "^7.2.3",
|
|
51
|
+
"yaml": "^2.6.1",
|
|
52
|
+
"zod": "~3.23.8",
|
|
53
|
+
"@milaboratories/pl-config": "^1.4.0",
|
|
54
|
+
"@milaboratories/ts-helpers": "^1.1.3"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"type-check": "tsc --noEmit --composite false",
|
|
58
|
+
"build": "vite build",
|
|
59
|
+
"test": "vitest",
|
|
60
|
+
"do-pack": "rm -rf src/.test && rm -f *.tgz && pnpm pack && mv *.tgz package.tgz",
|
|
61
|
+
"cleanup-docker": "docker container stop pl-ssh-test-pl pl-ssh-test-ssh && docker container rm pl-ssh-test-pl pl-ssh-test-ssh && docker image rm pl-ssh-test-container-ssh:1.0.0 pl-ssh-test-container-pl:1.0.0",
|
|
62
|
+
"exec-docker": "docker exec -ti pl-ssh-test-pl bash"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
export const OSes = ['linux', 'macos', 'windows'] as const;
|
|
4
|
+
export type OSType = (typeof OSes)[number];
|
|
5
|
+
|
|
6
|
+
/** @param osName - should be the thing returned from either {@link os.platform())} or `uname -s` */
|
|
7
|
+
export function newOs(osName: string): OSType {
|
|
8
|
+
switch (osName.toLowerCase()) {
|
|
9
|
+
case 'darwin':
|
|
10
|
+
return 'macos';
|
|
11
|
+
case 'linux':
|
|
12
|
+
return 'linux';
|
|
13
|
+
case 'win32':
|
|
14
|
+
return 'windows';
|
|
15
|
+
default:
|
|
16
|
+
throw new Error(
|
|
17
|
+
`operating system '${osName}' is not currently supported by Platforma ecosystem. The list of OSes supported: `
|
|
18
|
+
+ JSON.stringify(OSes),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Arches = ['amd64', 'arm64'] as const;
|
|
24
|
+
export type ArchType = (typeof Arches)[number];
|
|
25
|
+
|
|
26
|
+
/** @param arch - should be the thing returned from either {@link os.arch())} or `uname -m` */
|
|
27
|
+
export function newArch(arch: string): ArchType {
|
|
28
|
+
switch (arch) {
|
|
29
|
+
case 'aarch64':
|
|
30
|
+
case 'aarch64_be':
|
|
31
|
+
case 'arm64':
|
|
32
|
+
return 'arm64';
|
|
33
|
+
|
|
34
|
+
case 'x86_64':
|
|
35
|
+
case 'x64':
|
|
36
|
+
return 'amd64';
|
|
37
|
+
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(
|
|
40
|
+
`processor architecture '${arch}' is not currently supported by Platforma ecosystem. The list of architectures supported: `
|
|
41
|
+
+ JSON.stringify(Arches),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MiLogger } from '@milaboratories/ts-helpers';
|
|
2
|
+
import { assertNever } from '@milaboratories/ts-helpers';
|
|
3
|
+
import { downloadPlBinary, DownloadBinaryResult } from './pl_binary_download';
|
|
4
|
+
import { getDefaultPlVersion } from './pl_version';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
/** Shows how the binary should be got. */
|
|
8
|
+
export type PlBinarySource = PlBinarySourceDownload | PlBinarySourceLocal;
|
|
9
|
+
|
|
10
|
+
export type PlBinarySourceDownload = {
|
|
11
|
+
readonly type: 'Download';
|
|
12
|
+
readonly version: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type PlBinarySourceLocal = {
|
|
16
|
+
readonly type: 'Local';
|
|
17
|
+
readonly path: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function newDefaultPlBinarySource(): PlBinarySourceDownload {
|
|
21
|
+
return { type: 'Download', version: getDefaultPlVersion() };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function resolveLocalPlBinaryPath(
|
|
25
|
+
logger: MiLogger,
|
|
26
|
+
downloadDir: string,
|
|
27
|
+
src: PlBinarySource,
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
switch (src.type) {
|
|
30
|
+
case 'Download':
|
|
31
|
+
return (await downloadPlBinary(logger, downloadDir, src.version, os.arch(), os.platform())).binaryPath!;
|
|
32
|
+
|
|
33
|
+
case 'Local':
|
|
34
|
+
return src.path;
|
|
35
|
+
|
|
36
|
+
default:
|
|
37
|
+
assertNever(src);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fsp from 'fs/promises';
|
|
3
|
+
import upath from 'upath';
|
|
4
|
+
import { request } from 'undici';
|
|
5
|
+
import { Writable, Readable } from 'stream';
|
|
6
|
+
import { text } from 'stream/consumers';
|
|
7
|
+
import * as tar from 'tar';
|
|
8
|
+
import type { MiLogger } from '@milaboratories/ts-helpers';
|
|
9
|
+
import { assertNever, fileExists } from '@milaboratories/ts-helpers';
|
|
10
|
+
import decompress from 'decompress';
|
|
11
|
+
import type { ArchType, OSType } from './os_and_arch';
|
|
12
|
+
import { newOs, newArch } from './os_and_arch';
|
|
13
|
+
|
|
14
|
+
export type DownloadBinaryResult = {
|
|
15
|
+
archiveUrl: string;
|
|
16
|
+
archivePath: string;
|
|
17
|
+
archiveType: ArchiveType;
|
|
18
|
+
targetFolder: string;
|
|
19
|
+
baseName: string;
|
|
20
|
+
binaryPath?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function downloadBinaryNoExtract(
|
|
24
|
+
logger: MiLogger,
|
|
25
|
+
baseDir: string,
|
|
26
|
+
softwareName: string,
|
|
27
|
+
tgzName: string,
|
|
28
|
+
arch: string,
|
|
29
|
+
platform: string,
|
|
30
|
+
): Promise<DownloadBinaryResult> {
|
|
31
|
+
const opts = getPathsForDownload(softwareName, tgzName, baseDir, newArch(arch), newOs(platform));
|
|
32
|
+
const { archiveUrl, archivePath } = opts;
|
|
33
|
+
|
|
34
|
+
await downloadArchive(logger, archiveUrl, archivePath);
|
|
35
|
+
|
|
36
|
+
return opts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function downloadBinary(
|
|
40
|
+
logger: MiLogger,
|
|
41
|
+
baseDir: string,
|
|
42
|
+
softwareName: string,
|
|
43
|
+
archiveName: string,
|
|
44
|
+
arch: string,
|
|
45
|
+
platform: string,
|
|
46
|
+
): Promise<DownloadBinaryResult> {
|
|
47
|
+
const opts = getPathsForDownload(softwareName, archiveName, baseDir, newArch(arch), newOs(platform));
|
|
48
|
+
const { archiveUrl, archivePath, archiveType, targetFolder, baseName } = opts;
|
|
49
|
+
|
|
50
|
+
await downloadArchive(logger, archiveUrl, archivePath);
|
|
51
|
+
await extractArchive(logger, archivePath, archiveType, targetFolder);
|
|
52
|
+
|
|
53
|
+
return opts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function downloadPlBinary(
|
|
57
|
+
logger: MiLogger,
|
|
58
|
+
baseDir: string,
|
|
59
|
+
plVersion: string,
|
|
60
|
+
arch: string,
|
|
61
|
+
platform: string,
|
|
62
|
+
): Promise<DownloadBinaryResult> {
|
|
63
|
+
const opts = localDownloadPlOptions(plVersion, baseDir, newArch(arch), newOs(platform));
|
|
64
|
+
const { archiveUrl, archivePath, archiveType, targetFolder, binaryPath } = opts;
|
|
65
|
+
|
|
66
|
+
await downloadArchive(logger, archiveUrl, archivePath);
|
|
67
|
+
await extractArchive(logger, archivePath, archiveType, targetFolder);
|
|
68
|
+
|
|
69
|
+
return opts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getPathsForDownload(
|
|
73
|
+
softwareName: string,
|
|
74
|
+
archiveName: string,
|
|
75
|
+
baseDir: string,
|
|
76
|
+
arch: ArchType,
|
|
77
|
+
os: OSType,
|
|
78
|
+
): DownloadBinaryResult {
|
|
79
|
+
const baseName = `${archiveName}-${arch}`;
|
|
80
|
+
const archiveType = osToArchiveType[os];
|
|
81
|
+
|
|
82
|
+
const archiveFileName = `${baseName}.${archiveType}`;
|
|
83
|
+
const archiveUrl = `https://cdn.platforma.bio/software/${softwareName}/${os}/${archiveFileName}`;
|
|
84
|
+
const archivePath = upath.join(baseDir, archiveFileName);
|
|
85
|
+
// folder where binary distribution of pl will be unpacked
|
|
86
|
+
const targetFolder = upath.join(baseDir, baseName);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
archiveUrl,
|
|
90
|
+
archivePath,
|
|
91
|
+
archiveType,
|
|
92
|
+
targetFolder,
|
|
93
|
+
baseName,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function localDownloadPlOptions(
|
|
98
|
+
plVersion: string,
|
|
99
|
+
baseDir: string,
|
|
100
|
+
arch: ArchType,
|
|
101
|
+
os: OSType,
|
|
102
|
+
): DownloadBinaryResult {
|
|
103
|
+
const baseName = `pl-${plVersion}-${arch}`;
|
|
104
|
+
const archiveType = osToArchiveType[os];
|
|
105
|
+
|
|
106
|
+
const archiveFileName = `${baseName}.${archiveType}`;
|
|
107
|
+
const archiveUrl = `https://cdn.platforma.bio/software/pl/${os}/${archiveFileName}`;
|
|
108
|
+
const archivePath = upath.join(baseDir, archiveFileName);
|
|
109
|
+
|
|
110
|
+
// folder where binary distribution of pl will be unpacked
|
|
111
|
+
const targetFolder = upath.join(baseDir, baseName);
|
|
112
|
+
|
|
113
|
+
const binaryPath = upath.join(baseName, 'binaries', osToBinaryName[os]);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
archiveUrl,
|
|
117
|
+
archivePath,
|
|
118
|
+
archiveType,
|
|
119
|
+
targetFolder,
|
|
120
|
+
binaryPath,
|
|
121
|
+
baseName,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type DownloadInfo = {
|
|
126
|
+
dstArchive?: string;
|
|
127
|
+
fileExisted?: boolean;
|
|
128
|
+
dirnameCreated?: boolean;
|
|
129
|
+
statusCode?: number;
|
|
130
|
+
errorMsg?: string;
|
|
131
|
+
tmpPath?: string;
|
|
132
|
+
wroteTmp?: boolean;
|
|
133
|
+
tmpExisted?: boolean;
|
|
134
|
+
renamed?: boolean;
|
|
135
|
+
newExisted?: boolean;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export async function downloadArchive(
|
|
139
|
+
logger: MiLogger, archiveUrl: string, dstArchiveFile: string,
|
|
140
|
+
): Promise<DownloadInfo> {
|
|
141
|
+
const state: DownloadInfo = {};
|
|
142
|
+
state.dstArchive = dstArchiveFile;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
state.fileExisted = await fileExists(dstArchiveFile);
|
|
146
|
+
if (state.fileExisted) {
|
|
147
|
+
logger.info(`Platforma Backend archive download skipped: '${dstArchiveFile}' already exists`);
|
|
148
|
+
return state;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await fsp.mkdir(upath.dirname(dstArchiveFile), { recursive: true });
|
|
152
|
+
state.dirnameCreated = true;
|
|
153
|
+
|
|
154
|
+
logger.info(`Downloading archive:\n URL: ${archiveUrl}\n Save to: ${dstArchiveFile}`);
|
|
155
|
+
|
|
156
|
+
const { body, statusCode } = await request(archiveUrl);
|
|
157
|
+
state.statusCode = statusCode;
|
|
158
|
+
if (statusCode != 200) {
|
|
159
|
+
// completely draining the stream to prevent leaving open connections
|
|
160
|
+
const textBody = await text(body);
|
|
161
|
+
state.errorMsg = `failed to download archive: ${statusCode}, response: ${textBody.slice(0, 1000)}`;
|
|
162
|
+
logger.error(state.errorMsg);
|
|
163
|
+
throw new Error(state.errorMsg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// to prevent incomplete downloads we first write in a temp file
|
|
167
|
+
state.tmpPath = dstArchiveFile + '.tmp';
|
|
168
|
+
await Readable.toWeb(body).pipeTo(Writable.toWeb(fs.createWriteStream(state.tmpPath)));
|
|
169
|
+
state.wroteTmp = true;
|
|
170
|
+
state.tmpExisted = await fileExists(state.tmpPath);
|
|
171
|
+
|
|
172
|
+
// and then atomically rename it
|
|
173
|
+
await fsp.rename(state.tmpPath, dstArchiveFile);
|
|
174
|
+
state.renamed = true;
|
|
175
|
+
state.newExisted = await fileExists(dstArchiveFile);
|
|
176
|
+
|
|
177
|
+
return state;
|
|
178
|
+
} catch (e: unknown) {
|
|
179
|
+
const msg = `downloadArchive: error ${JSON.stringify(e)} occurred, state: ${JSON.stringify(state)}`;
|
|
180
|
+
logger.error(msg);
|
|
181
|
+
throw new Error(msg);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Used to prevent mid-way interrupted unarchived folders to be used */
|
|
186
|
+
const MarkerFileName = '.ok';
|
|
187
|
+
|
|
188
|
+
export async function extractArchive(
|
|
189
|
+
logger: MiLogger,
|
|
190
|
+
archivePath: string,
|
|
191
|
+
archiveType: ArchiveType,
|
|
192
|
+
dstFolder: string,
|
|
193
|
+
) {
|
|
194
|
+
logger.info('extracting archive...');
|
|
195
|
+
logger.info(` archive path: '${archivePath}'`);
|
|
196
|
+
logger.info(` target dir: '${dstFolder}'`);
|
|
197
|
+
|
|
198
|
+
if (!(await fileExists(archivePath))) {
|
|
199
|
+
const msg = `Platforma Backend binary archive not found at '${archivePath}'`;
|
|
200
|
+
logger.error(msg);
|
|
201
|
+
throw new Error(msg);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const markerFilePath = upath.join(dstFolder, MarkerFileName);
|
|
205
|
+
|
|
206
|
+
if (await fileExists(markerFilePath)) {
|
|
207
|
+
logger.info(`Platforma Backend binaries unpack skipped: '${dstFolder}' exists`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (await fileExists(dstFolder)) {
|
|
212
|
+
logger.info(`Removing previous incompletely unpacked folder: '${dstFolder}'`);
|
|
213
|
+
await fsp.rm(dstFolder, { recursive: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
logger.info(` creating target dir '${dstFolder}'`);
|
|
217
|
+
await fsp.mkdir(dstFolder, { recursive: true });
|
|
218
|
+
|
|
219
|
+
logger.info(
|
|
220
|
+
`Unpacking Platforma Backend archive:\n Archive: ${archivePath}\n Target dir: ${dstFolder}`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
switch (archiveType) {
|
|
224
|
+
case 'tgz':
|
|
225
|
+
await tar.x({
|
|
226
|
+
file: archivePath,
|
|
227
|
+
cwd: dstFolder,
|
|
228
|
+
gzip: true,
|
|
229
|
+
});
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case 'zip':
|
|
233
|
+
await decompress(archivePath, dstFolder);
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
assertNever(archiveType);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// writing marker file, to be able in the future detect that we completely unarchived the tar file
|
|
241
|
+
await fsp.writeFile(markerFilePath, 'ok');
|
|
242
|
+
|
|
243
|
+
logger.info(` ... unpack done.`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export type ArchiveType = 'tgz' | 'zip';
|
|
247
|
+
|
|
248
|
+
const osToArchiveType: Record<OSType, ArchiveType> = {
|
|
249
|
+
linux: 'tgz',
|
|
250
|
+
macos: 'tgz',
|
|
251
|
+
windows: 'zip',
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const osToBinaryName: Record<OSType, string> = {
|
|
255
|
+
linux: 'platforma',
|
|
256
|
+
macos: 'platforma',
|
|
257
|
+
windows: 'platforma.exe',
|
|
258
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
license:
|
|
2
|
+
value: ''
|
|
3
|
+
file: ''
|
|
4
|
+
|
|
5
|
+
logging:
|
|
6
|
+
level: 'info'
|
|
7
|
+
destinations:
|
|
8
|
+
- path: 'platforma.log'
|
|
9
|
+
|
|
10
|
+
monitoring:
|
|
11
|
+
listen: '127.0.0.1:11235'
|
|
12
|
+
|
|
13
|
+
debug:
|
|
14
|
+
listen: '127.0.0.1:11236'
|
|
15
|
+
|
|
16
|
+
core:
|
|
17
|
+
logging:
|
|
18
|
+
extendedInfo: true
|
|
19
|
+
dumpResourceData: true
|
|
20
|
+
|
|
21
|
+
grpc:
|
|
22
|
+
listen: '127.0.0.1:11234'
|
|
23
|
+
tlsEnabled: false
|
|
24
|
+
|
|
25
|
+
authEnabled: false
|
|
26
|
+
auth: []
|
|
27
|
+
db:
|
|
28
|
+
path: './db'
|
|
29
|
+
|
|
30
|
+
controllers:
|
|
31
|
+
data:
|
|
32
|
+
main:
|
|
33
|
+
storages:
|
|
34
|
+
main:
|
|
35
|
+
mode: primary
|
|
36
|
+
downloadable: true
|
|
37
|
+
|
|
38
|
+
work:
|
|
39
|
+
mode: active
|
|
40
|
+
downloadable: false
|
|
41
|
+
|
|
42
|
+
storages:
|
|
43
|
+
- id: 'main'
|
|
44
|
+
type: 'FS'
|
|
45
|
+
indexCachePeriod: '1m'
|
|
46
|
+
rootPath: './storages/main'
|
|
47
|
+
|
|
48
|
+
- id: 'work'
|
|
49
|
+
type: 'FS'
|
|
50
|
+
indexCachePeriod: '1m'
|
|
51
|
+
rootPath: './storages/work'
|
|
52
|
+
|
|
53
|
+
runner:
|
|
54
|
+
type: local
|
|
55
|
+
storageRoot: './storages/work'
|
|
56
|
+
|
|
57
|
+
packageLoader:
|
|
58
|
+
packagesRoot: './packages'
|
|
59
|
+
|
|
60
|
+
workflows: {}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SpawnOptions } from 'child_process';
|
|
2
|
+
import type { PlBinarySource } from '../common/pl_binary';
|
|
3
|
+
|
|
4
|
+
/** Options to start a local pl-core. */
|
|
5
|
+
export type LocalPlOptions = {
|
|
6
|
+
/** From what directory start a process. */
|
|
7
|
+
readonly workingDir: string;
|
|
8
|
+
/** A string representation of yaml config. */
|
|
9
|
+
readonly config: string;
|
|
10
|
+
/** How to get a binary, download it or get an existing one. */
|
|
11
|
+
readonly binary: PlBinarySource;
|
|
12
|
+
/** Additional options for a process, environments, stdout, stderr etc. */
|
|
13
|
+
readonly spawnOptions: SpawnOptions;
|
|
14
|
+
/**
|
|
15
|
+
* If the previous pl-core was started from the same directory,
|
|
16
|
+
* we can check if it's still running and then stop it before starting a new one.
|
|
17
|
+
*/
|
|
18
|
+
readonly closeOld: boolean;
|
|
19
|
+
/** What should we do on closing or if the process exit with error */
|
|
20
|
+
readonly restartMode: LocalPlRestart;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type LocalPlRestart = LocalPlRestartSilent | LocalPlRestartHook;
|
|
24
|
+
|
|
25
|
+
/** Do nothing if the error happened or a process exited. */
|
|
26
|
+
export type LocalPlRestartSilent = {
|
|
27
|
+
type: 'silent';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Run a hook if the error happened or a process exited. */
|
|
31
|
+
export type LocalPlRestartHook = {
|
|
32
|
+
type: 'hook';
|
|
33
|
+
hook(pl: any): void;
|
|
34
|
+
};
|
package/src/local/pid.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileExists } from '@milaboratories/ts-helpers';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import upath from 'upath';
|
|
4
|
+
|
|
5
|
+
export function filePid(dir: string) {
|
|
6
|
+
return upath.join(dir, 'pl_pid');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function readPid(filePath: string): Promise<number | undefined> {
|
|
10
|
+
if (!(await fileExists(filePath))) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const text = await fs.readFile(filePath);
|
|
15
|
+
|
|
16
|
+
return Number(text.toString());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function writePid(filePath: string, pid: number) {
|
|
20
|
+
await fs.writeFile(filePath, JSON.stringify(pid));
|
|
21
|
+
}
|