@kapeta/local-cluster-service 0.9.1 → 0.10.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.
@@ -7,8 +7,8 @@ import { Docker } from 'node-docker-api';
7
7
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
8
  import ClusterConfiguration from '@kapeta/local-cluster-config';
9
9
  import { Container } from 'node-docker-api/lib/container';
10
- import { getBindHost } from './utils/utils';
11
- import uuid from "node-uuid";
10
+ import uuid from 'node-uuid';
11
+ import md5 from 'md5';
12
12
 
13
13
  type StringMap = { [key: string]: string };
14
14
 
@@ -54,7 +54,7 @@ interface Health {
54
54
  retries?: number;
55
55
  }
56
56
 
57
- const LABEL_PORT_PREFIX = 'kapeta_port-';
57
+ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
58
58
  const NANO_SECOND = 1000000;
59
59
  const HEALTH_CHECK_INTERVAL = 3000;
60
60
  const HEALTH_CHECK_MAX = 20;
@@ -206,24 +206,21 @@ class ContainerManager {
206
206
  tag = 'latest';
207
207
  }
208
208
 
209
- if (tag !== 'latest') {
210
- if (IMAGE_PULL_CACHE[image]) {
211
- const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
212
- if (timeSince < cacheForMS) {
213
- return;
214
- }
209
+ if (IMAGE_PULL_CACHE[image]) {
210
+ const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
211
+ if (timeSince < cacheForMS) {
212
+ return false;
215
213
  }
214
+ }
216
215
 
217
- const imageTagList = (await this.docker().image.list())
218
- .map((image) => image.data as any)
219
- .filter((imageData) => !!imageData.RepoTags)
220
- .map((imageData) => imageData.RepoTags as string[]);
216
+ const imageTagList = (await this.docker().image.list())
217
+ .map((image) => image.data as any)
218
+ .filter((imageData) => !!imageData.RepoTags)
219
+ .map((imageData) => imageData.RepoTags as string[]);
221
220
 
222
- if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
223
- console.log('Image found: %s', image);
224
- return;
225
- }
226
- console.log('Image not found: %s', image);
221
+ if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
222
+ console.log('Image found: %s', image);
223
+ return false;
227
224
  }
228
225
 
229
226
  console.log('Pulling image: %s', image);
@@ -240,6 +237,8 @@ class ContainerManager {
240
237
  IMAGE_PULL_CACHE[image] = Date.now();
241
238
 
242
239
  console.log('Image pulled: %s', image);
240
+
241
+ return true;
243
242
  }
244
243
 
245
244
  toDockerMounts(mounts: StringMap) {
@@ -266,66 +265,63 @@ class ContainerManager {
266
265
  };
267
266
  }
268
267
 
