@kapeta/local-cluster-service 0.0.76 → 0.1.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.
@@ -1,8 +1,8 @@
1
- const { Docker } = require("node-docker-api");
2
- const _ = require("lodash");
3
- const os = require("os");
1
+ const {Docker} = require("node-docker-api");
4
2
  const path = require("path");
5
-
3
+ const _ = require('lodash');
4
+ const FS = require("node:fs");
5
+ const os = require("os");
6
6
  const LABEL_PORT_PREFIX = "kapeta_port-";
7
7
 
8
8
  const NANO_SECOND = 1000000;
@@ -10,301 +10,346 @@ const HEALTH_CHECK_INTERVAL = 1000;
10
10
  const HEALTH_CHECK_MAX = 20;
11
11
 
12
12
  const promisifyStream = (stream) =>
13
- new Promise((resolve, reject) => {
14
- stream.on("data", (d) => console.log(d.toString()));
15
- stream.on("end", resolve);
16
- stream.on("error", reject);
17
- });
13
+ new Promise((resolve, reject) => {
14
+ stream.on("data", (d) => console.log(d.toString()));
15
+ stream.on("end", resolve);
16
+ stream.on("error", reject);
17
+ });
18
18
 
19
19
  class ContainerManager {
20
- constructor() {
21
- this._docker = new Docker();
22
- this._alive = false;
23
- }
24
-
25
- async initialize() {
26
- // try
27
- const connectOptions = [
28
- // use defaults: DOCKER_HOST etc from env, if available
29
- undefined,
30
- // default linux
31
- { socketPath: "/var/run/docker.sock" },
32
- // default macOS
33
- { socketPath: path.join(os.homedir(), ".docker/run/docker.sock") },
34
- // Default http
35
- { protocol: "http", host: "localhost", port: 2375 },
36
- { protocol: "https", host: "localhost", port: 2376 },
37
- ];
38
- for (const opts of connectOptions) {
39
- try {
40
- const client = new Docker(opts);
41
- await client.ping();
42
- this._docker = client;
43
- return;
44
- } catch (err) {
45
- // silently ignore bad configs
46
- }
20
+ constructor() {
21
+ this._docker = null;
22
+ this._alive = false;
47
23
  }
48
- throw new Error("Unable to connect to docker");
49
- }
50
-
51
- async ping() {
52
- await this._docker.ping();
53
- this._alive = true;
54
- }
55
-
56
- async pull(image) {
57
- let [imageName, tag] = image.split(/:/);
58
- if (!tag) {
59
- tag = "latest";
24
+
25
+ isAlive() {
26
+ return this._alive;
60
27
  }
61
28
 
62
- await this._docker.image
63
- .create(
64
- {},
65
- {
66
- fromImage: imageName,
67
- tag: tag,
29
+ async initialize() {
30
+ // try
31
+ const connectOptions = [
32
+ // use defaults: DOCKER_HOST etc from env, if available
33
+ undefined,
34
+ // default linux
35
+ {socketPath: "/var/run/docker.sock"},
36
+ // default macOS
37
+ {socketPath: path.join(os.homedir(), ".docker/run/docker.sock")},
38
+ // Default http
39
+ {protocol: "http", host: "localhost", port: 2375},
40
+ {protocol: "https", host: "localhost", port: 2376},
41
+ ];
42
+ for (const opts of connectOptions) {
43
+ try {
44
+ const client = new Docker(opts);
45
+ await client.ping();
46
+ this._docker = client;
47
+ return;
48
+ } catch (err) {
49
+ // silently ignore bad configs
50
+ }
68
51
  }
69
- )
70
- .then((stream) => promisifyStream(stream));
71
- }
72
-
73
- /**
74
- *
75
- * @param {string} image
76
- * @param {string} name
77
- * @param {{ports:{},mounts:{},env:{}}} opts
78
- * @return {Promise<ContainerInfo>}
79
- */
80
- async run(image, name, opts) {
81
- const Mounts = [];
82
- const PortBindings = {};
83
- const Env = [];
84
- const Labels = {
85
- kapeta: "true",
86
- };
87
-
88
- console.log("Pulling image: %s", image);
89
-
90
- await this.pull(image);
91
-
92
- console.log("Image pulled: %s", image);
93
-
94
- _.forEach(opts.ports, (portInfo, containerPort) => {
95
- PortBindings["" + containerPort] = [
96
- {
97
- HostPort: "" + portInfo.hostPort,
98
- },
99
- ];
100
-
101
- Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
102
- });
52
+ throw new Error("Unable to connect to docker");
53
+ }
103
54
 
104
- _.forEach(opts.mounts, (Source, Target) => {
105
- Mounts.push({
106
- Target,
107
- Source,
108
- Type: "bind",
109
- ReadOnly: false,
110
- Consistency: "consistent",
111
- });
112
- });
55
+ async ping() {
56
+ await this._docker.ping();
57
+ this._alive = true;
58
+ }
113
59
 
114
- _.forEach(opts.env, (value, name) => {
115
- Env.push(name + "=" + value);
116
- });
60
+ async ping() {
61
+
62
+ try {
63
+ const pingResult = await this._docker.ping();
64
+ if (pingResult !== 'OK') {
65
+ throw new Error(`Ping failed: ${pingResult}`);
66
+ }
67
+ } catch (e) {
68
+ throw new Error(`Docker not running. Please start the docker daemon before running this command. Error: ${e.message}`);
69
+ }
117
70
 
118
- let HealthCheck = undefined;
119
-
120
- if (opts.health) {
121
- HealthCheck = {
122
- Test: ["CMD-SHELL", opts.health.cmd],
123
- Interval: opts.health.interval
124
- ? opts.health.interval * NANO_SECOND
125
- : 5000 * NANO_SECOND,
126
- Timeout: opts.health.timeout
127
- ? opts.health.timeout * NANO_SECOND
128
- : 15000 * NANO_SECOND,
129
- Retries: opts.health.retries || 10,
130
- };
131
-
132
- console.log("Adding health check", HealthCheck);
71
+ this._alive = true;
133
72
  }
134
73
 
135
- const dockerContainer = await this._docker.container.create({
136
- name: name,
137
- Image: image,
138
- Labels,
139
- Env,
140
- HealthCheck,
141
- HostConfig: {
142
- PortBindings,
143
- Mounts,
144
- },
145
- });
74
+ async ensureAlive() {
75
+ if (!this._alive) {
76
+ await this.ping();
77
+ }
78
+ }
146
79
 
147
- await dockerContainer.start();
80
+ async docker() {
81
+ await this.ensureAlive();
82
+ return this._docker;
83
+ }
148
84
 
149
- if (opts.health) {
150
- await this._waitForHealthy(dockerContainer);
85
+ async getContainerByName(containerName) {
86
+ const containers = await this._docker.container.list({all: true});
87
+ return containers.find(container => {
88
+ return container.data.Names.indexOf(`/${containerName}`) > -1;
89
+ });
151
90
  }
152
91
 
153
- return new ContainerInfo(dockerContainer);
154
- }
92
+ async pull(image) {
93
+ let [imageName, tag] = image.split(/:/);
94
+ if (!tag) {
95
+ tag = 'latest';
96
+ }
155
97
 
156
- async _waitForHealthy(container, attempt) {
157
- if (!attempt) {
158
- attempt = 0;
98
+ await this._docker.image
99
+ .create(
100
+ {},
101
+ {
102
+ fromImage: imageName,
103
+ tag: tag,
104
+ }
105
+ )
106
+ .then((stream) => promisifyStream(stream));
159
107
  }
160
108
 
161
- if (attempt >= HEALTH_CHECK_MAX) {
162
- throw new Error("Operator did not become healthy within the timeout");
109
+ /**
110
+ *
111
+ * @param {string} image
112
+ * @param {string} name
113
+ * @param {{ports:{},mounts:{},env:{}}} opts
114
+ * @return {Promise<ContainerInfo>}
115
+ */
116
+ async run(image, name, opts) {
117
+ const Mounts = [];
118
+ const PortBindings = {};
119
+ const Env = [];
120
+ const Labels = {
121
+ kapeta: "true",
122
+ };
123
+
124
+ console.log("Pulling image: %s", image);
125
+
126
+ await this.pull(image);
127
+
128
+ console.log("Image pulled: %s", image);
129
+
130
+ _.forEach(opts.ports, (portInfo, containerPort) => {
131
+ PortBindings['' + containerPort] = [
132
+ {
133
+ HostPort: '' + portInfo.hostPort,
134
+ HostIp: '127.0.0.1'
135
+ }
136
+ ];
137
+
138
+ Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
139
+ });
140
+
141
+ _.forEach(opts.mounts, (Source, Target) => {
142
+ Mounts.push({
143
+ Target,
144
+ Source,
145
+ Type: "bind",
146
+ ReadOnly: false,
147
+ Consistency: "consistent",
148
+ });
149
+ });
150
+
151
+ _.forEach(opts.env, (value, name) => {
152
+ Env.push(name + "=" + value);
153
+ });
154
+
155
+ let HealthCheck = undefined;
156
+
157
+ if (opts.health) {
158
+ HealthCheck = {
159
+ Test: ["CMD-SHELL", opts.health.cmd],
160
+ Interval: opts.health.interval
161
+ ? opts.health.interval * NANO_SECOND
162
+ : 5000 * NANO_SECOND,
163
+ Timeout: opts.health.timeout
164
+ ? opts.health.timeout * NANO_SECOND
165
+ : 15000 * NANO_SECOND,
166
+ Retries: opts.health.retries || 10,
167
+ };
168
+
169
+ console.log("Adding health check", HealthCheck);
170
+ }
171
+
172
+
173
+ const dockerContainer = await this.startContainer({
174
+ name: name,
175
+ Image: image,
176
+ Labels,
177
+ Env,
178
+ HealthCheck,
179
+ HostConfig: {
180
+ PortBindings,
181
+ Mounts
182
+ }
183
+ });
184
+
185
+ if (opts.health) {
186
+ await this._waitForHealthy(dockerContainer);
187
+ }
188
+
189
+ return new ContainerInfo(dockerContainer);
163
190
  }
164
191
 
165
- if (await this._isHealthy(container)) {
166
- console.log("Container became healthy");
167
- return;
192
+ async startContainer(opts) {
193
+ const dockerContainer = await this._docker.container.create(opts);
194
+
195
+ await dockerContainer.start();
196
+
197
+ return dockerContainer;
168
198
  }
169
199
 
170
- return new Promise((resolve) => {
171
- setTimeout(async () => {
172
- await this._waitForHealthy(container, attempt + 1);
173
- resolve();
174
- }, HEALTH_CHECK_INTERVAL);
175
- });
176
- }
177
-
178
- async _isHealthy(container) {
179
- const info = await container.status();
180
-
181
- return info?.data?.State?.Health?.Status === "healthy";
182
- }
183
-
184
- /**
185
- *
186
- * @param name
187
- * @return {Promise<ContainerInfo>}
188
- */
189
- async get(name) {
190
- let dockerContainer = null;
191
-
192
- try {
193
- dockerContainer = await this._docker.container.get(name);
194
- await dockerContainer.status();
195
- } catch (err) {
196
- //Ignore
197
- console.log("Container not available - creating it: %s", name);
198
- dockerContainer = null;
200
+
201
+ async _waitForHealthy(container, attempt) {
202
+ if (!attempt) {
203
+ attempt = 0;
204
+ }
205
+
206
+ if (attempt >= HEALTH_CHECK_MAX) {
207
+ throw new Error("Operator did not become healthy within the timeout");
208
+ }
209
+
210
+ if (await this._isHealthy(container)) {
211
+ console.log("Container became healthy");
212
+ return;
213
+ }
214
+
215
+ return new Promise((resolve) => {
216
+ setTimeout(async () => {
217
+ await this._waitForHealthy(container, attempt + 1);
218
+ resolve();
219
+ }, HEALTH_CHECK_INTERVAL);
220
+ });
199
221
  }
200
222
 
201
- if (!dockerContainer) {
202
- return null;
223
+ async _isHealthy(container) {
224
+ const info = await container.status();
225
+
226
+ return info?.data?.State?.Health?.Status === "healthy";
203
227
  }
204
228
 
205
- return new ContainerInfo(dockerContainer);
206
- }
229
+ /**
230
+ *
231
+ * @param name
232
+ * @return {Promise<ContainerInfo>}
233
+ */
234
+ async get(name) {
235
+ let dockerContainer = null;
236
+
237
+ try {
238
+ dockerContainer = await this._docker.container.get(name);
239
+ await dockerContainer.status();
240
+ } catch (err) {
241
+ //Ignore
242
+ console.log("Container not available - creating it: %s", name);
243
+ dockerContainer = null;
244
+ }
245
+
246
+ if (!dockerContainer) {
247
+ return null;
248
+ }
249
+
250
+ return new ContainerInfo(dockerContainer);
251
+ }
207
252
  }
208
253
 
209
254
  class ContainerInfo {
210
- /**
211
- *
212
- * @param {Container} dockerContainer
213
- */
214
- constructor(dockerContainer) {
215
255
  /**
216
256
  *
217
- * @type {Container}
218
- * @private
257
+ * @param {Container} dockerContainer
219
258
  */
220
- this._container = dockerContainer;
221
- }
222
-
223
- async isRunning() {
224
- const inspectResult = await this.getStatus();
225
-
226
- if (!inspectResult || !inspectResult.State) {
227
- return false;
259
+ constructor(dockerContainer) {
260
+ /**
261
+ *
262
+ * @type {Container}
263
+ * @private
264
+ */
265
+ this._container = dockerContainer;
228
266
  }
229
267
 
230
- return inspectResult.State.Running || inspectResult.State.Restarting;
231
- }
268
+ async isRunning() {
269
+ const inspectResult = await this.getStatus();
232
270
 
233
- async start() {
234
- await this._container.start();
235
- }
271
+ if (!inspectResult || !inspectResult.State) {
272
+ return false;
273
+ }
236
274
 
237
- async restart() {
238
- await this._container.restart();
239
- }
275
+ return inspectResult.State.Running || inspectResult.State.Restarting;
276
+ }
240
277
 
241
- async stop() {
242
- await this._container.stop();
243
- }
278
+ async start() {
279
+ await this._container.start();
280
+ }
244
281
 
245
- async remove(opts) {
246
- await this._container.delete({ force: !!opts.force });
247
- }
282
+ async restart() {
283
+ await this._container.restart();
284
+ }
248
285
 
249
- async getPort(type) {
250
- const ports = await this.getPorts();
286
+ async stop() {
287
+ await this._container.stop();
288
+ }
251
289
 
252
- if (ports[type]) {
253
- return ports[type];
290
+ async remove(opts) {
291
+ await this._container.delete({force: !!opts.force});
254
292
  }
255
293
 
256
- return null;
257
- }
294
+ async getPort(type) {
295
+ const ports = await this.getPorts();
258
296
 
259
- async getStatus() {
260
- const result = await this._container.status();
297
+ if (ports[type]) {
298
+ return ports[type];
299
+ }
261
300
 
262
- return result ? result.data : null;
263
- }
301
+ return null;
302
+ }
264
303
 
265
- async getPorts() {
266
- const inspectResult = await this.getStatus();
304
+ async getStatus() {
305
+ const result = await this._container.status();
267
306
 
268
- if (
269
- !inspectResult ||
270
- !inspectResult.Config ||
271
- !inspectResult.Config.Labels
272
- ) {
273
- return false;
307
+ return result ? result.data : null;
274
308
  }
275
309
 
276
- const portTypes = {};
277
- const ports = {};
310
+ async getPorts() {
311
+ const inspectResult = await this.getStatus();
278
312
 
279
- _.forEach(inspectResult.Config.Labels, (portType, name) => {
280
- if (!name.startsWith(LABEL_PORT_PREFIX)) {
281
- return;
282
- }
313
+ if (
314
+ !inspectResult ||
315
+ !inspectResult.Config ||
316
+ !inspectResult.Config.Labels
317
+ ) {
318
+ return false;
319
+ }
283
320
 
284
- const hostPort = name.substr(LABEL_PORT_PREFIX.length);
321
+ const portTypes = {};
322
+ const ports = {};
285
323
 
286
- portTypes[hostPort] = portType;
287
- });
324
+ _.forEach(inspectResult.Config.Labels, (portType, name) => {
325
+ if (!name.startsWith(LABEL_PORT_PREFIX)) {
326
+ return;
327
+ }
288
328
 
289
- _.forEach(
290
- inspectResult.HostConfig.PortBindings,
291
- (portBindings, containerPortSpec) => {
292
- let [containerPort, protocol] = containerPortSpec.split(/\//);
329
+ const hostPort = name.substr(LABEL_PORT_PREFIX.length);
293
330
 
294
- const hostPort = portBindings[0].HostPort;
331
+ portTypes[hostPort] = portType;
332
+ });
295
333
 
296
- const portType = portTypes[hostPort];
334
+ _.forEach(
335
+ inspectResult.HostConfig.PortBindings,
336
+ (portBindings, containerPortSpec) => {
337
+ let [containerPort, protocol] = containerPortSpec.split(/\//);
297
338
 
298
- ports[portType] = {
299
- containerPort,
300
- protocol,
301
- hostPort,
302
- };
303
- }
304
- );
339
+ const hostPort = portBindings[0].HostPort;
305
340
 
306
- return ports;
307
- }
341
+ const portType = portTypes[hostPort];
342
+
343
+ ports[portType] = {
344
+ containerPort,
345
+ protocol,
346
+ hostPort,
347
+ };
348
+ }
349
+ );
350
+
351
+ return ports;
352
+ }
308
353
  }
309
354
 
310
355
  module.exports = new ContainerManager();
@@ -1,7 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const request = require('request');
3
-
4
- const {BlockInstanceRunner} = require('@kapeta/local-cluster-executor');
3
+ const EventEmitter = require("events");
4
+ const BlockInstanceRunner = require('./utils/BlockInstanceRunner');
5
5
 
6
6
  const storageService = require('./storageService');
7
7
  const socketManager = require('./socketManager');
@@ -68,6 +68,10 @@ class InstanceManager {
68
68
 
69
69
  if (instance.status !== newStatus) {
70
70
  instance.status = newStatus;
71
+ console.log(
72
+ 'Instance status changed: %s %s -> %s',
73
+ instance.systemId, instance.instanceId, instance.status
74
+ )
71
75
  this._emit(instance.systemId, EVENT_STATUS_CHANGED, instance);
72
76
  changed = true;
73
77
  }
@@ -85,7 +89,11 @@ class InstanceManager {
85
89
 
86
90
  if (instance.type === 'docker') {
87
91
  const container = await containerManager.get(instance.pid);
88
- return await container.isRunning()
92
+ if (!container) {
93
+ console.warn('Container not found: %s', instance.pid);
94
+ return false;
95
+ }
96
+ return await container.isRunning();
89
97
  }
90
98
 
91
99
  //Otherwise its just a normal process.
@@ -185,8 +193,12 @@ class InstanceManager {
185
193
  if (instance) {
186
194
  instance.status = STATUS_STARTING;
187
195
  instance.pid = info.pid;
188
- instance.type = info.type;
189
- instance.health = healthUrl;
196
+ if (info.type) {
197
+ instance.type = info.type;
198
+ }
199
+ if (healthUrl) {
200
+ instance.health = healthUrl;
201
+ }
190
202
  this._emit(systemId, EVENT_STATUS_CHANGED, instance);
191
203
  } else {
192
204
  instance = {
@@ -268,7 +280,9 @@ class InstanceManager {
268
280
  try {
269
281
  if (instance.type === 'docker') {
270
282
  const container = await containerManager.get(instance.pid);
271
- await container.stop();
283
+ if (container) {
284
+ await container.stop();
285
+ }
272
286
  return;
273
287
  }
274
288
  process.kill(instance.pid, 'SIGTERM');
@@ -383,7 +397,8 @@ class InstanceManager {
383
397
  message: e.message,
384
398
  time: Date.now()
385
399
  }
386
- ]
400
+ ];
401
+
387
402
  await this.registerInstance(planRef, instanceId, {
388
403
  type: 'local',
389
404
  pid: null,
@@ -401,7 +416,12 @@ class InstanceManager {
401
416
  return this._processes[planRef][instanceId] = {
402
417
  pid: -1,
403
418
  type,
404
- logs: () => logs
419
+ logs: () => logs,
420
+ stop: () => Promise.resolve(),
421
+ ref: blockRef,
422
+ id: instanceId,
423
+ name: blockInstance.name,
424
+ output: new EventEmitter()
405
425
  };
406
426
  }
407
427
 
@@ -427,7 +447,11 @@ class InstanceManager {
427
447
  }
428
448
 
429
449
  if (this._processes[planRef][instanceId]) {
430
- await this._processes[planRef][instanceId].stop();
450
+ try {
451
+ await this._processes[planRef][instanceId].stop();
452
+ } catch (e) {
453
+ console.error('Failed to stop process for instance: %s -> %s', planRef, instanceId, e);
454
+ }
431
455
  delete this._processes[planRef][instanceId];
432
456
  }
433
457
  }