@kapeta/local-cluster-service 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [0.2.0](https://github.com/kapetacom/local-cluster-service/compare/v0.1.2...v0.2.0) (2023-05-07)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add support for health checks and mounts ([cac607b](https://github.com/kapetacom/local-cluster-service/commit/cac607bc8b592e27c8b6c2ff09476f90b2e4c3f3))
7
+
8
+ ## [0.1.2](https://github.com/kapetacom/local-cluster-service/compare/v0.1.1...v0.1.2) (2023-05-06)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Moved all docker init things into init - and rely on that ([9a012c3](https://github.com/kapetacom/local-cluster-service/commit/9a012c3a40a6b4e4ef55757a4b8454d48bd3987c))
14
+
1
15
  ## [0.1.1](https://github.com/kapetacom/local-cluster-service/compare/v0.1.0...v0.1.1) (2023-05-06)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -3,10 +3,14 @@ const path = require("path");
3
3
  const _ = require('lodash');
4
4
  const FS = require("node:fs");
5
5
  const os = require("os");
6
+ const Path = require("path");
7
+ const storageService = require("./storageService");
8
+ const mkdirp = require("mkdirp");
9
+ const {parseKapetaUri} = require("@kapeta/nodejs-utils");
6
10
  const LABEL_PORT_PREFIX = "kapeta_port-";
7
11
 
8
12
  const NANO_SECOND = 1000000;
9
- const HEALTH_CHECK_INTERVAL = 1000;
13
+ const HEALTH_CHECK_INTERVAL = 2000;
10
14
  const HEALTH_CHECK_MAX = 20;
11
15
 
12
16
  const promisifyStream = (stream) =>
@@ -20,12 +24,32 @@ class ContainerManager {
20
24
  constructor() {
21
25
  this._docker = null;
22
26
  this._alive = false;
27
+ this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
28
+ mkdirp.sync(this._mountDir);
23
29
  }
30
+
31
+
24
32
 
25
33
  isAlive() {
26
34
  return this._alive;
27
35
  }
28
36
 
37
+ getMountPoint(kind, mountName) {
38
+ const kindUri = parseKapetaUri(kind);
39
+ return Path.join(this._mountDir, kindUri.handle, kindUri.name, mountName);
40
+ }
41
+
42
+ createMounts(kind, mountOpts) {
43
+ const mounts = {};
44
+
45
+ _.forEach(mountOpts, (containerPath, mountName) => {
46
+ const hostPath = this.getMountPoint(kind, mountName);
47
+ mkdirp.sync(hostPath);
48
+ mounts[containerPath] = hostPath;
49
+ });
50
+ return mounts;
51
+ }
52
+
29
53
  async initialize() {
30
54
  // try
31
55
  const connectOptions = [
@@ -44,6 +68,7 @@ class ContainerManager {
44
68
  const client = new Docker(opts);
45
69
  await client.ping();
46
70
  this._docker = client;
71
+ this._alive = true;
47
72
  return;
48
73
  } catch (err) {
49
74
  // silently ignore bad configs
@@ -52,38 +77,26 @@ class ContainerManager {
52
77
  throw new Error("Unable to connect to docker");
53
78
  }
54
79
 
55
- async ping() {
56
- await this._docker.ping();
57
- this._alive = true;
58
- }
59
-
60
80
  async ping() {
61
81
 
62
82
  try {
63
- const pingResult = await this._docker.ping();
83
+ const pingResult = await this.docker().ping();
64
84
  if (pingResult !== 'OK') {
65
85
  throw new Error(`Ping failed: ${pingResult}`);
66
86
  }
67
87
  } catch (e) {
68
88
  throw new Error(`Docker not running. Please start the docker daemon before running this command. Error: ${e.message}`);
69
89
  }
70
-
71
- this._alive = true;
72
90
  }
73
-
74
- async ensureAlive() {
75
- if (!this._alive) {
76
- await this.ping();
91
+ docker() {
92
+ if (!this._docker) {
93
+ throw new Error(`Docker not running`);
77
94
  }
78
- }
79
-
80
- async docker() {
81
- await this.ensureAlive();
82
95
  return this._docker;
83
96
  }
84
97
 
85
98
  async getContainerByName(containerName) {
86
- const containers = await this._docker.container.list({all: true});
99
+ const containers = await this.docker().container.list({all: true});
87
100
  return containers.find(container => {
88
101
  return container.data.Names.indexOf(`/${containerName}`) > -1;
89
102
  });
@@ -95,7 +108,7 @@ class ContainerManager {
95
108
  tag = 'latest';
96
109
  }
97
110
 
98
- await this._docker.image
111
+ await this.docker().image
99
112
  .create(
100
113
  {},
101
114
  {
@@ -105,6 +118,34 @@ class ContainerManager {
105
118
  )
106
119
  .then((stream) => promisifyStream(stream));
107
120
  }
121
+
122
+ toDockerMounts(mounts) {
123
+ const Mounts = [];
124
+ _.forEach(mounts, (Source, Target) => {
125
+ Mounts.push({
126
+ Target,
127
+ Source,
128
+ Type: "bind",
129
+ ReadOnly: false,
130
+ Consistency: "consistent",
131
+ });
132
+ });
133
+
134
+ return Mounts;
135
+ }
136
+
137
+ toDockerHealth(health) {
138
+ return {
139
+ Test: ["CMD-SHELL", health.cmd],
140
+ Interval: health.interval
141
+ ? health.interval * NANO_SECOND
142
+ : 5000 * NANO_SECOND,
143
+ Timeout: health.timeout
144
+ ? health.timeout * NANO_SECOND
145
+ : 15000 * NANO_SECOND,
146
+ Retries: health.retries || 10,
147
+ };
148
+ }
108
149
 
109
150
  /**
110
151
  *
@@ -114,7 +155,7 @@ class ContainerManager {
114
155
  * @return {Promise<ContainerInfo>}
115
156
  */
116
157
  async run(image, name, opts) {
117
- const Mounts = [];
158
+
118
159
  const PortBindings = {};
119
160
  const Env = [];
120
161
  const Labels = {
@@ -138,15 +179,7 @@ class ContainerManager {
138
179
  Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
139
180
  });
140
181
 
141
- _.forEach(opts.mounts, (Source, Target) => {
142
- Mounts.push({
143
- Target,
144
- Source,
145
- Type: "bind",
146
- ReadOnly: false,
147
- Consistency: "consistent",
148
- });
149
- });
182
+ const Mounts = this.toDockerMounts(opts.mounts);
150
183
 
151
184
  _.forEach(opts.env, (value, name) => {
152
185
  Env.push(name + "=" + value);
@@ -155,16 +188,7 @@ class ContainerManager {
155
188
  let HealthCheck = undefined;
156
189
 
157
190
  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
- };
191
+ HealthCheck = this.toDockerHealth(opts.health);
168
192
 
169
193
  console.log("Adding health check", HealthCheck);
170
194
  }
@@ -183,14 +207,14 @@ class ContainerManager {
183
207
  });
184
208
 
185
209
  if (opts.health) {
186
- await this._waitForHealthy(dockerContainer);
210
+ await this.waitForHealthy(dockerContainer);
187
211
  }
188
212
 
189
213
  return new ContainerInfo(dockerContainer);
190
214
  }
191
215
 
192
216
  async startContainer(opts) {
193
- const dockerContainer = await this._docker.container.create(opts);
217
+ const dockerContainer = await this.docker().container.create(opts);
194
218
 
195
219
  await dockerContainer.start();
196
220
 
@@ -198,31 +222,33 @@ class ContainerManager {
198
222
  }
199
223
 
200
224
 
201
- async _waitForHealthy(container, attempt) {
225
+ async waitForHealthy(container, attempt) {
202
226
  if (!attempt) {
203
227
  attempt = 0;
204
228
  }
205
229
 
206
230
  if (attempt >= HEALTH_CHECK_MAX) {
207
- throw new Error("Operator did not become healthy within the timeout");
231
+ throw new Error("Container did not become healthy within the timeout");
208
232
  }
209
233
 
210
234
  if (await this._isHealthy(container)) {
211
- console.log("Container became healthy");
212
235
  return;
213
236
  }
214
237
 
215
- return new Promise((resolve) => {
238
+ return new Promise((resolve, reject) => {
216
239
  setTimeout(async () => {
217
- await this._waitForHealthy(container, attempt + 1);
218
- resolve();
240
+ try {
241
+ await this.waitForHealthy(container, attempt + 1);
242
+ resolve();
243
+ } catch (err) {
244
+ reject(err);
245
+ }
219
246
  }, HEALTH_CHECK_INTERVAL);
220
247
  });
221
248
  }
222
249
 
223
250
  async _isHealthy(container) {
224
251
  const info = await container.status();
225
-
226
252
  return info?.data?.State?.Health?.Status === "healthy";
227
253
  }
228
254
 
@@ -235,7 +261,7 @@ class ContainerManager {
235
261
  let dockerContainer = null;
236
262
 
237
263
  try {
238
- dockerContainer = await this._docker.container.get(name);
264
+ dockerContainer = await this.docker().container.get(name);
239
265
  await dockerContainer.status();
240
266
  } catch (err) {
241
267
  //Ignore
@@ -144,13 +144,7 @@ class OperatorManager {
144
144
  };
145
145
  }
146
146
 
147
- const mounts = {};
148
-
149
- _.forEach(operatorData.mounts, (containerPath, mountName) => {
150
- const hostPath = this._getMountPoint(resourceType, mountName);
151
- mkdirp.sync(hostPath);
152
- mounts[containerPath] = hostPath;
153
- });
147
+ const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
154
148
 
155
149
  const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
156
150
  let container = await containerManager.get(containerName);
@@ -9,6 +9,7 @@ const serviceManager = require("../serviceManager");
9
9
  const containerManager = require("../containerManager");
10
10
  const LogData = require("./LogData");
11
11
  const EventEmitter = require("events");
12
+ const md5 = require('md5');
12
13
  const {execSync} = require("child_process");
13
14
 
14
15
  const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
@@ -25,10 +26,6 @@ const DOCKER_ENV_VARS = [
25
26
  `KAPETA_ENVIRONMENT_TYPE=docker`,
26
27
  ]
27
28
 
28
- function md5(data) {
29
- return require('crypto').createHash('md5').update(data).digest("hex");
30
- }
31
-
32
29
 
33
30
  class BlockInstanceRunner {
34
31
  /**
@@ -386,6 +383,8 @@ class BlockInstanceRunner {
386
383
  const ExposedPorts = {};
387
384
  const addonEnv = {};
388
385
  const PortBindings = {};
386
+ let HealthCheck = undefined;
387
+ let Mounts = [];
389
388
  const promises = Object.entries(spec.local.ports)
390
389
  .map(async ([portType, value]) => {
391
390
  const dockerPort = `${value.port}/${value.type}`;
@@ -402,17 +401,34 @@ class BlockInstanceRunner {
402
401
 
403
402
  await Promise.all(promises);
404
403
 
404
+ if (spec.local?.env) {
405
+ Object.entries(spec.local.env).forEach(([key, value]) => {
406
+ addonEnv[key] = value;
407
+ });
408
+ }
409
+
410
+ if (spec.local?.mounts) {
411
+ const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
412
+ Mounts = containerManager.toDockerMounts(mounts);
413
+ }
414
+
415
+ if (spec.local?.health) {
416
+ HealthCheck = containerManager.toDockerHealth(spec.local?.health);
417
+ }
418
+
405
419
  logs.addLog(`Creating new container for block: ${containerName}`);
406
420
  container = await containerManager.startContainer({
407
421
  Image: dockerImage,
408
422
  name: containerName,
409
423
  ExposedPorts,
424
+ HealthCheck,
410
425
  HostConfig: {
411
426
  Binds: [
412
427
  `${kapetaYmlPath}:/kapeta.yml:ro`,
413
428
  `${ClusterConfig.getKapetaBasedir()}:${ClusterConfig.getKapetaBasedir()}`
414
429
  ],
415
- PortBindings
430
+ PortBindings,
431
+ Mounts
416
432
  },
417
433
  Labels: {
418
434
  'instance': blockInstance.id
@@ -426,6 +442,10 @@ class BlockInstanceRunner {
426
442
  }).map(([key, value]) => `${key}=${value}`)
427
443
  ]
428
444
  });
445
+
446
+ if (HealthCheck) {
447
+ await containerManager.waitForHealthy(container);
448
+ }
429
449
  }
430
450
 
431
451
  return this._handleContainer(container, logs, true);