@kapeta/local-cluster-service 0.0.60

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,416 @@
1
+ const _ = require('lodash');
2
+ const request = require('request');
3
+
4
+ const {BlockInstanceRunner} = require('@kapeta/local-cluster-executor');
5
+
6
+ const storageService = require('./storageService');
7
+ const socketManager = require('./socketManager');
8
+ const serviceManager = require('./serviceManager');
9
+ const assetManager = require('./assetManager');
10
+ const containerManager = require('./containerManager');
11
+
12
+ const CHECK_INTERVAL = 10000;
13
+ const DEFAULT_HEALTH_PORT_TYPE = 'rest';
14
+
15
+ const EVENT_STATUS_CHANGED = 'status-changed';
16
+ const EVENT_INSTANCE_CREATED = 'instance-created';
17
+ const EVENT_INSTANCE_EXITED = 'instance-exited';
18
+ const EVENT_INSTANCE_LOG = 'instance-log';
19
+
20
+ const STATUS_STARTING = 'starting';
21
+ const STATUS_READY = 'ready';
22
+ const STATUS_UNHEALTHY = 'unhealthy';
23
+ const STATUS_STOPPED = 'stopped';
24
+
25
+ const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs - it failed
26
+
27
+ class InstanceManager {
28
+ constructor() {
29
+ this._interval = setInterval(() => this._checkInstances(), CHECK_INTERVAL);
30
+ /**
31
+ * Contains an array of running instances that have self-registered with this
32
+ * cluster service. This is done by the Kapeta SDKs
33
+ *
34
+ * @type {any[]}
35
+ * @private
36
+ */
37
+ this._instances = storageService.section('instances', []);
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
+ * @type {{[systemId:string]:{[instanceId:string]:ProcessInfo}}}
43
+ * @private
44
+ */
45
+ this._processes = {};
46
+
47
+ this._checkInstances();
48
+ }
49
+
50
+ _save() {
51
+ storageService.put('instances', this._instances);
52
+ }
53
+
54
+ async _checkInstances() {
55
+ let changed = false;
56
+ for (let i = 0; i < this._instances.length; i++) {
57
+ const instance = this._instances[i];
58
+
59
+ const newStatus = await this._getInstanceStatus(instance);
60
+
61
+ if (newStatus === STATUS_UNHEALTHY &&
62
+ instance.status === STATUS_STARTING) {
63
+ // If instance is starting we consider unhealthy an indication
64
+ // that it is still starting
65
+ continue;
66
+ }
67
+
68
+ if (instance.status !== newStatus) {
69
+ instance.status = newStatus;
70
+ this._emit(instance.systemId, EVENT_STATUS_CHANGED, instance);
71
+ changed = true;
72
+ }
73
+ }
74
+
75
+ if (changed) {
76
+ this._save();
77
+ }
78
+ }
79
+
80
+ async _isRunning(instance) {
81
+ if (!instance.pid) {
82
+ return;
83
+ }
84
+
85
+ if (instance.type === 'docker') {
86
+ const container = await containerManager.get(instance.pid);
87
+ return await container.isRunning()
88
+ }
89
+
90
+ //Otherwise its just a normal process.
91
+ //TODO: Handle for Windows
92
+ try {
93
+ return process.kill(instance.pid, 0)
94
+ } catch (err) {
95
+ return err.code === 'EPERM';
96
+ }
97
+ }
98
+
99
+ async _getInstanceStatus(instance) {
100
+ if (instance.status === STATUS_STOPPED) {
101
+ //Will only change when it reregisters
102
+ return STATUS_STOPPED;
103
+ }
104
+
105
+ if (!await this._isRunning(instance)) {
106
+ return STATUS_STOPPED;
107
+ }
108
+
109
+ if (!instance.health) {
110
+ //No health url means we assume it's healthy as soon as it's running
111
+ return STATUS_READY;
112
+ }
113
+
114
+ return new Promise((resolve) => {
115
+ request(instance.health, (err, response) => {
116
+ if (err) {
117
+ resolve(STATUS_UNHEALTHY);
118
+ return;
119
+ }
120
+
121
+ if (response.responseCode > 399) {
122
+ resolve(STATUS_UNHEALTHY);
123
+ return;
124
+ }
125
+
126
+ resolve(STATUS_READY);
127
+ });
128
+ });
129
+ }
130
+
131
+ getInstances() {
132
+ if (!this._instances) {
133
+ return [];
134
+ }
135
+
136
+ return [...this._instances];
137
+ }
138
+
139
+ getInstancesForPlan(systemId) {
140
+ if (!this._instances) {
141
+ return [];
142
+ }
143
+
144
+ return this._instances.filter(instance => instance.systemId === systemId);
145
+ }
146
+
147
+ /**
148
+ * Get instance information
149
+ *
150
+ * @param {string} systemId
151
+ * @param {string} instanceId
152
+ * @return {*}
153
+ */
154
+ getInstance(systemId, instanceId) {
155
+ return _.find(this._instances, {systemId, instanceId});
156
+ }
157
+
158
+ /**
159
+ *
160
+ * @param {string} systemId
161
+ * @param {string} instanceId
162
+ * @param {{health:string,pid:string,type:'docker'|'local',portType?:string}} info
163
+ * @return {Promise<void>}
164
+ */
165
+ async registerInstance(systemId, instanceId, info) {
166
+ let instance = this.getInstance(systemId, instanceId);
167
+
168
+ //Get target address
169
+ let address = await serviceManager.getProviderAddress(
170
+ systemId,
171
+ instanceId,
172
+ info.portType ?? DEFAULT_HEALTH_PORT_TYPE
173
+ );
174
+
175
+ let healthUrl = null;
176
+ let health = info.health;
177
+ if (health) {
178
+ if (health.startsWith('/')) {
179
+ health = health.substring(1);
180
+ }
181
+ healthUrl = address + health;
182
+ }
183
+
184
+ if (instance) {
185
+ instance.status = STATUS_STARTING;
186
+ instance.pid = info.pid;
187
+ instance.type = info.type;
188
+ instance.health = healthUrl;
189
+ this._emit(systemId, EVENT_STATUS_CHANGED, instance);
190
+ } else {
191
+ instance = {
192
+ systemId,
193
+ instanceId,
194
+ status: STATUS_STARTING,
195
+ pid: info.pid,
196
+ type: info.type,
197
+ health: healthUrl
198
+ };
199
+
200
+ this._instances.push(instance);
201
+
202
+ this._emit(systemId, EVENT_INSTANCE_CREATED, instance);
203
+ }
204
+
205
+ this._save();
206
+ }
207
+
208
+ setInstanceAsStopped(systemId, instanceId) {
209
+ const instance = _.find(this._instances, {systemId, instanceId});
210
+ if (instance) {
211
+ instance.status = STATUS_STOPPED;
212
+ instance.pid = null;
213
+ instance.health = null;
214
+ this._emit(systemId, EVENT_STATUS_CHANGED, instance);
215
+ this._save();
216
+ }
217
+ }
218
+
219
+ _emit(systemId, type, payload) {
220
+ socketManager.emit(`${systemId}/instances`, type, payload);
221
+ }
222
+
223
+ /**
224
+ *
225
+ * @param planRef
226
+ * @return {Promise<ProcessInfo[]>}
227
+ */
228
+ async createProcessesForPlan(planRef) {
229
+ await this.stopAllForPlan(planRef);
230
+
231
+ const plan = assetManager.getPlan(planRef);
232
+ if (!plan) {
233
+ throw new Error('Plan not found: ' + planRef);
234
+ }
235
+
236
+ if (!plan.spec.blocks) {
237
+ console.warn('No blocks found in plan', planRef);
238
+ return [];
239
+ }
240
+
241
+ let processes = [];
242
+ let errors = [];
243
+ for(let blockInstance of Object.values(plan.spec.blocks)) {
244
+ try {
245
+ processes.push(await this.createProcess(planRef, blockInstance.id));
246
+ } catch (e) {
247
+ errors.push(e);
248
+ }
249
+ }
250
+
251
+ if (errors.length > 0) {
252
+ throw errors[0];
253
+ }
254
+
255
+ return processes;
256
+ }
257
+
258
+ async _stopInstance(instance) {
259
+ if (!instance.pid) {
260
+ return;
261
+ }
262
+
263
+ if (instance.status === 'stopped') {
264
+ return;
265
+ }
266
+
267
+ try {
268
+ if (instance.type === 'docker') {
269
+ const container = await containerManager.get(instance.pid);
270
+ await container.stop();
271
+ return;
272
+ }
273
+ process.kill(instance.pid, 'SIGTERM');
274
+ } catch (e) {
275
+ console.error('Failed to stop process', e);
276
+ }
277
+ }
278
+
279
+ async stopAllForPlan(planRef) {
280
+ if (this._processes[planRef]) {
281
+ for(let instance of Object.values(this._processes[planRef])) {
282
+ await instance.stop();
283
+ }
284
+
285
+ this._processes[planRef] = {};
286
+ }
287
+
288
+ //Also stop instances not being maintained by the cluster service
289
+ const instancesForPlan = this._instances
290
+ .filter(instance => instance.systemId === planRef);
291
+
292
+ for(let instance of instancesForPlan) {
293
+ await this._stopInstance(instance);
294
+ }
295
+ }
296
+
297
+ /**
298
+ *
299
+ * @param planRef
300
+ * @param instanceId
301
+ * @return {Promise<PromiseInfo>}
302
+ */
303
+ async createProcess(planRef, instanceId) {
304
+ const plan = assetManager.getPlan(planRef);
305
+ if (!plan) {
306
+ throw new Error('Plan not found: ' + planRef);
307
+ }
308
+
309
+ const blockInstance = plan.spec && plan.spec.blocks ? _.find(plan.spec.blocks, {id: instanceId}) : null;
310
+ if (!blockInstance) {
311
+ throw new Error('Block instance not found: ' + instanceId);
312
+ }
313
+
314
+ const blockRef = blockInstance.block.ref;
315
+
316
+ const blockAsset = assetManager.getAsset(blockRef);
317
+
318
+ if (!blockAsset) {
319
+ throw new Error('Block not found: ' + blockRef);
320
+ }
321
+
322
+ if (!this._processes[planRef]) {
323
+ this._processes[planRef] = {};
324
+ }
325
+
326
+ await this.stopProcess(planRef, instanceId);
327
+
328
+ const runner = new BlockInstanceRunner(planRef);
329
+ const startTime = Date.now();
330
+ const process = await runner.start(blockRef, instanceId);
331
+ //emit stdout/stderr via sockets
332
+ process.output.on("data", (data) => {
333
+ const payload = {source: "stdout", level: "INFO", message: data.toString(), time: Date.now()};
334
+ this._emit(instanceId, EVENT_INSTANCE_LOG, payload);
335
+ });
336
+
337
+ process.output.on('exit', (exitCode) => {
338
+ const timeRunning = Date.now() - startTime;
339
+ const instance = this.getInstance(planRef, instanceId);
340
+ if (instance.status === STATUS_READY) {
341
+ //It's already been running
342
+ return;
343
+ }
344
+
345
+ if (exitCode === 143 ||
346
+ exitCode === 137) {
347
+ //Process got SIGTERM (143) or SIGKILL (137)
348
+ //TODO: Windows?
349
+ return;
350
+ }
351
+
352
+ if (exitCode !== 0 || timeRunning < MIN_TIME_RUNNING) {
353
+ this._emit(blockInstance.id, EVENT_INSTANCE_EXITED, {
354
+ error: "Failed to start instance",
355
+ status: EVENT_INSTANCE_EXITED,
356
+ instanceId: blockInstance.id
357
+ })
358
+ }
359
+ });
360
+
361
+ await this.registerInstance(planRef, instanceId, {
362
+ type: process.type,
363
+ pid: process.pid,
364
+ health: null
365
+ });
366
+
367
+ return this._processes[planRef][instanceId] = process;
368
+ }
369
+
370
+ /**
371
+ *
372
+ * @param {string} planRef
373
+ * @param {string} instanceId
374
+ * @return {ProcessInfo|null}
375
+ */
376
+ getProcessForInstance(planRef, instanceId) {
377
+ if (!this._processes[planRef]) {
378
+ return null;
379
+ }
380
+
381
+ return this._processes[planRef][instanceId];
382
+ }
383
+
384
+ async stopProcess(planRef, instanceId) {
385
+ if (!this._processes[planRef]) {
386
+ return;
387
+ }
388
+
389
+ if (this._processes[planRef][instanceId]) {
390
+ await this._processes[planRef][instanceId].stop();
391
+ delete this._processes[planRef][instanceId];
392
+ }
393
+ }
394
+
395
+ async stopAllProcesses() {
396
+ for(let processesForPlan of Object.values(this._processes)) {
397
+ for(let processInfo of Object.values(processesForPlan)) {
398
+ await processInfo.stop();
399
+ }
400
+ }
401
+ this._processes = {};
402
+
403
+ for(let instance of this._instances) {
404
+ await this._stopInstance(instance);
405
+ }
406
+ }
407
+ }
408
+
409
+
410
+ const instanceManager = new InstanceManager();
411
+
412
+ process.on('exit', async () => {
413
+ await instanceManager.stopAllProcesses();
414
+ });
415
+
416
+ module.exports = instanceManager;
@@ -0,0 +1,117 @@
1
+ const Router = require('express-promise-router').default;
2
+
3
+ const instanceManager = require('../instanceManager');
4
+
5
+
6
+ const router = new Router();
7
+ router.use('/', require('../middleware/cors'));
8
+
9
+ /**
10
+ * Get all instances
11
+ */
12
+ router.get('/', (req, res) => {
13
+ res.send(instanceManager.getInstances());
14
+ });
15
+
16
+ /**
17
+ * Get all instances
18
+ */
19
+ router.get('/:systemId/instances', (req, res) => {
20
+ res.send(instanceManager.getInstancesForPlan(req.params.systemId));
21
+ });
22
+
23
+ /**
24
+ * Start all instances in a plan
25
+ */
26
+ router.post('/:systemId/start', async (req, res) => {
27
+ const processes = await instanceManager.createProcessesForPlan(req.params.systemId);
28
+
29
+ res.status(202).send({
30
+ ok:true,
31
+ processes: processes.map(p => {
32
+ return {pid:p.pid, type:p.type};
33
+ })
34
+ });
35
+ });
36
+
37
+ /**
38
+ * Stop all instances in plan
39
+ */
40
+ router.post('/:systemId/stop', async (req, res) => {
41
+ await instanceManager.stopAllForPlan(req.params.systemId);
42
+
43
+ res.status(202).send({
44
+ ok:true
45
+ });
46
+ });
47
+
48
+ /**
49
+ * Start single instance in a plan
50
+ */
51
+ router.post('/:systemId/:instanceId/start', async (req, res) => {
52
+ const process = await instanceManager.createProcess(req.params.systemId, req.params.instanceId);
53
+
54
+ res.status(202).send({
55
+ ok:true,
56
+ pid: process.pid,
57
+ type: process.type
58
+ });
59
+ });
60
+
61
+ /**
62
+ * Stop single instance in a plan
63
+ */
64
+ router.post('/:systemId/:instanceId/stop', async (req, res) => {
65
+ await instanceManager.stopProcess(req.params.systemId, req.params.instanceId);
66
+
67
+ res.status(202).send({ok:true});
68
+ });
69
+
70
+
71
+ /**
72
+ * Get logs for instance in a plan
73
+ */
74
+ router.get('/:systemId/:instanceId/logs', (req, res) => {
75
+ const processInfo = instanceManager.getProcessForInstance(req.params.systemId, req.params.instanceId);
76
+ if (!processInfo) {
77
+ res.status(404).send({ok:false});
78
+ return;
79
+ }
80
+
81
+ res.status(202).send({
82
+ logs: processInfo.logs()
83
+ });
84
+ });
85
+
86
+ router.use('/', require('../middleware/stringBody'));
87
+
88
+
89
+ router.use('/', require('../middleware/kapeta'));
90
+
91
+ /**
92
+ * Updates the full configuration for a given service.
93
+ */
94
+ router.put('/', async (req, res) => {
95
+
96
+ let instance = JSON.parse(req.stringBody);
97
+
98
+ await instanceManager.registerInstance(
99
+ req.kapeta.systemId,
100
+ req.kapeta.instanceId,
101
+ instance
102
+ );
103
+
104
+ res.status(202).send({ok:true});
105
+ });
106
+
107
+ /**
108
+ * Delete instance
109
+ */
110
+ router.delete('/', async (req, res) => {
111
+ await instanceManager.setInstanceAsStopped(req.kapeta.systemId, req.kapeta.instanceId);
112
+
113
+ res.status(202).send({ok:true});
114
+ });
115
+
116
+
117
+ module.exports = router;
@@ -0,0 +1,7 @@
1
+ module.exports = function cors(req, res, next) {
2
+ res.set('Access-Control-Allow-Origin', req.headers.origin);
3
+ res.set('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, HEAD, PATCH');
4
+ res.set('Access-Control-Allow-Headers', '*');
5
+
6
+ next();
7
+ };
@@ -0,0 +1,20 @@
1
+
2
+ module.exports = function kapeta(req, res, next) {
3
+
4
+ let blockRef = req.headers['x-kapeta-block'];
5
+ let systemId = req.headers['x-kapeta-system'];
6
+ let instanceId = req.headers['x-kapeta-instance'];
7
+
8
+ if (!blockRef) {
9
+ res.status(400).send({error: 'Missing X-Kapeta-Block header.'});
10
+ return;
11
+ }
12
+
13
+ req.kapeta = {
14
+ blockRef,
15
+ instanceId,
16
+ systemId
17
+ };
18
+
19
+ next();
20
+ };
@@ -0,0 +1,11 @@
1
+ module.exports = function stringBody (req, res, next) {
2
+
3
+ // push the data to body
4
+ const body = [];
5
+ req.on('data', (chunk) => {
6
+ body.push(chunk);
7
+ }).on('end', () => {
8
+ req.stringBody = Buffer.concat(body).toString();
9
+ next();
10
+ });
11
+ };
@@ -0,0 +1,120 @@
1
+ const uuid = require('node-uuid');
2
+ class NetworkManager {
3
+
4
+ static toConnectionId(connection) {
5
+ return [
6
+ connection.from.blockId,
7
+ connection.from.resourceName,
8
+ connection.to.blockId,
9
+ connection.to.resourceName
10
+ ].join('_');
11
+ }
12
+
13
+ constructor() {
14
+ this._connections = {};
15
+ this._sources = {};
16
+ this._targets = {};
17
+ }
18
+
19
+ _ensureSystem(systemId) {
20
+ if (!this._connections[systemId]) {
21
+ this._connections[systemId] = {};
22
+ }
23
+
24
+ if (!this._sources[systemId]) {
25
+ this._sources[systemId] = {};
26
+ }
27
+
28
+ if (!this._targets[systemId]) {
29
+ this._targets[systemId] = {};
30
+ }
31
+ }
32
+
33
+ _ensureConnection(systemId, connectionId) {
34
+ this._ensureSystem(systemId);
35
+
36
+ if (!this._connections[systemId][connectionId]) {
37
+ this._connections[systemId][connectionId] = [];
38
+ }
39
+
40
+ return this._connections[systemId][connectionId];
41
+ }
42
+
43
+ _ensureSource(systemId, sourceBlockInstanceId) {
44
+ this._ensureSystem(systemId);
45
+
46
+ if (!this._sources[systemId][sourceBlockInstanceId]) {
47
+ this._sources[systemId][sourceBlockInstanceId] = [];
48
+ }
49
+
50
+ return this._sources[systemId][sourceBlockInstanceId];
51
+ }
52
+
53
+ _ensureTarget(systemId, targetBlockInstanceId) {
54
+ this._ensureSystem(systemId);
55
+
56
+ if (!this._targets[systemId][targetBlockInstanceId]) {
57
+ this._targets[systemId][targetBlockInstanceId] = [];
58
+ }
59
+
60
+ return this._targets[systemId][targetBlockInstanceId];
61
+ }
62
+
63
+ addRequest(systemId, connection, request, consumerMethodId, providerMethodId) {
64
+
65
+ const traffic = new Traffic(connection, request, consumerMethodId, providerMethodId);
66
+
67
+ this._ensureConnection(systemId, traffic.connectionId).push(traffic);
68
+ this._ensureSource(systemId, connection.from.blockId).push(traffic);
69
+ this._ensureTarget(systemId, connection.to.blockId).push(traffic);
70
+
71
+ return traffic;
72
+ }
73
+
74
+ getTrafficForConnection(systemId, connectionId) {
75
+ return this._ensureConnection(systemId, connectionId);
76
+ }
77
+
78
+ getTrafficForSource(systemId, blockInstanceId) {
79
+
80
+ return this._ensureSource(systemId, blockInstanceId);
81
+ }
82
+
83
+ getTrafficForTarget(systemId, blockInstanceId) {
84
+ return this._ensureTarget(systemId, blockInstanceId);
85
+ }
86
+ }
87
+
88
+
89
+ class Traffic {
90
+
91
+ constructor(connection, request, consumerMethodId, providerMethodId) {
92
+ this.id = uuid.v4();
93
+ this.connectionId = NetworkManager.toConnectionId(connection);
94
+ this.consumerMethodId = consumerMethodId;
95
+ this.providerMethodId = providerMethodId;
96
+ this.request = request;
97
+ this.response = null;
98
+ this.error = null;
99
+ this.ended = null;
100
+ this.created = new Date().getTime();
101
+ }
102
+
103
+ asError(err) {
104
+ this.ended = new Date().getTime();
105
+ this.response = {
106
+ code: 0,
107
+ headers: {},
108
+ body: null
109
+ };
110
+ this.error = err + '';
111
+ }
112
+
113
+ withResponse(response) {
114
+ this.ended = new Date().getTime();
115
+ this.response = response;
116
+ }
117
+
118
+ }
119
+
120
+ module.exports = new NetworkManager();