@kapeta/local-cluster-service 0.9.1 → 0.10.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.
@@ -6,9 +6,9 @@ import FSExtra from 'fs-extra';
6
6
  import { Docker } from 'node-docker-api';
7
7
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
8
  import ClusterConfiguration from '@kapeta/local-cluster-config';
9
- import { getBindHost } from './utils/utils';
10
- import uuid from "node-uuid";
11
- const LABEL_PORT_PREFIX = 'kapeta_port-';
9
+ import uuid from 'node-uuid';
10
+ import md5 from 'md5';
11
+ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
12
12
  const NANO_SECOND = 1000000;
13
13
  const HEALTH_CHECK_INTERVAL = 3000;
14
14
  const HEALTH_CHECK_MAX = 20;
@@ -141,22 +141,19 @@ class ContainerManager {
141
141
  if (!tag) {
142
142
  tag = 'latest';
143
143
  }
144
- if (tag !== 'latest') {
145
- if (IMAGE_PULL_CACHE[image]) {
146
- const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
147
- if (timeSince < cacheForMS) {
148
- return;
149
- }
150
- }
151
- const imageTagList = (await this.docker().image.list())
152
- .map((image) => image.data)
153
- .filter((imageData) => !!imageData.RepoTags)
154
- .map((imageData) => imageData.RepoTags);
155
- if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
156
- console.log('Image found: %s', image);
157
- return;
144
+ if (IMAGE_PULL_CACHE[image]) {
145
+ const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
146
+ if (timeSince < cacheForMS) {
147
+ return false;
158
148
  }
159
- console.log('Image not found: %s', image);
149
+ }
150
+ const imageTagList = (await this.docker().image.list())
151
+ .map((image) => image.data)
152
+ .filter((imageData) => !!imageData.RepoTags)
153
+ .map((imageData) => imageData.RepoTags);
154
+ if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
155
+ console.log('Image found: %s', image);
156
+ return false;
160
157
  }
161
158
  console.log('Pulling image: %s', image);
162
159
  await this.docker()
@@ -167,6 +164,7 @@ class ContainerManager {
167
164
  .then((stream) => promisifyStream(stream));
168
165
  IMAGE_PULL_CACHE[image] = Date.now();
169
166
  console.log('Image pulled: %s', image);
167
+ return true;
170
168
  }
171
169
  toDockerMounts(mounts) {
172
170
  const Mounts = [];
@@ -189,51 +187,58 @@ class ContainerManager {
189
187
  Retries: health.retries || 10,
190
188
  };
191
189
  }
192
- async run(image, name, opts) {
193
- const PortBindings = {};
194
- const Env = [];
195
- const Labels = {
196
- kapeta: 'true',
197
- };
198
- await this.pull(image);
199
- const bindHost = getBindHost();
200
- const ExposedPorts = {};
201
- _.forEach(opts.ports, (portInfo, containerPort) => {
202
- ExposedPorts['' + containerPort] = {};
203
- PortBindings['' + containerPort] = [
204
- {
205
- HostPort: '' + portInfo.hostPort,
206
- HostIp: bindHost,
207
- },
208
- ];
209
- Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
210
- });
211
- const Mounts = this.toDockerMounts(opts.mounts);
212
- _.forEach(opts.env, (value, name) => {
213
- Env.push(name + '=' + value);
214
- });
215
- let HealthCheck = undefined;
216
- if (opts.health) {
217
- HealthCheck = this.toDockerHealth(opts.health);
218
- }
219
- const dockerContainer = await this.startContainer({
220
- name: name,
221
- Image: image,
222
- Hostname: name + '.kapeta',
223
- Labels,
224
- Cmd: opts.cmd,
225
- ExposedPorts,
226
- Env,
227
- HealthCheck,
228
- HostConfig: {
229
- PortBindings,
230
- Mounts,
231
- },
232
- });
233
- if (opts.health) {
234
- await this.waitForHealthy(dockerContainer);
190
+ applyHash(dockerOpts) {
191
+ if (dockerOpts?.Labels?.HASH) {
192
+ delete dockerOpts.Labels.HASH;
235
193
  }
236
- return new ContainerInfo(dockerContainer);
194
+ const hash = md5(JSON.stringify(dockerOpts));
195
+ if (!dockerOpts.Labels) {
196
+ dockerOpts.Labels = {};
197
+ }
198
+ dockerOpts.Labels.HASH = hash;
199
+ }
200
+ async ensureContainer(opts) {
201
+ let imagePulled = false;
202
+ try {
203
+ imagePulled = await this.pull(opts.Image);
204
+ }
205
+ catch (e) {
206
+ console.warn('Failed to pull image. Continuing...', e);
207
+ }
208
+ this.applyHash(opts);
209
+ if (!opts.name) {
210
+ console.log('Starting unnamed container: %s', opts.Image);
211
+ return this.startContainer(opts);
212
+ }
213
+ const containerInfo = await this.getContainerByName(opts.name);
214
+ if (imagePulled) {
215
+ console.log('New version of image was pulled: %s', opts.Image);
216
+ }
217
+ else {
218
+ // If image was pulled always recreate
219
+ if (!containerInfo) {
220
+ console.log('Starting new container: %s', opts.name);
221
+ return this.startContainer(opts);
222
+ }
223
+ const containerData = containerInfo.native.data;
224
+ if (containerData?.Labels?.HASH === opts.Labels.HASH) {
225
+ if (!(await containerInfo.isRunning())) {
226
+ console.log('Starting previously created container: %s', opts.name);
227
+ await containerInfo.start();
228
+ }
229
+ else {
230
+ console.log('Previously created container already running: %s', opts.name);
231
+ }
232
+ return containerInfo.native;
233
+ }
234
+ }
235
+ if (containerInfo) {
236
+ // Remove the container and start a new one
237
+ console.log('Replacing previously created container: %s', opts.name);
238
+ await containerInfo.remove({ force: true });
239
+ }
240
+ console.log('Starting new container: %s', opts.name);
241
+ return this.startContainer(opts);
237
242
  }
238
243
  async startContainer(opts) {
239
244
  const extraHosts = getExtraHosts(this._version);
@@ -411,10 +416,10 @@ export class ContainerInfo {
411
416
  const portTypes = {};
412
417
  const ports = {};
413
418
  _.forEach(inspectResult.Config.Labels, (portType, name) => {
414
- if (!name.startsWith(LABEL_PORT_PREFIX)) {
419
+ if (!name.startsWith(CONTAINER_LABEL_PORT_PREFIX)) {
415
420
  return;
416
421
  }
417
- const hostPort = name.substr(LABEL_PORT_PREFIX.length);
422
+ const hostPort = name.substr(CONTAINER_LABEL_PORT_PREFIX.length);
418
423
  portTypes[hostPort] = portType;
419
424
  });
420
425
  _.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
@@ -447,7 +447,8 @@ export class InstanceManager {
447
447
  changed = true;
448
448
  }
449
449
  }
450
- if (instance.desiredStatus === DesiredInstanceStatus.RUN && newStatus === InstanceStatus.STOPPED) {
450
+ if (instance.desiredStatus === DesiredInstanceStatus.RUN &&
451
+ [InstanceStatus.STOPPED, InstanceStatus.FAILED, InstanceStatus.STOPPING].includes(newStatus)) {
451
452
  //If the instance is stopped but we want it to run, start it
452
453
  try {
453
454
  await this.start(instance.systemId, instance.instanceId);
@@ -457,7 +458,8 @@ export class InstanceManager {
457
458
  }
458
459
  return;
459
460
  }
460
- if (instance.desiredStatus === DesiredInstanceStatus.STOP && newStatus === InstanceStatus.READY) {
461
+ if (instance.desiredStatus === DesiredInstanceStatus.STOP &&
462
+ [InstanceStatus.READY, InstanceStatus.STARTING, InstanceStatus.UNHEALTHY].includes(newStatus)) {
461
463
  //If the instance is running but we want it to stop, stop it
462
464
  try {
463
465
  await this.stop(instance.systemId, instance.instanceId);
@@ -3,10 +3,11 @@ import md5 from 'md5';
3
3
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
4
4
  import { serviceManager } from './serviceManager';
5
5
  import { storageService } from './storageService';
6
- import { containerManager } from './containerManager';
6
+ import { CONTAINER_LABEL_PORT_PREFIX, ContainerInfo, containerManager } from './containerManager';
7
7
  import FSExtra from 'fs-extra';
8
8
  import { definitionsManager } from './definitionsManager';
9
- import { normalizeKapetaUri } from './utils/utils';
9
+ import { getBindHost, normalizeKapetaUri } from './utils/utils';
10
+ import _ from 'lodash';
10
11
  const KIND_OPERATOR = 'core/resource-type-operator';
11
12
  class Operator {
12
13
  _data;
@@ -132,32 +133,46 @@ class OperatorManager {
132
133
  }
133
134
  const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
134
135
  const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
135
- let container = await containerManager.get(containerName);
136
- const isRunning = container ? await container.isRunning() : false;
137
- if (container && !isRunning) {
138
- await container.start();
139
- }
140
- if (!container) {
141
- container = await containerManager.run(operatorData.image, containerName, {
142
- mounts,
143
- ports,
144
- health: operatorData.health,
145
- env: operatorData.env,
146
- cmd: operatorData.cmd,
147
- });
148
- }
149
- try {
150
- if (operatorData.health) {
151
- await containerManager.waitForHealthy(container.native);
152
- }
153
- else {
154
- await containerManager.waitForReady(container.native);
155
- }
156
- }
157
- catch (e) {
158
- console.error(e.message);
136
+ const PortBindings = {};
137
+ const Env = [];
138
+ const Labels = {
139
+ kapeta: 'true',
140
+ };
141
+ const bindHost = getBindHost();
142
+ const ExposedPorts = {};
143
+ _.forEach(ports, (portInfo, containerPort) => {
144
+ ExposedPorts['' + containerPort] = {};
145
+ PortBindings['' + containerPort] = [
146
+ {
147
+ HostPort: '' + portInfo.hostPort,
148
+ HostIp: bindHost,
149
+ },
150
+ ];
151
+ Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
152
+ });
153
+ const Mounts = containerManager.toDockerMounts(mounts);
154
+ _.forEach(operatorData.env, (value, name) => {
155
+ Env.push(name + '=' + value);
156
+ });
157
+ let HealthCheck = undefined;
158
+ if (operatorData.health) {
159
+ HealthCheck = containerManager.toDockerHealth(operatorData.health);
159
160
  }
160
- return container;
161
+ const container = await containerManager.ensureContainer({
162
+ name: containerName,
163
+ Image: operatorData.image,
164
+ Hostname: containerName + '.kapeta',
165
+ Labels,
166
+ Cmd: operatorData.cmd,
167
+ ExposedPorts,
168
+ Env,
169
+ HealthCheck,
170
+ HostConfig: {
171
+ PortBindings,
172
+ Mounts,
173
+ },
174
+ });
175
+ return new ContainerInfo(container);
161
176
  }
162
177
  }
163
178
  export const operatorManager = new OperatorManager();
@@ -14,6 +14,7 @@ export declare class BlockInstanceRunner {
14
14
  * Starts local process
15
15
  */
16
16
  private _startLocalProcess;
17
+ private ensureContainer;
17
18
  private _handleContainer;
18
19
  private _startDockerProcess;
19
20
  /**
@@ -128,27 +128,6 @@ export class BlockInstanceRunner {
128
128
  throw new Error(`Missing docker image information: ${JSON.stringify(localContainer)}`);
129
129
  }
130
130
  const containerName = getBlockInstanceContainerName(blockInstance.id);
131
- const logs = new LogData();
132
- logs.addLog(`Starting block ${blockInstance.ref}`);
133
- let containerInfo = await containerManager.getContainerByName(containerName);
134
- let container = containerInfo?.native;
135
- console.log('Starting dev container', containerName);
136
- if (containerInfo) {
137
- console.log(`Dev container already exists. Deleting...`);
138
- try {
139
- await containerInfo.remove({
140
- force: true,
141
- });
142
- }
143
- catch (e) {
144
- throw new Error('Failed to delete existing container: ' + e.message);
145
- }
146
- container = undefined;
147
- containerInfo = undefined;
148
- }
149
- logs.addLog(`Creating new container for block: ${containerName}`);
150
- console.log('Creating new dev container', containerName, dockerImage);
151
- await containerManager.pull(dockerImage);
152
131
  const startCmd = localContainer.handlers?.onCreate ? localContainer.handlers.onCreate : '';
153
132
  const dockerOpts = localContainer.options ?? {};
154
133
  const homeDir = localContainer.userHome ? localContainer.userHome : '/root';
@@ -177,8 +156,7 @@ export class BlockInstanceRunner {
177
156
  if (localContainer.healthcheck) {
178
157
  HealthCheck = containerManager.toDockerHealth({ cmd: localContainer.healthcheck });
179
158
  }
180
- console.log('Starting dev container', containerName, dockerImage);
181
- container = await containerManager.startContainer({
159
+ return this.ensureContainer({
182
160
  Image: dockerImage,
183
161
  name: containerName,
184
162
  WorkingDir: workingDir,
@@ -205,8 +183,12 @@ export class BlockInstanceRunner {
205
183
  },
206
184
  ...dockerOpts,
207
185
  });
186
+ }
187
+ async ensureContainer(opts) {
188
+ const logs = new LogData();
189
+ const container = await containerManager.ensureContainer(opts);
208
190
  try {
209
- if (HealthCheck) {
191
+ if (opts.HealthCheck) {
210
192
  await containerManager.waitForHealthy(container);
211
193
  }
212
194
  else {
@@ -284,45 +266,23 @@ export class BlockInstanceRunner {
284
266
  }
285
267
  const containerName = getBlockInstanceContainerName(blockInstance.id);
286
268
  const logs = new LogData();
287
- const containerInfo = await containerManager.getContainerByName(containerName);
288
- let container = containerInfo?.native;
289
269
  // For windows we need to default to root
290
270
  const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
291
- if (container) {
292
- const containerData = container.data;
293
- if (containerData.State === 'running') {
294
- logs.addLog(`Found existing running container for block: ${containerName}`);
295
- }
296
- else {
297
- logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
298
- await container.start();
299
- }
300
- }
301
- else {
302
- logs.addLog(`Creating new container for block: ${containerName}`);
303
- container = await containerManager.startContainer({
304
- Image: dockerImage,
305
- name: containerName,
306
- Labels: {
307
- instance: blockInstance.id,
308
- },
309
- Env: [
310
- ...DOCKER_ENV_VARS,
311
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
312
- ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
313
- ],
314
- HostConfig: {
315
- Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
316
- },
317
- });
318
- try {
319
- await containerManager.waitForReady(container);
320
- }
321
- catch (e) {
322
- logs.addLog(e.message, 'ERROR');
323
- }
324
- }
325
- return this._handleContainer(container, logs);
271
+ return this.ensureContainer({
272
+ Image: dockerImage,
273
+ name: containerName,
274
+ Labels: {
275
+ instance: blockInstance.id,
276
+ },
277
+ Env: [
278
+ ...DOCKER_ENV_VARS,
279
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
280
+ ...Object.entries(env).map(([key, value]) => `${key}=${value}`),
281
+ ],
282
+ HostConfig: {
283
+ Binds: [`${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`],
284
+ },
285
+ });
326
286
  }
327
287
  /**
328
288
  *
@@ -345,120 +305,68 @@ export class BlockInstanceRunner {
345
305
  throw new Error(`Provider did not have local image: ${providerRef}`);
346
306
  }
347
307
  const dockerImage = spec?.local?.image;
348
- try {
349
- await containerManager.pull(dockerImage);
350
- }
351
- catch (e) {
352
- console.warn('Failed to pull image. Continuing...', e);
353
- }
354
308
  const containerName = getBlockInstanceContainerName(blockInstance.id);
355
309
  const logs = new LogData();
356
- const containerInfo = await containerManager.getContainerByName(containerName);
357
- let container = containerInfo?.native;
358
- if (container) {
359
- const containerData = container.data;
360
- if (containerData.State === 'running') {
361
- logs.addLog(`Found existing running container for block: ${containerName}`);
362
- }
363
- else {
364
- if (containerData.State?.ExitCode > 0) {
365
- logs.addLog(`Container exited with code: ${containerData.State.ExitCode}. Deleting...`);
366
- try {
367
- await containerManager.remove(container);
368
- }
369
- catch (e) { }
370
- container = undefined;
371
- }
372
- else {
373
- logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
374
- try {
375
- await container.start();
376
- }
377
- catch (e) {
378
- console.warn('Failed to start container. Deleting...', e);
379
- try {
380
- await containerManager.remove(container);
381
- }
382
- catch (e) { }
383
- container = undefined;
384
- }
385
- }
386
- }
387
- }
388
310
  const bindHost = getBindHost();
389
- if (!container) {
390
- const ExposedPorts = {};
391
- const addonEnv = {};
392
- const PortBindings = {};
393
- let HealthCheck = undefined;
394
- let Mounts = [];
395
- const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
396
- const dockerPort = `${value.port}/${value.type}`;
397
- ExposedPorts[dockerPort] = {};
398
- addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
399
- const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
400
- PortBindings[dockerPort] = [
401
- {
402
- HostIp: bindHost,
403
- HostPort: `${publicPort}`,
404
- },
405
- ];
406
- });
407
- await Promise.all(promises);
408
- if (spec.local?.env) {
409
- Object.entries(spec.local.env).forEach(([key, value]) => {
410
- addonEnv[key] = value;
411
- });
412
- }
413
- if (spec.local?.mounts) {
414
- const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
415
- Mounts = containerManager.toDockerMounts(mounts);
416
- }
417
- if (spec.local?.health) {
418
- HealthCheck = containerManager.toDockerHealth(spec.local?.health);
419
- }
420
- // For windows we need to default to root
421
- const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
422
- logs.addLog(`Creating new container for block: ${containerName}`);
423
- container = await containerManager.startContainer({
424
- Image: dockerImage,
425
- name: containerName,
426
- ExposedPorts,
427
- HealthCheck,
428
- HostConfig: {
429
- Binds: [
430
- `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
431
- `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
432
- ],
433
- PortBindings,
434
- Mounts,
435
- },
436
- Labels: {
437
- instance: blockInstance.id,
311
+ const ExposedPorts = {};
312
+ const addonEnv = {};
313
+ const PortBindings = {};
314
+ let HealthCheck = undefined;
315
+ let Mounts = [];
316
+ const promises = Object.entries(spec.local.ports).map(async ([portType, value]) => {
317
+ const dockerPort = `${value.port}/${value.type}`;
318
+ ExposedPorts[dockerPort] = {};
319
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
320
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
321
+ PortBindings[dockerPort] = [
322
+ {
323
+ HostIp: bindHost,
324
+ HostPort: `${publicPort}`,
438
325
  },
439
- Env: [
440
- `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
441
- `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
442
- ...DOCKER_ENV_VARS,
443
- ...Object.entries({
444
- ...env,
445
- ...addonEnv,
446
- }).map(([key, value]) => `${key}=${value}`),
447
- ],
326
+ ];
327
+ });
328
+ await Promise.all(promises);
329
+ if (spec.local?.env) {
330
+ Object.entries(spec.local.env).forEach(([key, value]) => {
331
+ addonEnv[key] = value;
448
332
  });
449
- try {
450
- if (HealthCheck) {
451
- await containerManager.waitForHealthy(container);
452
- }
453
- else {
454
- await containerManager.waitForReady(container);
455
- }
456
- }
457
- catch (e) {
458
- logs.addLog(e.message, 'ERROR');
459
- }
460
333
  }
461
- const out = await this._handleContainer(container, logs, true);
334
+ if (spec.local?.mounts) {
335
+ const mounts = containerManager.createMounts(blockUri.id, spec.local.mounts);
336
+ Mounts = containerManager.toDockerMounts(mounts);
337
+ }
338
+ if (spec.local?.health) {
339
+ HealthCheck = containerManager.toDockerHealth(spec.local?.health);
340
+ }
341
+ // For windows we need to default to root
342
+ const innerHome = process.platform === 'win32' ? '/root/.kapeta' : ClusterConfig.getKapetaBasedir();
343
+ logs.addLog(`Creating new container for block: ${containerName}`);
344
+ const out = await this.ensureContainer({
345
+ Image: dockerImage,
346
+ name: containerName,
347
+ ExposedPorts,
348
+ HealthCheck,
349
+ HostConfig: {
350
+ Binds: [
351
+ `${toLocalBindVolume(kapetaYmlPath)}:/kapeta.yml:ro`,
352
+ `${toLocalBindVolume(ClusterConfig.getKapetaBasedir())}:${innerHome}`,
353
+ ],
354
+ PortBindings,
355
+ Mounts,
356
+ },
357
+ Labels: {
358
+ instance: blockInstance.id,
359
+ },
360
+ Env: [
361
+ `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
362
+ `KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
363
+ ...DOCKER_ENV_VARS,
364
+ ...Object.entries({
365
+ ...env,
366
+ ...addonEnv,
367
+ }).map(([key, value]) => `${key}=${value}`),
368
+ ],
369
+ });
462
370
  const portTypes = spec.local.ports ? Object.keys(spec.local.ports) : [];
463
371
  if (portTypes.length > 0) {
464
372
  out.portType = portTypes[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {