@kapeta/local-cluster-service 0.11.1 → 0.12.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.
- package/CHANGELOG.md +14 -0
- package/definitions.d.ts +7 -0
- package/dist/cjs/src/config/routes.js +1 -1
- package/dist/cjs/src/containerManager.d.ts +3 -2
- package/dist/cjs/src/containerManager.js +127 -34
- package/dist/cjs/src/definitionsManager.d.ts +1 -0
- package/dist/cjs/src/definitionsManager.js +7 -4
- package/dist/cjs/src/instanceManager.d.ts +8 -1
- package/dist/cjs/src/instanceManager.js +56 -21
- package/dist/cjs/src/instances/routes.js +2 -0
- package/dist/cjs/src/operatorManager.d.ts +2 -0
- package/dist/cjs/src/operatorManager.js +70 -67
- package/dist/cjs/src/socketManager.d.ts +1 -0
- package/dist/cjs/src/socketManager.js +3 -0
- package/dist/cjs/src/types.d.ts +1 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +2 -3
- package/dist/esm/src/config/routes.js +1 -1
- package/dist/esm/src/containerManager.d.ts +3 -2
- package/dist/esm/src/containerManager.js +128 -35
- package/dist/esm/src/definitionsManager.d.ts +1 -0
- package/dist/esm/src/definitionsManager.js +8 -5
- package/dist/esm/src/instanceManager.d.ts +8 -1
- package/dist/esm/src/instanceManager.js +56 -21
- package/dist/esm/src/instances/routes.js +2 -0
- package/dist/esm/src/operatorManager.d.ts +2 -0
- package/dist/esm/src/operatorManager.js +68 -65
- package/dist/esm/src/socketManager.d.ts +1 -0
- package/dist/esm/src/socketManager.js +3 -0
- package/dist/esm/src/types.d.ts +1 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +2 -3
- package/dist/esm/src/utils/utils.js +1 -1
- package/package.json +1 -1
- package/src/config/routes.ts +1 -1
- package/src/containerManager.ts +181 -60
- package/src/definitionsManager.ts +9 -5
- package/src/instanceManager.ts +82 -42
- package/src/instances/routes.ts +3 -1
- package/src/operatorManager.ts +73 -69
- package/src/socketManager.ts +4 -0
- package/src/types.ts +1 -1
- package/src/utils/BlockInstanceRunner.ts +12 -24
- package/src/utils/utils.ts +2 -2
@@ -10,6 +10,9 @@ import { containerManager, HEALTH_CHECK_TIMEOUT } from './containerManager';
|
|
10
10
|
import { configManager } from './configManager';
|
11
11
|
import { DesiredInstanceStatus, InstanceOwner, InstanceStatus, InstanceType } from './types';
|
12
12
|
import { getBlockInstanceContainerName, normalizeKapetaUri } from './utils/utils';
|
13
|
+
import { KIND_OPERATOR, operatorManager } from './operatorManager';
|
14
|
+
import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
15
|
+
import { definitionsManager } from './definitionsManager';
|
13
16
|
const CHECK_INTERVAL = 5000;
|
14
17
|
const DEFAULT_HEALTH_PORT_TYPE = 'rest';
|
15
18
|
const EVENT_STATUS_CHANGED = 'status-changed';
|
@@ -46,7 +49,13 @@ export class InstanceManager {
|
|
46
49
|
return [];
|
47
50
|
}
|
48
51
|
systemId = normalizeKapetaUri(systemId);
|
49
|
-
|
52
|
+
const planInfo = definitionsManager.getDefinition(systemId);
|
53
|
+
if (!planInfo) {
|
54
|
+
return [];
|
55
|
+
}
|
56
|
+
const plan = planInfo.definition;
|
57
|
+
const instanceIds = plan.spec.blocks.map((block) => block.id);
|
58
|
+
return this._instances.filter((instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId));
|
50
59
|
}
|
51
60
|
getInstance(systemId, instanceId) {
|
52
61
|
systemId = normalizeKapetaUri(systemId);
|
@@ -55,7 +64,10 @@ export class InstanceManager {
|
|
55
64
|
async exclusive(systemId, instanceId, fn) {
|
56
65
|
systemId = normalizeKapetaUri(systemId);
|
57
66
|
const key = `${systemId}/${instanceId}`;
|
58
|
-
|
67
|
+
//console.log(`Acquiring lock for ${key}`, this.instanceLocks.isBusy(key));
|
68
|
+
const result = await this.instanceLocks.acquire(key, fn);
|
69
|
+
//console.log(`Releasing lock for ${key}`, this.instanceLocks.isBusy(key));
|
70
|
+
return result;
|
59
71
|
}
|
60
72
|
async getLogs(systemId, instanceId) {
|
61
73
|
const instance = this.getInstance(systemId, instanceId);
|
@@ -66,19 +78,23 @@ export class InstanceManager {
|
|
66
78
|
case InstanceType.DOCKER:
|
67
79
|
return await containerManager.getLogs(instance);
|
68
80
|
case InstanceType.UNKNOWN:
|
69
|
-
return [
|
81
|
+
return [
|
82
|
+
{
|
70
83
|
level: 'INFO',
|
71
84
|
message: 'Instance is starting...',
|
72
85
|
time: Date.now(),
|
73
86
|
source: 'stdout',
|
74
|
-
}
|
87
|
+
},
|
88
|
+
];
|
75
89
|
case InstanceType.LOCAL:
|
76
|
-
return [
|
90
|
+
return [
|
91
|
+
{
|
77
92
|
level: 'INFO',
|
78
93
|
message: 'Instance started outside Kapeta - logs not available...',
|
79
94
|
time: Date.now(),
|
80
95
|
source: 'stdout',
|
81
|
-
}
|
96
|
+
},
|
97
|
+
];
|
82
98
|
}
|
83
99
|
return [];
|
84
100
|
}
|
@@ -119,7 +135,8 @@ export class InstanceManager {
|
|
119
135
|
const address = await serviceManager.getProviderAddress(systemId, instanceId, info.portType ?? DEFAULT_HEALTH_PORT_TYPE);
|
120
136
|
const healthUrl = this.getHealthUrl(info, address);
|
121
137
|
if (instance) {
|
122
|
-
if (instance.status === InstanceStatus.STOPPING &&
|
138
|
+
if (instance.status === InstanceStatus.STOPPING &&
|
139
|
+
instance.desiredStatus === DesiredInstanceStatus.STOP) {
|
123
140
|
//If instance is stopping do not interfere
|
124
141
|
return;
|
125
142
|
}
|
@@ -229,8 +246,7 @@ export class InstanceManager {
|
|
229
246
|
if (instance.status === InstanceStatus.STOPPED) {
|
230
247
|
return;
|
231
248
|
}
|
232
|
-
if (changeDesired &&
|
233
|
-
instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
|
249
|
+
if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
|
234
250
|
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
235
251
|
}
|
236
252
|
instance.status = InstanceStatus.STOPPING;
|
@@ -324,6 +340,24 @@ export class InstanceManager {
|
|
324
340
|
console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
|
325
341
|
// Save the instance before starting it, so that we can track the status
|
326
342
|
await this.saveInternalInstance(instance);
|
343
|
+
const blockSpec = blockAsset.data.spec;
|
344
|
+
if (blockSpec.consumers) {
|
345
|
+
const promises = blockSpec.consumers.map((consumer) => {
|
346
|
+
const consumerUri = parseKapetaUri(consumer.kind);
|
347
|
+
const asset = definitionsManager.getDefinition(consumer.kind);
|
348
|
+
if (!asset) {
|
349
|
+
// Definition not found
|
350
|
+
return Promise.resolve();
|
351
|
+
}
|
352
|
+
if (KIND_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
|
353
|
+
// Not an operator
|
354
|
+
return Promise.resolve();
|
355
|
+
}
|
356
|
+
console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
|
357
|
+
return operatorManager.ensureResource(systemId, consumerUri.fullName, consumerUri.version);
|
358
|
+
});
|
359
|
+
await Promise.all(promises);
|
360
|
+
}
|
327
361
|
if (existingInstance) {
|
328
362
|
// Check if the instance is already running - but after we've commmuicated the desired status
|
329
363
|
const currentStatus = await this.requestInstanceStatus(existingInstance);
|
@@ -348,7 +382,7 @@ export class InstanceManager {
|
|
348
382
|
});
|
349
383
|
}
|
350
384
|
catch (e) {
|
351
|
-
console.warn('Failed to start instance', e);
|
385
|
+
console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e.message);
|
352
386
|
const logs = [
|
353
387
|
{
|
354
388
|
source: 'stdout',
|
@@ -359,11 +393,12 @@ export class InstanceManager {
|
|
359
393
|
];
|
360
394
|
const out = await this.saveInternalInstance({
|
361
395
|
...instance,
|
362
|
-
type: InstanceType.
|
396
|
+
type: InstanceType.UNKNOWN,
|
363
397
|
pid: null,
|
364
398
|
health: null,
|
365
399
|
portType: DEFAULT_HEALTH_PORT_TYPE,
|
366
400
|
status: InstanceStatus.FAILED,
|
401
|
+
errorMessage: e.message ?? 'Failed to start - Check logs for details.',
|
367
402
|
});
|
368
403
|
this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, logs[0]);
|
369
404
|
this.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
|
@@ -375,15 +410,16 @@ export class InstanceManager {
|
|
375
410
|
}
|
376
411
|
});
|
377
412
|
}
|
378
|
-
|
413
|
+
/**
|
414
|
+
* Stops an instance but does not remove it from the list of active instances
|
415
|
+
*
|
416
|
+
* It will be started again next time the system checks the status of the instance
|
417
|
+
*
|
418
|
+
* We do it this way to not cause the user to wait for the instance to start again
|
419
|
+
*/
|
420
|
+
async prepareForRestart(systemId, instanceId) {
|
379
421
|
systemId = normalizeKapetaUri(systemId);
|
380
422
|
await this.stopInner(systemId, instanceId);
|
381
|
-
const existingInstance = this.getInstance(systemId, instanceId);
|
382
|
-
if (existingInstance?.desiredStatus === DesiredInstanceStatus.STOP) {
|
383
|
-
// Internal instance was marked as stopped - abort restart
|
384
|
-
return existingInstance;
|
385
|
-
}
|
386
|
-
return this.start(systemId, instanceId);
|
387
423
|
}
|
388
424
|
async stopAll() {
|
389
425
|
return this.stopInstances(this._instances);
|
@@ -441,8 +477,7 @@ export class InstanceManager {
|
|
441
477
|
const oldStatus = instance.status;
|
442
478
|
const skipUpdate = (newStatus === InstanceStatus.STOPPED && instance.status === InstanceStatus.FAILED) ||
|
443
479
|
([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
|
444
|
-
instance.status === InstanceStatus.STOPPING
|
445
|
-
instance.desiredStatus === DesiredInstanceStatus.STOP) ||
|
480
|
+
instance.status === InstanceStatus.STOPPING) ||
|
446
481
|
(newStatus === InstanceStatus.STOPPED &&
|
447
482
|
instance.status === InstanceStatus.STARTING &&
|
448
483
|
instance.desiredStatus === DesiredInstanceStatus.RUN);
|
@@ -482,7 +517,7 @@ export class InstanceManager {
|
|
482
517
|
//If the instance is unhealthy, try to restart it
|
483
518
|
console.log('Restarting unhealthy instance', instance);
|
484
519
|
try {
|
485
|
-
await this.
|
520
|
+
await this.prepareForRestart(instance.systemId, instance.instanceId);
|
486
521
|
}
|
487
522
|
catch (e) {
|
488
523
|
console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
|
@@ -117,8 +117,10 @@ router.put('/', async (req, res) => {
|
|
117
117
|
const oldInstance = instanceManager.getInstance(req.kapeta.systemId, req.kapeta.instanceId);
|
118
118
|
if (oldInstance) {
|
119
119
|
instance.pid = oldInstance.pid;
|
120
|
+
instance.desiredStatus = oldInstance.desiredStatus;
|
120
121
|
}
|
121
122
|
instance.type = InstanceType.DOCKER;
|
123
|
+
instance.owner = InstanceOwner.INTERNAL;
|
122
124
|
}
|
123
125
|
else {
|
124
126
|
// Coming from user starting the instance outside of kapeta
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { ContainerInfo } from './containerManager';
|
2
2
|
import { EnvironmentType, OperatorInfo } from './types';
|
3
|
+
export declare const KIND_OPERATOR = "core/resource-type-operator";
|
3
4
|
declare class Operator {
|
4
5
|
private _data;
|
5
6
|
constructor(data: any);
|
@@ -8,6 +9,7 @@ declare class Operator {
|
|
8
9
|
}
|
9
10
|
declare class OperatorManager {
|
10
11
|
private _mountDir;
|
12
|
+
private operatorLock;
|
11
13
|
constructor();
|
12
14
|
_getMountPoint(operatorType: string, mountName: string): string;
|
13
15
|
/**
|
@@ -8,7 +8,8 @@ import FSExtra from 'fs-extra';
|
|
8
8
|
import { definitionsManager } from './definitionsManager';
|
9
9
|
import { getBindHost, normalizeKapetaUri } from './utils/utils';
|
10
10
|
import _ from 'lodash';
|
11
|
-
|
11
|
+
import AsyncLock from 'async-lock';
|
12
|
+
export const KIND_OPERATOR = 'core/resource-type-operator';
|
12
13
|
class Operator {
|
13
14
|
_data;
|
14
15
|
constructor(data) {
|
@@ -23,6 +24,7 @@ class Operator {
|
|
23
24
|
}
|
24
25
|
class OperatorManager {
|
25
26
|
_mountDir;
|
27
|
+
operatorLock = new AsyncLock();
|
26
28
|
constructor() {
|
27
29
|
this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
|
28
30
|
FSExtra.mkdirpSync(this._mountDir);
|
@@ -107,74 +109,75 @@ class OperatorManager {
|
|
107
109
|
* @return {Promise<ContainerInfo>}
|
108
110
|
*/
|
109
111
|
async ensureResource(systemId, resourceType, version) {
|
110
|
-
|
111
|
-
const
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
containerPortInfo =
|
121
|
-
|
122
|
-
|
123
|
-
|
112
|
+
systemId = normalizeKapetaUri(systemId);
|
113
|
+
const key = `${systemId}#${resourceType}:${version}`;
|
114
|
+
return await this.operatorLock.acquire(key, async () => {
|
115
|
+
const operator = this.getOperator(resourceType, version);
|
116
|
+
const operatorData = operator.getData();
|
117
|
+
const portTypes = Object.keys(operatorData.ports);
|
118
|
+
portTypes.sort();
|
119
|
+
const ports = {};
|
120
|
+
for (let i = 0; i < portTypes.length; i++) {
|
121
|
+
const portType = portTypes[i];
|
122
|
+
let containerPortInfo = operatorData.ports[portType];
|
123
|
+
const hostPort = await serviceManager.ensureServicePort(systemId, resourceType, portType);
|
124
|
+
if (typeof containerPortInfo === 'number' || typeof containerPortInfo === 'string') {
|
125
|
+
containerPortInfo = { port: containerPortInfo, type: 'tcp' };
|
126
|
+
}
|
127
|
+
if (!containerPortInfo.type) {
|
128
|
+
containerPortInfo.type = 'tcp';
|
129
|
+
}
|
130
|
+
const portId = containerPortInfo.port + '/' + containerPortInfo.type;
|
131
|
+
ports[portId] = {
|
132
|
+
type: portType,
|
133
|
+
hostPort,
|
134
|
+
};
|
124
135
|
}
|
125
|
-
const
|
126
|
-
|
127
|
-
|
128
|
-
|
136
|
+
const mounts = await containerManager.createMounts(systemId, resourceType, operatorData.mounts);
|
137
|
+
const nameParts = [systemId, resourceType.toLowerCase(), version];
|
138
|
+
const containerName = `kapeta-resource-${md5(nameParts.join('_'))}`;
|
139
|
+
const PortBindings = {};
|
140
|
+
const Env = [];
|
141
|
+
const Labels = {
|
142
|
+
kapeta: 'true',
|
129
143
|
};
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
144
|
+
const bindHost = getBindHost();
|
145
|
+
const ExposedPorts = {};
|
146
|
+
_.forEach(ports, (portInfo, containerPort) => {
|
147
|
+
ExposedPorts['' + containerPort] = {};
|
148
|
+
PortBindings['' + containerPort] = [
|
149
|
+
{
|
150
|
+
HostPort: '' + portInfo.hostPort,
|
151
|
+
HostIp: bindHost,
|
152
|
+
},
|
153
|
+
];
|
154
|
+
Labels[CONTAINER_LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
|
155
|
+
});
|
156
|
+
const Mounts = containerManager.toDockerMounts(mounts);
|
157
|
+
_.forEach(operatorData.env, (value, name) => {
|
158
|
+
Env.push(name + '=' + value);
|
159
|
+
});
|
160
|
+
let HealthCheck = undefined;
|
161
|
+
if (operatorData.health) {
|
162
|
+
HealthCheck = containerManager.toDockerHealth(operatorData.health);
|
163
|
+
}
|
164
|
+
const container = await containerManager.ensureContainer({
|
165
|
+
name: containerName,
|
166
|
+
Image: operatorData.image,
|
167
|
+
Hostname: containerName + '.kapeta',
|
168
|
+
Labels,
|
169
|
+
Cmd: operatorData.cmd,
|
170
|
+
ExposedPorts,
|
171
|
+
Env,
|
172
|
+
HealthCheck,
|
173
|
+
HostConfig: {
|
174
|
+
PortBindings,
|
175
|
+
Mounts,
|
151
176
|
},
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
const Mounts = containerManager.toDockerMounts(mounts);
|
156
|
-
_.forEach(operatorData.env, (value, name) => {
|
157
|
-
Env.push(name + '=' + value);
|
158
|
-
});
|
159
|
-
let HealthCheck = undefined;
|
160
|
-
if (operatorData.health) {
|
161
|
-
HealthCheck = containerManager.toDockerHealth(operatorData.health);
|
162
|
-
}
|
163
|
-
const container = await containerManager.ensureContainer({
|
164
|
-
name: containerName,
|
165
|
-
Image: operatorData.image,
|
166
|
-
Hostname: containerName + '.kapeta',
|
167
|
-
Labels,
|
168
|
-
Cmd: operatorData.cmd,
|
169
|
-
ExposedPorts,
|
170
|
-
Env,
|
171
|
-
HealthCheck,
|
172
|
-
HostConfig: {
|
173
|
-
PortBindings,
|
174
|
-
Mounts,
|
175
|
-
},
|
177
|
+
});
|
178
|
+
await containerManager.waitForReady(container);
|
179
|
+
return new ContainerInfo(container);
|
176
180
|
});
|
177
|
-
return new ContainerInfo(container);
|
178
181
|
}
|
179
182
|
}
|
180
183
|
export const operatorManager = new OperatorManager();
|
@@ -7,6 +7,7 @@ export declare class SocketManager {
|
|
7
7
|
isAlive(): boolean;
|
8
8
|
private get io();
|
9
9
|
emit(context: string, type: string, payload: any): void;
|
10
|
+
emitGlobal(type: string, payload: any): void;
|
10
11
|
_bindIO(): void;
|
11
12
|
_handleSocketCreated(socket: Socket): void;
|
12
13
|
_bindSocket(socket: Socket): void;
|
@@ -23,6 +23,9 @@ export class SocketManager {
|
|
23
23
|
emit(context, type, payload) {
|
24
24
|
this.io.to(context).emit(type, { context, payload });
|
25
25
|
}
|
26
|
+
emitGlobal(type, payload) {
|
27
|
+
this.io.emit(type, { payload });
|
28
|
+
}
|
26
29
|
_bindIO() {
|
27
30
|
this.io.on('connection', (socket) => this._handleSocketCreated(socket));
|
28
31
|
}
|
package/dist/esm/src/types.d.ts
CHANGED
@@ -194,7 +194,7 @@ export class BlockInstanceRunner {
|
|
194
194
|
`KAPETA_LOCAL_CLUSTER_PORT=${clusterService.getClusterServicePort()}`,
|
195
195
|
...Object.entries({
|
196
196
|
...env,
|
197
|
-
...addonEnv
|
197
|
+
...addonEnv,
|
198
198
|
}).map(([key, value]) => `${key}=${value}`),
|
199
199
|
],
|
200
200
|
HostConfig: {
|
@@ -318,13 +318,12 @@ export class BlockInstanceRunner {
|
|
318
318
|
}
|
319
319
|
async ensureContainer(opts) {
|
320
320
|
const container = await containerManager.ensureContainer(opts);
|
321
|
-
await containerManager.waitForReady(container);
|
322
321
|
return this._handleContainer(container);
|
323
322
|
}
|
324
323
|
async _handleContainer(container) {
|
325
324
|
return {
|
326
325
|
type: InstanceType.DOCKER,
|
327
|
-
pid: container.id
|
326
|
+
pid: container.id,
|
328
327
|
};
|
329
328
|
}
|
330
329
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import FS from 'node:fs';
|
2
2
|
import YAML from 'yaml';
|
3
3
|
import { parseKapetaUri } from '@kapeta/nodejs-utils';
|
4
|
-
import md5 from
|
4
|
+
import md5 from 'md5';
|
5
5
|
export function getBlockInstanceContainerName(systemId, instanceId) {
|
6
6
|
return `kapeta-block-instance-${md5(systemId + instanceId)}`;
|
7
7
|
}
|
package/package.json
CHANGED
package/src/config/routes.ts
CHANGED
@@ -41,7 +41,7 @@ router.put('/instance', async (req: KapetaBodyRequest, res) => {
|
|
41
41
|
if (req.kapeta!.instanceId) {
|
42
42
|
configManager.setConfigForSection(req.kapeta!.systemId, req.kapeta!.instanceId, config);
|
43
43
|
//Restart the instance if it is running after config change
|
44
|
-
await instanceManager.
|
44
|
+
await instanceManager.prepareForRestart(req.kapeta!.systemId, req.kapeta!.instanceId);
|
45
45
|
} else {
|
46
46
|
configManager.setConfigForSystem(req.kapeta!.systemId, config);
|
47
47
|
}
|