@kapeta/local-cluster-service 0.6.1 → 0.7.1
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/.eslintrc.cjs +17 -0
- package/.github/workflows/main.yml +22 -22
- package/.prettierignore +4 -0
- package/.vscode/launch.json +2 -4
- package/CHANGELOG.md +14 -0
- package/definitions.d.ts +17 -35
- package/dist/cjs/index.d.ts +27 -0
- package/dist/cjs/index.js +126 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/src/assetManager.d.ts +31 -0
- package/dist/cjs/src/assetManager.js +153 -0
- package/dist/cjs/src/assets/routes.d.ts +3 -0
- package/dist/cjs/src/assets/routes.js +117 -0
- package/dist/cjs/src/clusterService.d.ts +40 -0
- package/dist/cjs/src/clusterService.js +114 -0
- package/dist/cjs/src/codeGeneratorManager.d.ts +8 -0
- package/dist/cjs/src/codeGeneratorManager.js +53 -0
- package/dist/cjs/src/config/routes.d.ts +3 -0
- package/dist/cjs/src/config/routes.js +126 -0
- package/dist/cjs/src/configManager.d.ts +36 -0
- package/dist/cjs/src/configManager.js +110 -0
- package/dist/cjs/src/containerManager.d.ts +89 -0
- package/dist/cjs/src/containerManager.js +365 -0
- package/dist/cjs/src/filesystem/routes.d.ts +3 -0
- package/dist/cjs/src/filesystem/routes.js +69 -0
- package/dist/cjs/src/filesystemManager.d.ts +15 -0
- package/dist/cjs/src/filesystemManager.js +87 -0
- package/dist/cjs/src/identities/routes.d.ts +3 -0
- package/dist/cjs/src/identities/routes.js +18 -0
- package/dist/cjs/src/instanceManager.d.ts +56 -0
- package/dist/cjs/src/instanceManager.js +424 -0
- package/dist/cjs/src/instances/routes.d.ts +3 -0
- package/dist/cjs/src/instances/routes.js +134 -0
- package/dist/cjs/src/middleware/cors.d.ts +2 -0
- package/dist/cjs/src/middleware/cors.js +10 -0
- package/dist/cjs/src/middleware/kapeta.d.ts +11 -0
- package/dist/cjs/src/middleware/kapeta.js +17 -0
- package/dist/cjs/src/middleware/stringBody.d.ts +5 -0
- package/dist/cjs/src/middleware/stringBody.js +14 -0
- package/dist/cjs/src/networkManager.d.ts +32 -0
- package/dist/cjs/src/networkManager.js +109 -0
- package/dist/cjs/src/operatorManager.d.ts +36 -0
- package/dist/cjs/src/operatorManager.js +165 -0
- package/dist/cjs/src/progressListener.d.ts +20 -0
- package/dist/cjs/src/progressListener.js +91 -0
- package/dist/cjs/src/providerManager.d.ts +9 -0
- package/dist/cjs/src/providerManager.js +51 -0
- package/dist/cjs/src/providers/routes.d.ts +3 -0
- package/dist/cjs/src/providers/routes.js +42 -0
- package/dist/cjs/src/proxy/routes.d.ts +3 -0
- package/dist/cjs/src/proxy/routes.js +111 -0
- package/dist/cjs/src/proxy/types/rest.d.ts +4 -0
- package/dist/cjs/src/proxy/types/rest.js +114 -0
- package/dist/cjs/src/proxy/types/web.d.ts +4 -0
- package/dist/cjs/src/proxy/types/web.js +53 -0
- package/dist/cjs/src/repositoryManager.d.ts +17 -0
- package/dist/cjs/src/repositoryManager.js +218 -0
- package/dist/cjs/src/serviceManager.d.ts +29 -0
- package/dist/cjs/src/serviceManager.js +99 -0
- package/dist/cjs/src/socketManager.d.ts +14 -0
- package/dist/cjs/src/socketManager.js +53 -0
- package/dist/cjs/src/storageService.d.ts +17 -0
- package/dist/cjs/src/storageService.js +74 -0
- package/dist/cjs/src/traffic/routes.d.ts +3 -0
- package/dist/cjs/src/traffic/routes.js +18 -0
- package/dist/cjs/src/types.d.ts +88 -0
- package/dist/cjs/src/types.js +2 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +29 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +468 -0
- package/dist/cjs/src/utils/LogData.d.ts +19 -0
- package/dist/cjs/src/utils/LogData.js +43 -0
- package/dist/cjs/src/utils/pathTemplateParser.d.ts +26 -0
- package/dist/cjs/src/utils/pathTemplateParser.js +121 -0
- package/dist/cjs/src/utils/utils.d.ts +1 -0
- package/dist/cjs/src/utils/utils.js +18 -0
- package/dist/cjs/start.d.ts +1 -0
- package/dist/cjs/start.js +12 -0
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +121 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/src/assetManager.d.ts +31 -0
- package/{src → dist/esm/src}/assetManager.js +22 -60
- package/dist/esm/src/assets/routes.d.ts +3 -0
- package/{src → dist/esm/src}/assets/routes.js +21 -36
- package/dist/esm/src/clusterService.d.ts +40 -0
- package/{src → dist/esm/src}/clusterService.js +14 -37
- package/dist/esm/src/codeGeneratorManager.d.ts +8 -0
- package/{src → dist/esm/src}/codeGeneratorManager.js +15 -24
- package/dist/esm/src/config/routes.d.ts +3 -0
- package/{src → dist/esm/src}/config/routes.js +40 -89
- package/dist/esm/src/configManager.d.ts +36 -0
- package/{src → dist/esm/src}/configManager.js +11 -40
- package/dist/esm/src/containerManager.d.ts +89 -0
- package/{src → dist/esm/src}/containerManager.js +81 -182
- package/dist/esm/src/filesystem/routes.d.ts +3 -0
- package/dist/esm/src/filesystem/routes.js +64 -0
- package/dist/esm/src/filesystemManager.d.ts +15 -0
- package/{src → dist/esm/src}/filesystemManager.js +20 -28
- package/dist/esm/src/identities/routes.d.ts +3 -0
- package/dist/esm/src/identities/routes.js +13 -0
- package/dist/esm/src/instanceManager.d.ts +56 -0
- package/{src → dist/esm/src}/instanceManager.js +88 -179
- package/dist/esm/src/instances/routes.d.ts +3 -0
- package/{src → dist/esm/src}/instances/routes.js +31 -70
- package/dist/esm/src/middleware/cors.d.ts +2 -0
- package/{src → dist/esm/src}/middleware/cors.js +2 -3
- package/dist/esm/src/middleware/kapeta.d.ts +11 -0
- package/{src → dist/esm/src}/middleware/kapeta.js +3 -7
- package/dist/esm/src/middleware/stringBody.d.ts +5 -0
- package/{src → dist/esm/src}/middleware/stringBody.js +2 -3
- package/dist/esm/src/networkManager.d.ts +32 -0
- package/{src → dist/esm/src}/networkManager.js +16 -33
- package/dist/esm/src/operatorManager.d.ts +36 -0
- package/{src → dist/esm/src}/operatorManager.js +35 -91
- package/dist/esm/src/progressListener.d.ts +20 -0
- package/dist/esm/src/progressListener.js +88 -0
- package/dist/esm/src/providerManager.d.ts +9 -0
- package/dist/esm/src/providerManager.js +45 -0
- package/dist/esm/src/providers/routes.d.ts +3 -0
- package/{src → dist/esm/src}/providers/routes.js +10 -16
- package/dist/esm/src/proxy/routes.d.ts +3 -0
- package/dist/esm/src/proxy/routes.js +106 -0
- package/dist/esm/src/proxy/types/rest.d.ts +4 -0
- package/dist/esm/src/proxy/types/rest.js +107 -0
- package/dist/esm/src/proxy/types/web.d.ts +4 -0
- package/{src → dist/esm/src}/proxy/types/web.js +13 -35
- package/dist/esm/src/repositoryManager.d.ts +17 -0
- package/dist/esm/src/repositoryManager.js +212 -0
- package/dist/esm/src/serviceManager.d.ts +29 -0
- package/{src → dist/esm/src}/serviceManager.js +12 -42
- package/dist/esm/src/socketManager.d.ts +14 -0
- package/{src → dist/esm/src}/socketManager.js +19 -23
- package/dist/esm/src/storageService.d.ts +17 -0
- package/{src → dist/esm/src}/storageService.js +8 -27
- package/dist/esm/src/traffic/routes.d.ts +3 -0
- package/{src → dist/esm/src}/traffic/routes.js +4 -9
- package/dist/esm/src/types.d.ts +88 -0
- package/dist/esm/src/types.js +1 -0
- package/dist/esm/src/utils/BlockInstanceRunner.d.ts +29 -0
- package/{src → dist/esm/src}/utils/BlockInstanceRunner.js +137 -256
- package/dist/esm/src/utils/LogData.d.ts +19 -0
- package/{src → dist/esm/src}/utils/LogData.js +11 -22
- package/dist/esm/src/utils/pathTemplateParser.d.ts +26 -0
- package/{src → dist/esm/src}/utils/pathTemplateParser.js +21 -40
- package/dist/esm/src/utils/utils.d.ts +1 -0
- package/dist/esm/src/utils/utils.js +11 -0
- package/dist/esm/start.d.ts +1 -0
- package/dist/esm/start.js +7 -0
- package/index.ts +147 -0
- package/package.json +106 -74
- package/src/assetManager.ts +191 -0
- package/src/assets/routes.ts +132 -0
- package/src/clusterService.ts +134 -0
- package/src/codeGeneratorManager.ts +57 -0
- package/src/config/routes.ts +159 -0
- package/src/configManager.ts +148 -0
- package/src/containerManager.ts +466 -0
- package/src/filesystem/routes.ts +74 -0
- package/src/filesystemManager.ts +93 -0
- package/src/identities/routes.ts +20 -0
- package/src/instanceManager.ts +503 -0
- package/src/instances/routes.ts +164 -0
- package/src/middleware/cors.ts +9 -0
- package/src/middleware/kapeta.ts +27 -0
- package/src/middleware/stringBody.ts +16 -0
- package/src/networkManager.ts +137 -0
- package/src/operatorManager.ts +221 -0
- package/src/progressListener.ts +102 -0
- package/src/{providerManager.js → providerManager.ts} +15 -31
- package/src/providers/routes.ts +46 -0
- package/src/proxy/routes.ts +148 -0
- package/src/proxy/types/{rest.js → rest.ts} +30 -30
- package/src/proxy/types/web.ts +60 -0
- package/src/{repositoryManager.js → repositoryManager.ts} +49 -73
- package/src/serviceManager.ts +120 -0
- package/src/socketManager.ts +57 -0
- package/src/storageService.ts +88 -0
- package/src/traffic/routes.ts +18 -0
- package/src/types.ts +97 -0
- package/src/utils/BlockInstanceRunner.ts +555 -0
- package/src/utils/LogData.ts +47 -0
- package/src/utils/pathTemplateParser.ts +138 -0
- package/src/utils/utils.ts +12 -0
- package/start.ts +8 -0
- package/tsconfig.json +13 -0
- package/index.js +0 -127
- package/src/filesystem/routes.js +0 -74
- package/src/identities/routes.js +0 -19
- package/src/progressListener.js +0 -82
- package/src/proxy/routes.js +0 -126
- package/src/utils/utils.js +0 -13
- package/start.js +0 -7
@@ -0,0 +1,93 @@
|
|
1
|
+
import Path from 'path';
|
2
|
+
import FS from 'fs';
|
3
|
+
import FSExtra from 'fs-extra';
|
4
|
+
import { storageService } from './storageService';
|
5
|
+
|
6
|
+
const SECTION_ID = 'filesystem';
|
7
|
+
const PROJECT_ROOT = 'project_root';
|
8
|
+
|
9
|
+
function isFile(path: string) {
|
10
|
+
try {
|
11
|
+
return FS.statSync(path).isFile();
|
12
|
+
} catch (error) {
|
13
|
+
return false;
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
class FilesystemManager {
|
18
|
+
async writeFile(path: string, data: string | Buffer) {
|
19
|
+
const dirName = Path.dirname(path);
|
20
|
+
console.log('Dir name', dirName, path);
|
21
|
+
if (!FS.existsSync(dirName)) {
|
22
|
+
console.log('Making folder', dirName);
|
23
|
+
FSExtra.mkdirpSync(dirName, {});
|
24
|
+
}
|
25
|
+
FS.writeFileSync(path, data);
|
26
|
+
}
|
27
|
+
|
28
|
+
async createFolder(path: string): Promise<void> {
|
29
|
+
return new Promise((resolve, reject) => {
|
30
|
+
FS.mkdir(path, (err) => {
|
31
|
+
if (err) {
|
32
|
+
err.message += '. You can only create one single folder at a time.';
|
33
|
+
reject(err.message);
|
34
|
+
return;
|
35
|
+
}
|
36
|
+
resolve();
|
37
|
+
});
|
38
|
+
});
|
39
|
+
}
|
40
|
+
|
41
|
+
async readDirectory(path: string): Promise<{ path: string; folder: boolean }[]> {
|
42
|
+
return new Promise((resolve, reject) => {
|
43
|
+
let response: { path: string; folder: boolean }[] = [];
|
44
|
+
FS.readdir(path, (err: any, files: string[]) => {
|
45
|
+
if (err) {
|
46
|
+
reject(new Error(err));
|
47
|
+
return;
|
48
|
+
}
|
49
|
+
files.forEach((file) => {
|
50
|
+
response.push({
|
51
|
+
path: Path.join(path, file),
|
52
|
+
folder: FS.lstatSync(Path.join(path, file)).isDirectory(),
|
53
|
+
});
|
54
|
+
});
|
55
|
+
resolve(response);
|
56
|
+
});
|
57
|
+
});
|
58
|
+
}
|
59
|
+
|
60
|
+
async readFile(path: string): Promise<Buffer> {
|
61
|
+
return new Promise((resolve, reject) => {
|
62
|
+
if (!isFile(path)) {
|
63
|
+
reject(
|
64
|
+
new Error(
|
65
|
+
'The path provided is invalid.Please check that the path and file name that were provided are spelled correctly. '
|
66
|
+
)
|
67
|
+
);
|
68
|
+
} else {
|
69
|
+
FS.readFile(path, (err, data) => {
|
70
|
+
if (err) {
|
71
|
+
reject(new Error(err.message));
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
resolve(data);
|
75
|
+
});
|
76
|
+
}
|
77
|
+
});
|
78
|
+
}
|
79
|
+
|
80
|
+
getRootFolder(): string {
|
81
|
+
return require('os').homedir();
|
82
|
+
}
|
83
|
+
|
84
|
+
getProjectRootFolder(): string | undefined {
|
85
|
+
return storageService.get(SECTION_ID, PROJECT_ROOT);
|
86
|
+
}
|
87
|
+
|
88
|
+
setProjectRootFolder(folder: string) {
|
89
|
+
storageService.put(SECTION_ID, PROJECT_ROOT, folder);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
export const filesystemManager = new FilesystemManager();
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import Router from 'express-promise-router';
|
2
|
+
import { KapetaAPI } from '@kapeta/nodejs-api-client';
|
3
|
+
|
4
|
+
import { corsHandler } from '../middleware/cors';
|
5
|
+
import { Request, Response } from 'express';
|
6
|
+
|
7
|
+
const router = Router();
|
8
|
+
const api = new KapetaAPI();
|
9
|
+
|
10
|
+
router.use('/', corsHandler);
|
11
|
+
|
12
|
+
router.get('/current', async (req: Request, res: Response) => {
|
13
|
+
res.send(await api.getCurrentIdentity());
|
14
|
+
});
|
15
|
+
|
16
|
+
router.get('/:identityId/memberships', async (req: Request, res: Response) => {
|
17
|
+
res.send(await api.getMemberships(req.params.identityId));
|
18
|
+
});
|
19
|
+
|
20
|
+
export default router;
|
@@ -0,0 +1,503 @@
|
|
1
|
+
import _ from 'lodash';
|
2
|
+
import request from 'request';
|
3
|
+
import EventEmitter from 'events';
|
4
|
+
import { BlockInstanceRunner } from './utils/BlockInstanceRunner';
|
5
|
+
import { storageService } from './storageService';
|
6
|
+
import { socketManager } from './socketManager';
|
7
|
+
import { serviceManager } from './serviceManager';
|
8
|
+
import { assetManager } from './assetManager';
|
9
|
+
import { containerManager } from './containerManager';
|
10
|
+
import { configManager } from './configManager';
|
11
|
+
import { InstanceInfo, LogEntry, ProcessInfo } from './types';
|
12
|
+
import { BlockInstance } from '@kapeta/schemas';
|
13
|
+
|
14
|
+
const CHECK_INTERVAL = 10000;
|
15
|
+
const DEFAULT_HEALTH_PORT_TYPE = 'rest';
|
16
|
+
|
17
|
+
const EVENT_STATUS_CHANGED = 'status-changed';
|
18
|
+
const EVENT_INSTANCE_CREATED = 'instance-created';
|
19
|
+
const EVENT_INSTANCE_EXITED = 'instance-exited';
|
20
|
+
const EVENT_INSTANCE_LOG = 'instance-log';
|
21
|
+
|
22
|
+
const STATUS_STARTING = 'starting';
|
23
|
+
const STATUS_READY = 'ready';
|
24
|
+
const STATUS_UNHEALTHY = 'unhealthy';
|
25
|
+
const STATUS_STOPPED = 'stopped';
|
26
|
+
|
27
|
+
const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs - it failed
|
28
|
+
|
29
|
+
class InstanceManager {
|
30
|
+
private _interval: NodeJS.Timer;
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Contains an array of running instances that have self-registered with this
|
34
|
+
* cluster service. This is done by the Kapeta SDKs
|
35
|
+
*/
|
36
|
+
private _instances: InstanceInfo[] = [];
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Contains the process info for the instances started by this manager. In memory only
|
40
|
+
* so can't be relied on for knowing everything that's running.
|
41
|
+
*
|
42
|
+
*/
|
43
|
+
private _processes: { [systemId: string]: { [instanceId: string]: ProcessInfo } } = {};
|
44
|
+
|
45
|
+
constructor() {
|
46
|
+
this._interval = setInterval(() => this._checkInstances(), CHECK_INTERVAL);
|
47
|
+
this._instances = storageService.section('instances', []);
|
48
|
+
this._processes = {};
|
49
|
+
|
50
|
+
this._checkInstances();
|
51
|
+
}
|
52
|
+
|
53
|
+
_save() {
|
54
|
+
storageService.put('instances', this._instances);
|
55
|
+
}
|
56
|
+
|
57
|
+
async _checkInstances() {
|
58
|
+
let changed = false;
|
59
|
+
for (let i = 0; i < this._instances.length; i++) {
|
60
|
+
const instance = this._instances[i];
|
61
|
+
|
62
|
+
const newStatus = await this._getInstanceStatus(instance);
|
63
|
+
|
64
|
+
if (newStatus === STATUS_UNHEALTHY && instance.status === STATUS_STARTING) {
|
65
|
+
// If instance is starting we consider unhealthy an indication
|
66
|
+
// that it is still starting
|
67
|
+
continue;
|
68
|
+
}
|
69
|
+
|
70
|
+
if (instance.status !== newStatus) {
|
71
|
+
instance.status = newStatus;
|
72
|
+
console.log(
|
73
|
+
'Instance status changed: %s %s -> %s',
|
74
|
+
instance.systemId,
|
75
|
+
instance.instanceId,
|
76
|
+
instance.status
|
77
|
+
);
|
78
|
+
this._emit(instance.systemId, EVENT_STATUS_CHANGED, instance);
|
79
|
+
changed = true;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
if (changed) {
|
84
|
+
this._save();
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
async _isRunning(instance: InstanceInfo) {
|
89
|
+
if (!instance.pid) {
|
90
|
+
return;
|
91
|
+
}
|
92
|
+
|
93
|
+
if (instance.type === 'docker') {
|
94
|
+
const container = await containerManager.get(instance.pid as string);
|
95
|
+
if (!container) {
|
96
|
+
console.warn('Container not found: %s', instance.pid);
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
return await container.isRunning();
|
100
|
+
}
|
101
|
+
|
102
|
+
//Otherwise its just a normal process.
|
103
|
+
//TODO: Handle for Windows
|
104
|
+
try {
|
105
|
+
return process.kill(instance.pid as number, 0);
|
106
|
+
} catch (err: any) {
|
107
|
+
return err.code === 'EPERM';
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
async _getInstanceStatus(instance: InstanceInfo): Promise<string> {
|
112
|
+
if (instance.status === STATUS_STOPPED) {
|
113
|
+
//Will only change when it reregisters
|
114
|
+
return STATUS_STOPPED;
|
115
|
+
}
|
116
|
+
|
117
|
+
if (!(await this._isRunning(instance))) {
|
118
|
+
return STATUS_STOPPED;
|
119
|
+
}
|
120
|
+
|
121
|
+
if (!instance.health) {
|
122
|
+
//No health url means we assume it's healthy as soon as it's running
|
123
|
+
return STATUS_READY;
|
124
|
+
}
|
125
|
+
|
126
|
+
return new Promise((resolve) => {
|
127
|
+
if (!instance.health) {
|
128
|
+
resolve(STATUS_READY);
|
129
|
+
return;
|
130
|
+
}
|
131
|
+
request(instance.health, (err, response) => {
|
132
|
+
if (err) {
|
133
|
+
resolve(STATUS_UNHEALTHY);
|
134
|
+
return;
|
135
|
+
}
|
136
|
+
|
137
|
+
if (response.statusCode > 399) {
|
138
|
+
resolve(STATUS_UNHEALTHY);
|
139
|
+
return;
|
140
|
+
}
|
141
|
+
|
142
|
+
resolve(STATUS_READY);
|
143
|
+
});
|
144
|
+
});
|
145
|
+
}
|
146
|
+
|
147
|
+
getInstances() {
|
148
|
+
if (!this._instances) {
|
149
|
+
return [];
|
150
|
+
}
|
151
|
+
|
152
|
+
return [...this._instances];
|
153
|
+
}
|
154
|
+
|
155
|
+
getInstancesForPlan(systemId: string) {
|
156
|
+
if (!this._instances) {
|
157
|
+
return [];
|
158
|
+
}
|
159
|
+
|
160
|
+
return this._instances.filter((instance) => instance.systemId === systemId);
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* Get instance information
|
165
|
+
*
|
166
|
+
* @param {string} systemId
|
167
|
+
* @param {string} instanceId
|
168
|
+
* @return {*}
|
169
|
+
*/
|
170
|
+
getInstance(systemId: string, instanceId: string) {
|
171
|
+
return _.find(this._instances, { systemId, instanceId });
|
172
|
+
}
|
173
|
+
|
174
|
+
/**
|
175
|
+
*
|
176
|
+
* @param {string} systemId
|
177
|
+
* @param {string} instanceId
|
178
|
+
* @param {InstanceInfo} info
|
179
|
+
* @return {Promise<void>}
|
180
|
+
*/
|
181
|
+
async registerInstance(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>) {
|
182
|
+
let instance = this.getInstance(systemId, instanceId);
|
183
|
+
|
184
|
+
//Get target address
|
185
|
+
let address = await serviceManager.getProviderAddress(
|
186
|
+
systemId,
|
187
|
+
instanceId,
|
188
|
+
info.portType ?? DEFAULT_HEALTH_PORT_TYPE
|
189
|
+
);
|
190
|
+
|
191
|
+
let healthUrl = null;
|
192
|
+
let health = info.health;
|
193
|
+
if (health) {
|
194
|
+
if (health.startsWith('/')) {
|
195
|
+
health = health.substring(1);
|
196
|
+
}
|
197
|
+
healthUrl = address + health;
|
198
|
+
}
|
199
|
+
|
200
|
+
if (instance) {
|
201
|
+
instance.status = STATUS_STARTING;
|
202
|
+
instance.pid = info.pid;
|
203
|
+
instance.address = address;
|
204
|
+
if (info.type) {
|
205
|
+
instance.type = info.type;
|
206
|
+
}
|
207
|
+
if (healthUrl) {
|
208
|
+
instance.health = healthUrl;
|
209
|
+
}
|
210
|
+
this._emit(systemId, EVENT_STATUS_CHANGED, instance);
|
211
|
+
} else {
|
212
|
+
instance = {
|
213
|
+
systemId,
|
214
|
+
instanceId,
|
215
|
+
status: STATUS_STARTING,
|
216
|
+
pid: info.pid,
|
217
|
+
type: info.type,
|
218
|
+
health: healthUrl,
|
219
|
+
address,
|
220
|
+
};
|
221
|
+
|
222
|
+
this._instances.push(instance);
|
223
|
+
|
224
|
+
this._emit(systemId, EVENT_INSTANCE_CREATED, instance);
|
225
|
+
}
|
226
|
+
|
227
|
+
this._save();
|
228
|
+
}
|
229
|
+
|
230
|
+
setInstanceAsStopped(systemId: string, instanceId: string) {
|
231
|
+
const instance = _.find(this._instances, { systemId, instanceId });
|
232
|
+
if (instance) {
|
233
|
+
instance.status = STATUS_STOPPED;
|
234
|
+
instance.pid = null;
|
235
|
+
instance.health = null;
|
236
|
+
this._emit(systemId, EVENT_STATUS_CHANGED, instance);
|
237
|
+
this._save();
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
_emit(systemId: string, type: string, payload: any) {
|
242
|
+
try {
|
243
|
+
socketManager.emit(`${systemId}/instances`, type, payload);
|
244
|
+
} catch (e: any) {
|
245
|
+
console.warn('Failed to emit instance event: %s', e.message);
|
246
|
+
}
|
247
|
+
}
|
248
|
+
|
249
|
+
async createProcessesForPlan(planRef: string): Promise<ProcessInfo[]> {
|
250
|
+
await this.stopAllForPlan(planRef);
|
251
|
+
|
252
|
+
const plan = await assetManager.getPlan(planRef, true);
|
253
|
+
if (!plan) {
|
254
|
+
throw new Error('Plan not found: ' + planRef);
|
255
|
+
}
|
256
|
+
|
257
|
+
if (!plan.spec.blocks) {
|
258
|
+
console.warn('No blocks found in plan', planRef);
|
259
|
+
return [];
|
260
|
+
}
|
261
|
+
|
262
|
+
let promises: Promise<ProcessInfo>[] = [];
|
263
|
+
let errors = [];
|
264
|
+
for (let blockInstance of Object.values(plan.spec.blocks as BlockInstance[])) {
|
265
|
+
try {
|
266
|
+
promises.push(this.createProcess(planRef, blockInstance.id));
|
267
|
+
} catch (e) {
|
268
|
+
errors.push(e);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
const settled = await Promise.allSettled(promises);
|
273
|
+
|
274
|
+
if (errors.length > 0) {
|
275
|
+
throw errors[0];
|
276
|
+
}
|
277
|
+
|
278
|
+
return settled.map((p) => (p.status === 'fulfilled' ? p.value : null)).filter((p) => !!p) as ProcessInfo[];
|
279
|
+
}
|
280
|
+
|
281
|
+
async _stopInstance(instance: InstanceInfo) {
|
282
|
+
if (!instance.pid) {
|
283
|
+
return;
|
284
|
+
}
|
285
|
+
|
286
|
+
if (instance.status === 'stopped') {
|
287
|
+
return;
|
288
|
+
}
|
289
|
+
|
290
|
+
try {
|
291
|
+
if (instance.type === 'docker') {
|
292
|
+
const container = await containerManager.get(instance.pid as string);
|
293
|
+
if (container) {
|
294
|
+
try {
|
295
|
+
await container.stop();
|
296
|
+
} catch (e) {
|
297
|
+
console.error('Failed to stop container', e);
|
298
|
+
}
|
299
|
+
}
|
300
|
+
return;
|
301
|
+
}
|
302
|
+
process.kill(instance.pid as number, 'SIGTERM');
|
303
|
+
} catch (e) {
|
304
|
+
console.error('Failed to stop process', e);
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
308
|
+
async stopAllForPlan(planRef: string) {
|
309
|
+
if (this._processes[planRef]) {
|
310
|
+
const promises = [];
|
311
|
+
console.log('Stopping all processes for plan', planRef);
|
312
|
+
for (let instance of Object.values(this._processes[planRef])) {
|
313
|
+
promises.push(instance.stop());
|
314
|
+
}
|
315
|
+
|
316
|
+
await Promise.all(promises);
|
317
|
+
|
318
|
+
this._processes[planRef] = {};
|
319
|
+
}
|
320
|
+
|
321
|
+
//Also stop instances not being maintained by the cluster service
|
322
|
+
const instancesForPlan = this._instances.filter((instance) => instance.systemId === planRef);
|
323
|
+
|
324
|
+
const promises = [];
|
325
|
+
for (let instance of instancesForPlan) {
|
326
|
+
promises.push(this._stopInstance(instance));
|
327
|
+
}
|
328
|
+
|
329
|
+
await Promise.all(promises);
|
330
|
+
}
|
331
|
+
|
332
|
+
async createProcess(planRef: string, instanceId: string): Promise<ProcessInfo> {
|
333
|
+
const plan = await assetManager.getPlan(planRef, true);
|
334
|
+
if (!plan) {
|
335
|
+
throw new Error('Plan not found: ' + planRef);
|
336
|
+
}
|
337
|
+
|
338
|
+
const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, { id: instanceId }) : null;
|
339
|
+
if (!blockInstance) {
|
340
|
+
throw new Error('Block instance not found: ' + instanceId);
|
341
|
+
}
|
342
|
+
|
343
|
+
const blockRef = blockInstance.block.ref;
|
344
|
+
|
345
|
+
const blockAsset = await assetManager.getAsset(blockRef, true);
|
346
|
+
const instanceConfig = await configManager.getConfigForSection(planRef, instanceId);
|
347
|
+
|
348
|
+
if (!blockAsset) {
|
349
|
+
throw new Error('Block not found: ' + blockRef);
|
350
|
+
}
|
351
|
+
|
352
|
+
if (!this._processes[planRef]) {
|
353
|
+
this._processes[planRef] = {};
|
354
|
+
}
|
355
|
+
|
356
|
+
await this.stopProcess(planRef, instanceId);
|
357
|
+
const type = blockAsset.version === 'local' ? 'local' : 'docker';
|
358
|
+
|
359
|
+
const runner = new BlockInstanceRunner(planRef);
|
360
|
+
|
361
|
+
const startTime = Date.now();
|
362
|
+
try {
|
363
|
+
const process = await runner.start(blockRef, instanceId, instanceConfig);
|
364
|
+
//emit stdout/stderr via sockets
|
365
|
+
process.output.on('data', (data: Buffer) => {
|
366
|
+
const payload = {
|
367
|
+
source: 'stdout',
|
368
|
+
level: 'INFO',
|
369
|
+
message: data.toString(),
|
370
|
+
time: Date.now(),
|
371
|
+
};
|
372
|
+
this._emit(instanceId, EVENT_INSTANCE_LOG, payload);
|
373
|
+
});
|
374
|
+
|
375
|
+
process.output.on('exit', (exitCode: number) => {
|
376
|
+
const timeRunning = Date.now() - startTime;
|
377
|
+
const instance = this.getInstance(planRef, instanceId);
|
378
|
+
if (instance?.status === STATUS_READY) {
|
379
|
+
//It's already been running
|
380
|
+
return;
|
381
|
+
}
|
382
|
+
|
383
|
+
if (exitCode === 143 || exitCode === 137) {
|
384
|
+
//Process got SIGTERM (143) or SIGKILL (137)
|
385
|
+
//TODO: Windows?
|
386
|
+
return;
|
387
|
+
}
|
388
|
+
|
389
|
+
if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
|
390
|
+
this._emit(blockInstance.id, EVENT_INSTANCE_EXITED, {
|
391
|
+
error: 'Failed to start instance',
|
392
|
+
status: EVENT_INSTANCE_EXITED,
|
393
|
+
instanceId: blockInstance.id,
|
394
|
+
});
|
395
|
+
}
|
396
|
+
});
|
397
|
+
|
398
|
+
await this.registerInstance(planRef, instanceId, {
|
399
|
+
type: process.type,
|
400
|
+
pid: process.pid ?? -1,
|
401
|
+
health: null,
|
402
|
+
portType: process.portType,
|
403
|
+
status: STATUS_STARTING,
|
404
|
+
});
|
405
|
+
|
406
|
+
return (this._processes[planRef][instanceId] = process);
|
407
|
+
} catch (e: any) {
|
408
|
+
console.warn('Failed to start instance', e);
|
409
|
+
const logs: LogEntry[] = [
|
410
|
+
{
|
411
|
+
source: 'stdout',
|
412
|
+
level: 'ERROR',
|
413
|
+
message: e.message,
|
414
|
+
time: Date.now(),
|
415
|
+
},
|
416
|
+
];
|
417
|
+
|
418
|
+
await this.registerInstance(planRef, instanceId, {
|
419
|
+
type: 'local',
|
420
|
+
pid: null,
|
421
|
+
health: null,
|
422
|
+
portType: DEFAULT_HEALTH_PORT_TYPE,
|
423
|
+
status: STATUS_UNHEALTHY,
|
424
|
+
});
|
425
|
+
|
426
|
+
this._emit(instanceId, EVENT_INSTANCE_LOG, logs[0]);
|
427
|
+
|
428
|
+
this._emit(blockInstance.id, EVENT_INSTANCE_EXITED, {
|
429
|
+
error: `Failed to start instance: ${e.message}`,
|
430
|
+
status: EVENT_INSTANCE_EXITED,
|
431
|
+
instanceId: blockInstance.id,
|
432
|
+
});
|
433
|
+
|
434
|
+
return (this._processes[planRef][instanceId] = {
|
435
|
+
pid: -1,
|
436
|
+
type,
|
437
|
+
logs: () => logs,
|
438
|
+
stop: () => Promise.resolve(),
|
439
|
+
ref: blockRef,
|
440
|
+
id: instanceId,
|
441
|
+
name: blockInstance.name,
|
442
|
+
output: new EventEmitter(),
|
443
|
+
});
|
444
|
+
}
|
445
|
+
}
|
446
|
+
|
447
|
+
/**
|
448
|
+
*
|
449
|
+
* @param {string} planRef
|
450
|
+
* @param {string} instanceId
|
451
|
+
* @return {ProcessInfo|null}
|
452
|
+
*/
|
453
|
+
getProcessForInstance(planRef: string, instanceId: string) {
|
454
|
+
if (!this._processes[planRef]) {
|
455
|
+
return null;
|
456
|
+
}
|
457
|
+
|
458
|
+
return this._processes[planRef][instanceId];
|
459
|
+
}
|
460
|
+
|
461
|
+
async restartIfRunning(planRef: string, instanceId: string) {
|
462
|
+
if (!this._processes[planRef] || !this._processes[planRef][instanceId]) {
|
463
|
+
return;
|
464
|
+
}
|
465
|
+
|
466
|
+
// createProcess will stop the process first if it's running
|
467
|
+
return this.createProcess(planRef, instanceId);
|
468
|
+
}
|
469
|
+
|
470
|
+
async stopProcess(planRef: string, instanceId: string) {
|
471
|
+
if (!this._processes[planRef]) {
|
472
|
+
return;
|
473
|
+
}
|
474
|
+
|
475
|
+
if (this._processes[planRef][instanceId]) {
|
476
|
+
try {
|
477
|
+
await this._processes[planRef][instanceId].stop();
|
478
|
+
} catch (e) {
|
479
|
+
console.error('Failed to stop process for instance: %s -> %s', planRef, instanceId, e);
|
480
|
+
}
|
481
|
+
delete this._processes[planRef][instanceId];
|
482
|
+
}
|
483
|
+
}
|
484
|
+
|
485
|
+
async stopAllProcesses() {
|
486
|
+
for (let processesForPlan of Object.values(this._processes)) {
|
487
|
+
for (let processInfo of Object.values(processesForPlan)) {
|
488
|
+
await processInfo.stop();
|
489
|
+
}
|
490
|
+
}
|
491
|
+
this._processes = {};
|
492
|
+
|
493
|
+
for (let instance of this._instances) {
|
494
|
+
await this._stopInstance(instance);
|
495
|
+
}
|
496
|
+
}
|
497
|
+
}
|
498
|
+
|
499
|
+
export const instanceManager = new InstanceManager();
|
500
|
+
|
501
|
+
process.on('exit', async () => {
|
502
|
+
await instanceManager.stopAllProcesses();
|
503
|
+
});
|