269
- async run(
270
- image: string,
271
- name: string,
272
- opts: { ports: {}; mounts: {}; env: {}; cmd: string; health: Health }
273
- ): Promise<ContainerInfo> {
274
- const PortBindings: { [key: string]: any } = {};
275
- const Env: string[] = [];
276
- const Labels: StringMap = {
277
- kapeta: 'true',
278
- };
279
-
280
- await this.pull(image);
281
-
282
- const bindHost = getBindHost();
268
+ private applyHash(dockerOpts: any) {
269
+ if (dockerOpts?.Labels?.HASH) {
270
+ delete dockerOpts.Labels.HASH;
271
+ }
283
272
 
284
- const ExposedPorts: { [key: string]: any } = {};
273
+ const hash = md5(JSON.stringify(dockerOpts));
285
274
 
286
- _.forEach(opts.ports, (portInfo: any, containerPort) => {
287
- ExposedPorts['' + containerPort] = {};
288
- PortBindings['' + containerPort] = [
289
- {
290
- HostPort: '' + portInfo.hostPort,
291
- HostIp: bindHost,
292
- },
293
- ];
275
+ if (!dockerOpts.Labels) {
276
+ dockerOpts.Labels = {};
277
+ }
278
+ dockerOpts.Labels.HASH = hash;
279
+ }
294
280
 
295
- Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
296
- });
281
+ async ensureContainer(opts: any) {
282
+ let imagePulled = false;
283
+ try {
284
+ imagePulled = await this.pull(opts.Image);
285
+ } catch (e) {
286
+ console.warn('Failed to pull image. Continuing...', e);
287
+ }
297
288
 
298
- const Mounts = this.toDockerMounts(opts.mounts);
289
+ this.applyHash(opts);
290
+ if (!opts.name) {
291
+ console.log('Starting unnamed container: %s', opts.Image);
292
+ return this.startContainer(opts);
293
+ }
294
+ const containerInfo = await this.getContainerByName(opts.name);
295
+ if (imagePulled) {
296
+ console.log('New version of image was pulled: %s', opts.Image);
297
+ } else {
298
+ // If image was pulled always recreate
299
+ if (!containerInfo) {
300
+ console.log('Starting new container: %s', opts.name);
301
+ return this.startContainer(opts);
302
+ }
299
303
 
300
- _.forEach(opts.env, (value, name) => {
301
- Env.push(name + '=' + value);
302
- });
304
+ const containerData = containerInfo.native.data as any;
303
305
 
304
- let HealthCheck = undefined;
305
-
306
- if (opts.health) {
307
- HealthCheck = this.toDockerHealth(opts.health);
308
- }
309
- const dockerContainer = await this.startContainer({
310
- name: name,
311
- Image: image,
312
- Hostname: name + '.kapeta',
313
- Labels,
314
- Cmd: opts.cmd,
315
- ExposedPorts,
316
- Env,
317
- HealthCheck,
318
- HostConfig: {
319
- PortBindings,
320
- Mounts,
321
- },
322
- });
306
+ if (containerData?.Labels?.HASH === opts.Labels.HASH) {
307
+ if (!(await containerInfo.isRunning())) {
308
+ console.log('Starting previously created container: %s', opts.name);
309
+ await containerInfo.start();
310
+ } else {
311
+ console.log('Previously created container already running: %s', opts.name);
312
+ }
313
+ return containerInfo.native;
314
+ }
315
+ }
323
316
 
324
- if (opts.health) {
325
- await this.waitForHealthy(dockerContainer);
317
+ if (containerInfo) {
318
+ // Remove the container and start a new one
319
+ console.log('Replacing previously created container: %s', opts.name);
320
+ await containerInfo.remove({ force: true });
326
321
  }
327
322
 
328
- return new ContainerInfo(dockerContainer);
323
+ console.log('Starting new container: %s', opts.name);
324
+ return this.startContainer(opts);
329
325
  }
330
326
 
331
327
  async startContainer(opts: any) {
@@ -423,8 +419,8 @@ class ContainerManager {
423
419
  }
424
420
  }
425
421
 
426
- async remove(container:Container, opts?: { force?: boolean }) {
427
- const newName = 'deleting-' + uuid.v4()
422
+ async remove(container: Container, opts?: { force?: boolean }) {
423
+ const newName = 'deleting-' + uuid.v4();
428
424
  const containerData = container.data as any;
429
425
  // Rename the container first to avoid name conflicts if people start the same container
430
426
  await container.rename({ name: newName });
@@ -537,11 +533,11 @@ export class ContainerInfo {
537
533
  const ports: PortMap = {};
538
534
 
539
535
  _.forEach(inspectResult.Config.Labels, (portType, name) => {
540
- if (!name.startsWith(LABEL_PORT_PREFIX)) {
536
+ if (!name.startsWith(CONTAINER_LABEL_PORT_PREFIX)) {
541
537
  return;
542
538
  }
543
539
 
544
- const hostPort = name.substr(LABEL_PORT_PREFIX.length);
540
+ const hostPort = name.substr(CONTAINER_LABEL_PORT_PREFIX.length);
545
541
 
546
542
  portTypes[hostPort] = portType;
547
543
  });
@@ -559,7 +559,10 @@ export class InstanceManager {
559
559
  }
560
560
  }
561
561
 
562
- if (instance.desiredStatus === DesiredInstanceStatus.RUN && newStatus === InstanceStatus.STOPPED) {
562
+ if (
563
+ instance.desiredStatus === DesiredInstanceStatus.RUN &&
564
+ [InstanceStatus.STOPPED, InstanceStatus.FAILED, InstanceStatus.STOPPING].includes(newStatus)
565
+ ) {
563
566
  //If the instance is stopped but we want it to run, start it
564
567
  try {
565
568
  await this.start(instance.systemId, instance.instanceId);
@@ -569,7 +572,10 @@ export class InstanceManager {
569
572
  return;
570
573
  }
571
574
 
572
- if (instance.desiredStatus === DesiredInstanceStatus.STOP && newStatus === InstanceStatus.READY) {
575
+ if (
576
+ instance.desiredStatus === DesiredInstanceStatus.STOP &&
577
+ [InstanceStatus.READY, InstanceStatus.STARTING, InstanceStatus.UNHEALTHY].includes(newStatus)
578
+ ) {
573
579
  //If the instance is running but we want it to stop, stop it
574
580
  try {
575
581
  await this.stop(instance.systemId, instance.instanceId);
@@ -4,12 +4,14 @@ import md5 from 'md5';
4
4
  import { parseKapetaUri } from '@kapeta/nodejs-utils';
5
5
  import { serviceManager } from './serviceManager';
6
6
  import { storageService } from './storageService';
7
- import { ContainerInfo, containerManager } from './containerManager';
7
+ import { CONTAINER_LABEL_PORT_PREFIX, ContainerInfo, containerManager } from './containerManager';
8
8
  import FSExtra from 'fs-extra';
9
- import { AnyMap, EnvironmentType, OperatorInfo } from './types';
9
+ import { AnyMap, EnvironmentType, OperatorInfo, StringMap } from './types';
10
10
  import { BlockInstance, Resource } from '@kapeta/schemas';
11
11
  import { definitionsManager } from './definitionsManager';
12
- import { normalizeKapetaUri } from './utils/utils';
12
+ import { getBindHost, normalizeKapetaUri } from './utils/utils';
13
+ import _ from 'lodash';
14
+ import { Container } from 'node-docker-api/lib/container';
13
15
 
14
16
  const KIND_OPERATOR = 'core/resource-type-operator';
15
17
 
@@ -191,34 +193,58 @@ class OperatorManager {
191
193
  const mounts = containerManager.createMounts(resourceType, operatorData.mounts);
192
194
 
193
195
  const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
194
- let container = await containerManager.get(containerName);
195
196
 
196
- const isRunning = container ? await container.isRunning() : false;
197
- if (container && !isRunning) {
198
- await container.start();
199
- }
197
+ const PortBindings: { [key: string]: any } = {};
198
+ const Env: string[] = [];
200
199
 
201
- if (!container) {
202
- container = await containerManager.run(operatorData.image, containerName, {
203
- mounts,
204
- ports,
205
- health: operatorData.health,
206
- env: operatorData.env,
207
- cmd: operatorData.cmd,
208
- });
209
- }
200
+ const Labels: StringMap = {
201
+ kapeta: 'true',
202
+ };
210
203
 
211
- try {
212
- if (operatorData.health) {
213
- await containerManager.waitForHealthy(container.native);
214
- } else {
215
- await containerManager.waitForReady(container.native);
216
- }
217
- } catch (e: any) {
218
- console.error(e.message);
204
+ const bindHost = getBindHost();
205
+
206
+ const ExposedPorts: { [key: string]: any } = {};
207
+
208
+ _.forEach(ports, (portInfo: any, containerPort) => {
209
+ ExposedPorts['' + containerPort] = {};
210
+ PortBindings['' + containerPort] = [
211
+ {
212
+ HostPort: '' + portInfo.hostPort,
213
+ HostIp: bindHost,
214
+ },
215
+ ];
216
+
217
+ Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
218
+ });
219
+
220
+ const Mounts = containerManager.toDockerMounts(mounts);
221
+
222
+ _.forEach(operatorData.env, (value, name) => {
223
+ Env.push(name + '=' + value);
224
+ });
225
+
226
+ let HealthCheck = undefined;
227
+
228
+ if (operatorData.health) {
229
+ HealthCheck = containerManager.toDockerHealth(operatorData.health);
219
230
  }
220
231
 
221
- return container;
232
+ const container = await containerManager.ensureContainer({
233
+ name: containerName,
234
+ Image: operatorData.image,
235
+ Hostname: containerName + '.kapeta',
236
+ Labels,
237
+ Cmd: operatorData.cmd,
238
+ ExposedPorts,
239
+ Env,
240
+ HealthCheck,
241
+ HostConfig: {
242
+ PortBindings,
243
+ Mounts,
244
+ },
245
+ });
246
+
247
+ return new ContainerInfo(container);
222
248
  }
223
249
  }
224
250