@kapeta/local-cluster-service 0.0.0-96f91ef
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/.eslintrc.cjs +25 -0
- package/.github/workflows/check-license.yml +17 -0
- package/.github/workflows/main.yml +26 -0
- package/.prettierignore +4 -0
- package/.vscode/launch.json +19 -0
- package/CHANGELOG.md +920 -0
- package/LICENSE +38 -0
- package/README.md +36 -0
- package/definitions.d.ts +35 -0
- package/dist/cjs/index.d.ts +34 -0
- package/dist/cjs/index.js +263 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/src/RepositoryWatcher.d.ts +30 -0
- package/dist/cjs/src/RepositoryWatcher.js +332 -0
- package/dist/cjs/src/ai/aiClient.d.ts +20 -0
- package/dist/cjs/src/ai/aiClient.js +74 -0
- package/dist/cjs/src/ai/routes.d.ts +7 -0
- package/dist/cjs/src/ai/routes.js +37 -0
- package/dist/cjs/src/ai/transform.d.ts +11 -0
- package/dist/cjs/src/ai/transform.js +239 -0
- package/dist/cjs/src/ai/types.d.ts +40 -0
- package/dist/cjs/src/ai/types.js +2 -0
- package/dist/cjs/src/api.d.ts +7 -0
- package/dist/cjs/src/api.js +29 -0
- package/dist/cjs/src/assetManager.d.ts +41 -0
- package/dist/cjs/src/assetManager.js +274 -0
- package/dist/cjs/src/assets/routes.d.ts +7 -0
- package/dist/cjs/src/assets/routes.js +165 -0
- package/dist/cjs/src/attachments/routes.d.ts +7 -0
- package/dist/cjs/src/attachments/routes.js +72 -0
- package/dist/cjs/src/authManager.d.ts +16 -0
- package/dist/cjs/src/authManager.js +64 -0
- package/dist/cjs/src/cacheManager.d.ts +20 -0
- package/dist/cjs/src/cacheManager.js +51 -0
- package/dist/cjs/src/clusterService.d.ts +44 -0
- package/dist/cjs/src/clusterService.js +120 -0
- package/dist/cjs/src/codeGeneratorManager.d.ts +14 -0
- package/dist/cjs/src/codeGeneratorManager.js +93 -0
- package/dist/cjs/src/config/routes.d.ts +7 -0
- package/dist/cjs/src/config/routes.js +160 -0
- package/dist/cjs/src/configManager.d.ts +42 -0
- package/dist/cjs/src/configManager.js +136 -0
- package/dist/cjs/src/containerManager.d.ts +148 -0
- package/dist/cjs/src/containerManager.js +958 -0
- package/dist/cjs/src/definitionsManager.d.ts +20 -0
- package/dist/cjs/src/definitionsManager.js +171 -0
- package/dist/cjs/src/filesystem/routes.d.ts +7 -0
- package/dist/cjs/src/filesystem/routes.js +105 -0
- package/dist/cjs/src/filesystemManager.d.ts +27 -0
- package/dist/cjs/src/filesystemManager.js +118 -0
- package/dist/cjs/src/identities/routes.d.ts +7 -0
- package/dist/cjs/src/identities/routes.js +37 -0
- package/dist/cjs/src/instanceManager.d.ts +69 -0
- package/dist/cjs/src/instanceManager.js +910 -0
- package/dist/cjs/src/instances/routes.d.ts +7 -0
- package/dist/cjs/src/instances/routes.js +179 -0
- package/dist/cjs/src/middleware/cors.d.ts +6 -0
- package/dist/cjs/src/middleware/cors.js +14 -0
- package/dist/cjs/src/middleware/kapeta.d.ts +15 -0
- package/dist/cjs/src/middleware/kapeta.js +28 -0
- package/dist/cjs/src/middleware/stringBody.d.ts +9 -0
- package/dist/cjs/src/middleware/stringBody.js +18 -0
- package/dist/cjs/src/networkManager.d.ts +37 -0
- package/dist/cjs/src/networkManager.js +119 -0
- package/dist/cjs/src/operatorManager.d.ts +41 -0
- package/dist/cjs/src/operatorManager.js +211 -0
- package/dist/cjs/src/progressListener.d.ts +31 -0
- package/dist/cjs/src/progressListener.js +133 -0
- package/dist/cjs/src/providerManager.d.ts +11 -0
- package/dist/cjs/src/providerManager.js +84 -0
- package/dist/cjs/src/providers/routes.d.ts +7 -0
- package/dist/cjs/src/providers/routes.js +46 -0
- package/dist/cjs/src/proxy/routes.d.ts +7 -0
- package/dist/cjs/src/proxy/routes.js +115 -0
- package/dist/cjs/src/proxy/types/rest.d.ts +10 -0
- package/dist/cjs/src/proxy/types/rest.js +123 -0
- package/dist/cjs/src/proxy/types/web.d.ts +8 -0
- package/dist/cjs/src/proxy/types/web.js +61 -0
- package/dist/cjs/src/repositoryManager.d.ts +35 -0
- package/dist/cjs/src/repositoryManager.js +247 -0
- package/dist/cjs/src/serviceManager.d.ts +36 -0
- package/dist/cjs/src/serviceManager.js +106 -0
- package/dist/cjs/src/socketManager.d.ts +32 -0
- package/dist/cjs/src/socketManager.js +125 -0
- package/dist/cjs/src/storageService.d.ts +21 -0
- package/dist/cjs/src/storageService.js +81 -0
- package/dist/cjs/src/taskManager.d.ts +70 -0
- package/dist/cjs/src/taskManager.js +181 -0
- package/dist/cjs/src/tasks/routes.d.ts +7 -0
- package/dist/cjs/src/tasks/routes.js +39 -0
- package/dist/cjs/src/traffic/routes.d.ts +7 -0
- package/dist/cjs/src/traffic/routes.js +22 -0
- package/dist/cjs/src/types.d.ts +99 -0
- package/dist/cjs/src/types.js +39 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +28 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +432 -0
- package/dist/cjs/src/utils/DefaultProviderInstaller.d.ts +15 -0
- package/dist/cjs/src/utils/DefaultProviderInstaller.js +136 -0
- package/dist/cjs/src/utils/InternalConfigProvider.d.ts +38 -0
- package/dist/cjs/src/utils/InternalConfigProvider.js +146 -0
- package/dist/cjs/src/utils/LogData.d.ts +23 -0
- package/dist/cjs/src/utils/LogData.js +46 -0
- package/dist/cjs/src/utils/commandLineUtils.d.ts +8 -0
- package/dist/cjs/src/utils/commandLineUtils.js +39 -0
- package/dist/cjs/src/utils/pathTemplateParser.d.ts +30 -0
- package/dist/cjs/src/utils/pathTemplateParser.js +135 -0
- package/dist/cjs/src/utils/utils.d.ts +40 -0
- package/dist/cjs/src/utils/utils.js +148 -0
- package/dist/cjs/start.d.ts +5 -0
- package/dist/cjs/start.js +17 -0
- package/dist/cjs/test/proxy/types/rest.test.d.ts +5 -0
- package/dist/cjs/test/proxy/types/rest.test.js +48 -0
- package/dist/cjs/test/utils/pathTemplateParser.test.d.ts +5 -0
- package/dist/cjs/test/utils/pathTemplateParser.test.js +27 -0
- package/dist/esm/index.d.ts +34 -0
- package/dist/esm/index.js +263 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/src/RepositoryWatcher.d.ts +30 -0
- package/dist/esm/src/RepositoryWatcher.js +332 -0
- package/dist/esm/src/ai/aiClient.d.ts +20 -0
- package/dist/esm/src/ai/aiClient.js +74 -0
- package/dist/esm/src/ai/routes.d.ts +7 -0
- package/dist/esm/src/ai/routes.js +37 -0
- package/dist/esm/src/ai/transform.d.ts +11 -0
- package/dist/esm/src/ai/transform.js +239 -0
- package/dist/esm/src/ai/types.d.ts +40 -0
- package/dist/esm/src/ai/types.js +2 -0
- package/dist/esm/src/api.d.ts +7 -0
- package/dist/esm/src/api.js +29 -0
- package/dist/esm/src/assetManager.d.ts +41 -0
- package/dist/esm/src/assetManager.js +274 -0
- package/dist/esm/src/assets/routes.d.ts +7 -0
- package/dist/esm/src/assets/routes.js +165 -0
- package/dist/esm/src/attachments/routes.d.ts +7 -0
- package/dist/esm/src/attachments/routes.js +72 -0
- package/dist/esm/src/authManager.d.ts +16 -0
- package/dist/esm/src/authManager.js +64 -0
- package/dist/esm/src/cacheManager.d.ts +20 -0
- package/dist/esm/src/cacheManager.js +51 -0
- package/dist/esm/src/clusterService.d.ts +44 -0
- package/dist/esm/src/clusterService.js +120 -0
- package/dist/esm/src/codeGeneratorManager.d.ts +14 -0
- package/dist/esm/src/codeGeneratorManager.js +93 -0
- package/dist/esm/src/config/routes.d.ts +7 -0
- package/dist/esm/src/config/routes.js +160 -0
- package/dist/esm/src/configManager.d.ts +42 -0
- package/dist/esm/src/configManager.js +136 -0
- package/dist/esm/src/containerManager.d.ts +148 -0
- package/dist/esm/src/containerManager.js +958 -0
- package/dist/esm/src/definitionsManager.d.ts +20 -0
- package/dist/esm/src/definitionsManager.js +171 -0
- package/dist/esm/src/filesystem/routes.d.ts +7 -0
- package/dist/esm/src/filesystem/routes.js +105 -0
- package/dist/esm/src/filesystemManager.d.ts +27 -0
- package/dist/esm/src/filesystemManager.js +118 -0
- package/dist/esm/src/identities/routes.d.ts +7 -0
- package/dist/esm/src/identities/routes.js +37 -0
- package/dist/esm/src/instanceManager.d.ts +69 -0
- package/dist/esm/src/instanceManager.js +910 -0
- package/dist/esm/src/instances/routes.d.ts +7 -0
- package/dist/esm/src/instances/routes.js +179 -0
- package/dist/esm/src/middleware/cors.d.ts +6 -0
- package/dist/esm/src/middleware/cors.js +14 -0
- package/dist/esm/src/middleware/kapeta.d.ts +15 -0
- package/dist/esm/src/middleware/kapeta.js +28 -0
- package/dist/esm/src/middleware/stringBody.d.ts +9 -0
- package/dist/esm/src/middleware/stringBody.js +18 -0
- package/dist/esm/src/networkManager.d.ts +37 -0
- package/dist/esm/src/networkManager.js +119 -0
- package/dist/esm/src/operatorManager.d.ts +41 -0
- package/dist/esm/src/operatorManager.js +211 -0
- package/dist/esm/src/progressListener.d.ts +31 -0
- package/dist/esm/src/progressListener.js +133 -0
- package/dist/esm/src/providerManager.d.ts +11 -0
- package/dist/esm/src/providerManager.js +84 -0
- package/dist/esm/src/providers/routes.d.ts +7 -0
- package/dist/esm/src/providers/routes.js +46 -0
- package/dist/esm/src/proxy/routes.d.ts +7 -0
- package/dist/esm/src/proxy/routes.js +115 -0
- package/dist/esm/src/proxy/types/rest.d.ts +10 -0
- package/dist/esm/src/proxy/types/rest.js +123 -0
- package/dist/esm/src/proxy/types/web.d.ts +8 -0
- package/dist/esm/src/proxy/types/web.js +61 -0
- package/dist/esm/src/repositoryManager.d.ts +35 -0
- package/dist/esm/src/repositoryManager.js +247 -0
- package/dist/esm/src/serviceManager.d.ts +36 -0
- package/dist/esm/src/serviceManager.js +106 -0
- package/dist/esm/src/socketManager.d.ts +32 -0
- package/dist/esm/src/socketManager.js +125 -0
- package/dist/esm/src/storageService.d.ts +21 -0
- package/dist/esm/src/storageService.js +81 -0
- package/dist/esm/src/taskManager.d.ts +70 -0
- package/dist/esm/src/taskManager.js +181 -0
- package/dist/esm/src/tasks/routes.d.ts +7 -0
- package/dist/esm/src/tasks/routes.js +39 -0
- package/dist/esm/src/traffic/routes.d.ts +7 -0
- package/dist/esm/src/traffic/routes.js +22 -0
- package/dist/esm/src/types.d.ts +99 -0
- package/dist/esm/src/types.js +39 -0
- package/dist/esm/src/utils/BlockInstanceRunner.d.ts +28 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +432 -0
- package/dist/esm/src/utils/DefaultProviderInstaller.d.ts +15 -0
- package/dist/esm/src/utils/DefaultProviderInstaller.js +136 -0
- package/dist/esm/src/utils/InternalConfigProvider.d.ts +38 -0
- package/dist/esm/src/utils/InternalConfigProvider.js +146 -0
- package/dist/esm/src/utils/LogData.d.ts +23 -0
- package/dist/esm/src/utils/LogData.js +46 -0
- package/dist/esm/src/utils/commandLineUtils.d.ts +8 -0
- package/dist/esm/src/utils/commandLineUtils.js +39 -0
- package/dist/esm/src/utils/pathTemplateParser.d.ts +30 -0
- package/dist/esm/src/utils/pathTemplateParser.js +135 -0
- package/dist/esm/src/utils/utils.d.ts +40 -0
- package/dist/esm/src/utils/utils.js +148 -0
- package/dist/esm/start.d.ts +5 -0
- package/dist/esm/start.js +17 -0
- package/dist/esm/test/proxy/types/rest.test.d.ts +5 -0
- package/dist/esm/test/proxy/types/rest.test.js +48 -0
- package/dist/esm/test/utils/pathTemplateParser.test.d.ts +5 -0
- package/dist/esm/test/utils/pathTemplateParser.test.js +27 -0
- package/index.ts +280 -0
- package/jest.config.js +8 -0
- package/package.json +134 -0
- package/src/RepositoryWatcher.ts +363 -0
- package/src/ai/aiClient.ts +93 -0
- package/src/ai/routes.ts +39 -0
- package/src/ai/transform.ts +275 -0
- package/src/ai/types.ts +45 -0
- package/src/api.ts +32 -0
- package/src/assetManager.ts +355 -0
- package/src/assets/routes.ts +183 -0
- package/src/attachments/routes.ts +79 -0
- package/src/authManager.ts +67 -0
- package/src/cacheManager.ts +59 -0
- package/src/clusterService.ts +142 -0
- package/src/codeGeneratorManager.ts +109 -0
- package/src/config/routes.ts +201 -0
- package/src/configManager.ts +180 -0
- package/src/containerManager.ts +1178 -0
- package/src/definitionsManager.ts +212 -0
- package/src/filesystem/routes.ts +123 -0
- package/src/filesystemManager.ts +133 -0
- package/src/identities/routes.ts +38 -0
- package/src/instanceManager.ts +1160 -0
- package/src/instances/routes.ts +203 -0
- package/src/middleware/cors.ts +14 -0
- package/src/middleware/kapeta.ts +41 -0
- package/src/middleware/stringBody.ts +21 -0
- package/src/networkManager.ts +148 -0
- package/src/operatorManager.ts +294 -0
- package/src/progressListener.ts +151 -0
- package/src/providerManager.ts +97 -0
- package/src/providers/routes.ts +51 -0
- package/src/proxy/routes.ts +153 -0
- package/src/proxy/types/rest.ts +172 -0
- package/src/proxy/types/web.ts +70 -0
- package/src/repositoryManager.ts +291 -0
- package/src/serviceManager.ts +133 -0
- package/src/socketManager.ts +138 -0
- package/src/storageService.ts +97 -0
- package/src/taskManager.ts +247 -0
- package/src/tasks/routes.ts +43 -0
- package/src/traffic/routes.ts +23 -0
- package/src/types.ts +112 -0
- package/src/utils/BlockInstanceRunner.ts +577 -0
- package/src/utils/DefaultProviderInstaller.ts +150 -0
- package/src/utils/InternalConfigProvider.ts +214 -0
- package/src/utils/LogData.ts +50 -0
- package/src/utils/commandLineUtils.ts +45 -0
- package/src/utils/pathTemplateParser.ts +157 -0
- package/src/utils/utils.ts +155 -0
- package/start.ts +14 -0
- package/test/proxy/types/rest.test.ts +54 -0
- package/test/utils/pathTemplateParser.test.ts +29 -0
- package/tsconfig.json +15 -0
@@ -0,0 +1,1160 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright 2023 Kapeta Inc.
|
3
|
+
* SPDX-License-Identifier: BUSL-1.1
|
4
|
+
*/
|
5
|
+
|
6
|
+
import _ from 'lodash';
|
7
|
+
import request from 'request';
|
8
|
+
import AsyncLock from 'async-lock';
|
9
|
+
import { BlockInstanceRunner } from './utils/BlockInstanceRunner';
|
10
|
+
import { storageService } from './storageService';
|
11
|
+
import { EVENT_INSTANCE_CREATED, EVENT_STATUS_CHANGED, socketManager } from './socketManager';
|
12
|
+
import { serviceManager } from './serviceManager';
|
13
|
+
import { assetManager, EnrichedAsset } from './assetManager';
|
14
|
+
import {
|
15
|
+
containerManager,
|
16
|
+
DockerContainerHealth,
|
17
|
+
DockerContainerStatus,
|
18
|
+
HEALTH_CHECK_TIMEOUT,
|
19
|
+
} from './containerManager';
|
20
|
+
import { configManager } from './configManager';
|
21
|
+
import {
|
22
|
+
DesiredInstanceStatus,
|
23
|
+
EnvironmentType,
|
24
|
+
InstanceInfo,
|
25
|
+
InstanceOwner,
|
26
|
+
InstanceStatus,
|
27
|
+
InstanceType,
|
28
|
+
KIND_BLOCK_TYPE_EXECUTABLE,
|
29
|
+
KIND_BLOCK_TYPE_OPERATOR,
|
30
|
+
KIND_RESOURCE_OPERATOR,
|
31
|
+
LogEntry,
|
32
|
+
} from './types';
|
33
|
+
import { BlockDefinitionSpec, LocalInstance, Plan } from '@kapeta/schemas';
|
34
|
+
import {
|
35
|
+
getBlockInstanceContainerName,
|
36
|
+
getOperatorInstancePorts,
|
37
|
+
getRemoteHostForEnvironment,
|
38
|
+
getResolvedConfiguration,
|
39
|
+
} from './utils/utils';
|
40
|
+
import { operatorManager } from './operatorManager';
|
41
|
+
import { normalizeKapetaUri, parseKapetaUri } from '@kapeta/nodejs-utils';
|
42
|
+
import { definitionsManager } from './definitionsManager';
|
43
|
+
import { Task, taskManager } from './taskManager';
|
44
|
+
import { InstanceOperator, InstanceOperatorPort } from '@kapeta/sdk-config';
|
45
|
+
|
46
|
+
const CHECK_INTERVAL = 5000;
|
47
|
+
const DEFAULT_HEALTH_PORT_TYPE = 'http';
|
48
|
+
|
49
|
+
const MIN_TIME_RUNNING = 30000; //If something didnt run for more than 30 secs - it failed
|
50
|
+
|
51
|
+
export class InstanceManager {
|
52
|
+
private _interval: any = undefined;
|
53
|
+
|
54
|
+
private readonly _instances: InstanceInfo[] = [];
|
55
|
+
|
56
|
+
private readonly instanceLocks: AsyncLock = new AsyncLock();
|
57
|
+
|
58
|
+
constructor() {
|
59
|
+
this._instances = storageService.section('instances', []);
|
60
|
+
|
61
|
+
// We need to wait a bit before running the first check
|
62
|
+
this.checkInstancesLater(1000);
|
63
|
+
}
|
64
|
+
|
65
|
+
private checkInstancesLater(time = CHECK_INTERVAL) {
|
66
|
+
if (this._interval) {
|
67
|
+
clearTimeout(this._interval);
|
68
|
+
}
|
69
|
+
|
70
|
+
this._interval = setTimeout(async () => {
|
71
|
+
await this.checkInstances();
|
72
|
+
this.checkInstancesLater();
|
73
|
+
}, time);
|
74
|
+
}
|
75
|
+
|
76
|
+
public getInstances() {
|
77
|
+
if (!this._instances) {
|
78
|
+
return [];
|
79
|
+
}
|
80
|
+
|
81
|
+
return [...this._instances];
|
82
|
+
}
|
83
|
+
|
84
|
+
public async getInstancesForPlan(systemId: string) {
|
85
|
+
if (!this._instances) {
|
86
|
+
return [];
|
87
|
+
}
|
88
|
+
|
89
|
+
systemId = normalizeKapetaUri(systemId);
|
90
|
+
|
91
|
+
const planInfo = await definitionsManager.getDefinition(systemId);
|
92
|
+
|
93
|
+
if (!planInfo) {
|
94
|
+
return [];
|
95
|
+
}
|
96
|
+
|
97
|
+
const plan = planInfo.definition as Plan;
|
98
|
+
if (!plan?.spec?.blocks) {
|
99
|
+
return [];
|
100
|
+
}
|
101
|
+
|
102
|
+
const instanceIds = plan.spec?.blocks?.map((block) => block.id) || [];
|
103
|
+
|
104
|
+
return this._instances.filter(
|
105
|
+
(instance) => instance.systemId === systemId && instanceIds.includes(instance.instanceId)
|
106
|
+
);
|
107
|
+
}
|
108
|
+
|
109
|
+
public getInstance(systemId: string, instanceId: string) {
|
110
|
+
systemId = normalizeKapetaUri(systemId);
|
111
|
+
|
112
|
+
return this._instances.find((i) => i.systemId === systemId && i.instanceId === instanceId);
|
113
|
+
}
|
114
|
+
|
115
|
+
private async exclusive<T = any>(systemId: string, instanceId: string, fn: () => Promise<T>) {
|
116
|
+
systemId = normalizeKapetaUri(systemId);
|
117
|
+
const key = `${systemId}/${instanceId}`;
|
118
|
+
//console.log(`Acquiring lock for ${key}`, this.instanceLocks.isBusy(key));
|
119
|
+
const result = await this.instanceLocks.acquire(key, fn);
|
120
|
+
//console.log(`Releasing lock for ${key}`, this.instanceLocks.isBusy(key));
|
121
|
+
return result;
|
122
|
+
}
|
123
|
+
|
124
|
+
private isLocked(systemId: string, instanceId: string) {
|
125
|
+
return this.instanceLocks.isBusy(`${systemId}/${instanceId}`);
|
126
|
+
}
|
127
|
+
|
128
|
+
public async getLogs(systemId: string, instanceId: string): Promise<LogEntry[]> {
|
129
|
+
const instance = this.getInstance(systemId, instanceId);
|
130
|
+
if (!instance) {
|
131
|
+
throw new Error(`Instance ${systemId}/${instanceId} not found`);
|
132
|
+
}
|
133
|
+
|
134
|
+
switch (instance.type) {
|
135
|
+
case InstanceType.DOCKER:
|
136
|
+
return await containerManager.getLogs(instance);
|
137
|
+
|
138
|
+
case InstanceType.UNKNOWN:
|
139
|
+
return [
|
140
|
+
{
|
141
|
+
level: 'INFO',
|
142
|
+
message: 'Instance is starting...',
|
143
|
+
time: Date.now(),
|
144
|
+
source: 'stdout',
|
145
|
+
},
|
146
|
+
];
|
147
|
+
|
148
|
+
case InstanceType.LOCAL:
|
149
|
+
return [
|
150
|
+
{
|
151
|
+
level: 'INFO',
|
152
|
+
message: 'Instance started outside Kapeta - logs not available...',
|
153
|
+
time: Date.now(),
|
154
|
+
source: 'stdout',
|
155
|
+
},
|
156
|
+
];
|
157
|
+
}
|
158
|
+
|
159
|
+
return [];
|
160
|
+
}
|
161
|
+
|
162
|
+
public async saveInternalInstance(instance: InstanceInfo) {
|
163
|
+
instance.systemId = normalizeKapetaUri(instance.systemId);
|
164
|
+
if (instance.ref) {
|
165
|
+
instance.ref = normalizeKapetaUri(instance.ref);
|
166
|
+
}
|
167
|
+
|
168
|
+
//Get target address
|
169
|
+
let address = await serviceManager.getProviderAddress(
|
170
|
+
instance.systemId,
|
171
|
+
instance.instanceId,
|
172
|
+
instance.portType ?? DEFAULT_HEALTH_PORT_TYPE
|
173
|
+
);
|
174
|
+
|
175
|
+
const healthUrl = this.getHealthUrl(instance, address);
|
176
|
+
|
177
|
+
instance.address = address;
|
178
|
+
if (healthUrl) {
|
179
|
+
instance.health = healthUrl;
|
180
|
+
}
|
181
|
+
|
182
|
+
let existingInstance = this.getInstance(instance.systemId, instance.instanceId);
|
183
|
+
if (existingInstance) {
|
184
|
+
const ix = this._instances.indexOf(existingInstance);
|
185
|
+
this._instances.splice(ix, 1, instance);
|
186
|
+
socketManager.emitSystemEvent(instance.systemId, EVENT_STATUS_CHANGED, instance);
|
187
|
+
} else {
|
188
|
+
this._instances.push(instance);
|
189
|
+
socketManager.emitSystemEvent(instance.systemId, EVENT_INSTANCE_CREATED, instance);
|
190
|
+
}
|
191
|
+
|
192
|
+
this.save();
|
193
|
+
|
194
|
+
return instance;
|
195
|
+
}
|
196
|
+
|
197
|
+
/**
|
198
|
+
* Method is called when instance is started from the Kapeta SDKs (e.g. NodeJS SDK)
|
199
|
+
* which self-registers with the cluster service locally on startup.
|
200
|
+
*/
|
201
|
+
public async registerInstanceFromSDK(
|
202
|
+
systemId: string,
|
203
|
+
instanceId: string,
|
204
|
+
info: Omit<InstanceInfo, 'systemId' | 'instanceId'>
|
205
|
+
) {
|
206
|
+
return this.exclusive(systemId, instanceId, async () => {
|
207
|
+
systemId = normalizeKapetaUri(systemId);
|
208
|
+
|
209
|
+
let instance = this.getInstance(systemId, instanceId);
|
210
|
+
|
211
|
+
//Get target address
|
212
|
+
const address = await serviceManager.getProviderAddress(
|
213
|
+
systemId,
|
214
|
+
instanceId,
|
215
|
+
info.portType ?? DEFAULT_HEALTH_PORT_TYPE
|
216
|
+
);
|
217
|
+
|
218
|
+
const healthUrl = this.getHealthUrl(info, address);
|
219
|
+
|
220
|
+
if (instance) {
|
221
|
+
if (
|
222
|
+
instance.status === InstanceStatus.STOPPING &&
|
223
|
+
instance.desiredStatus === DesiredInstanceStatus.STOP
|
224
|
+
) {
|
225
|
+
//If instance is stopping do not interfere
|
226
|
+
return;
|
227
|
+
}
|
228
|
+
|
229
|
+
if (info.owner === InstanceOwner.EXTERNAL) {
|
230
|
+
//If instance was started externally - then we want to replace the internal instance with that
|
231
|
+
if (
|
232
|
+
instance.owner === InstanceOwner.INTERNAL &&
|
233
|
+
(instance.status === InstanceStatus.READY ||
|
234
|
+
instance.status === InstanceStatus.STARTING ||
|
235
|
+
instance.status === InstanceStatus.UNHEALTHY)
|
236
|
+
) {
|
237
|
+
throw new Error(`Instance ${instanceId} is already running`);
|
238
|
+
}
|
239
|
+
|
240
|
+
instance.desiredStatus = info.desiredStatus;
|
241
|
+
instance.owner = info.owner;
|
242
|
+
instance.status = InstanceStatus.STARTING;
|
243
|
+
instance.startedAt = Date.now();
|
244
|
+
}
|
245
|
+
|
246
|
+
instance.pid = info.pid;
|
247
|
+
instance.address = address;
|
248
|
+
if (info.type) {
|
249
|
+
instance.type = info.type;
|
250
|
+
}
|
251
|
+
|
252
|
+
if (healthUrl) {
|
253
|
+
instance.health = healthUrl;
|
254
|
+
}
|
255
|
+
|
256
|
+
if (info.portType) {
|
257
|
+
instance.portType = info.portType;
|
258
|
+
}
|
259
|
+
|
260
|
+
socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
261
|
+
} else {
|
262
|
+
//If instance was not found - then we're receiving an externally started instance
|
263
|
+
instance = {
|
264
|
+
...info,
|
265
|
+
systemId,
|
266
|
+
instanceId,
|
267
|
+
status: InstanceStatus.STARTING,
|
268
|
+
startedAt: Date.now(),
|
269
|
+
desiredStatus: DesiredInstanceStatus.EXTERNAL,
|
270
|
+
owner: InstanceOwner.EXTERNAL,
|
271
|
+
health: healthUrl,
|
272
|
+
address,
|
273
|
+
};
|
274
|
+
|
275
|
+
this._instances.push(instance);
|
276
|
+
|
277
|
+
socketManager.emitSystemEvent(systemId, EVENT_INSTANCE_CREATED, instance);
|
278
|
+
}
|
279
|
+
|
280
|
+
this.save();
|
281
|
+
|
282
|
+
return instance;
|
283
|
+
});
|
284
|
+
}
|
285
|
+
|
286
|
+
private getHealthUrl(info: Omit<InstanceInfo, 'systemId' | 'instanceId'>, address: string) {
|
287
|
+
let healthUrl = null;
|
288
|
+
let health = info.health ?? '/.kapeta/health';
|
289
|
+
if (health) {
|
290
|
+
if (health.startsWith('/')) {
|
291
|
+
health = health.substring(1);
|
292
|
+
}
|
293
|
+
healthUrl = address + health;
|
294
|
+
}
|
295
|
+
return healthUrl;
|
296
|
+
}
|
297
|
+
|
298
|
+
public markAsStopped(systemId: string, instanceId: string) {
|
299
|
+
return this.exclusive(systemId, instanceId, async () => {
|
300
|
+
systemId = normalizeKapetaUri(systemId);
|
301
|
+
const instance = _.find(this._instances, { systemId, instanceId });
|
302
|
+
if (instance && instance.owner === InstanceOwner.EXTERNAL && instance.status !== InstanceStatus.STOPPED) {
|
303
|
+
if (instance.status != InstanceStatus.FAILED) {
|
304
|
+
instance.status = InstanceStatus.STOPPED;
|
305
|
+
}
|
306
|
+
instance.pid = null;
|
307
|
+
instance.health = null;
|
308
|
+
socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
309
|
+
this.save();
|
310
|
+
}
|
311
|
+
});
|
312
|
+
}
|
313
|
+
|
314
|
+
public async startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>> {
|
315
|
+
systemId = normalizeKapetaUri(systemId);
|
316
|
+
const plan = await assetManager.getPlan(systemId, true);
|
317
|
+
if (!plan) {
|
318
|
+
throw new Error(`Plan not found: ${systemId}`);
|
319
|
+
}
|
320
|
+
|
321
|
+
if (!plan.spec.blocks) {
|
322
|
+
throw new Error(`No blocks found in plan: ${systemId}`);
|
323
|
+
}
|
324
|
+
|
325
|
+
return taskManager.add(
|
326
|
+
`plan:start:${systemId}`,
|
327
|
+
async () => {
|
328
|
+
const promises: Promise<InstanceInfo>[] = [];
|
329
|
+
const errors = [];
|
330
|
+
const instanceIds = await this.getAllInstancesExceptKind(systemId, KIND_BLOCK_TYPE_EXECUTABLE);
|
331
|
+
for (const instanceId of instanceIds) {
|
332
|
+
try {
|
333
|
+
promises.push(
|
334
|
+
this.start(systemId, instanceId).then((taskOrInstance) => {
|
335
|
+
if (taskOrInstance instanceof Task) {
|
336
|
+
return taskOrInstance.wait();
|
337
|
+
}
|
338
|
+
return taskOrInstance;
|
339
|
+
})
|
340
|
+
);
|
341
|
+
} catch (e) {
|
342
|
+
errors.push(e);
|
343
|
+
}
|
344
|
+
}
|
345
|
+
|
346
|
+
const settled = await Promise.allSettled(promises);
|
347
|
+
|
348
|
+
if (errors.length > 0) {
|
349
|
+
throw errors[0];
|
350
|
+
}
|
351
|
+
|
352
|
+
return settled
|
353
|
+
.map((p) => (p.status === 'fulfilled' ? p.value : null))
|
354
|
+
.filter((p) => !!p) as InstanceInfo[];
|
355
|
+
},
|
356
|
+
{
|
357
|
+
name: `Starting plan ${systemId}`,
|
358
|
+
}
|
359
|
+
);
|
360
|
+
}
|
361
|
+
|
362
|
+
public stopAllForPlan(systemId: string) {
|
363
|
+
systemId = normalizeKapetaUri(systemId);
|
364
|
+
const instancesForPlan = this._instances.filter((instance) => instance.systemId === systemId);
|
365
|
+
return taskManager.add(
|
366
|
+
`plan:stop:${systemId}`,
|
367
|
+
async () => {
|
368
|
+
return this.stopInstances(instancesForPlan);
|
369
|
+
},
|
370
|
+
{
|
371
|
+
name: `Stopping plan ${systemId}`,
|
372
|
+
}
|
373
|
+
);
|
374
|
+
}
|
375
|
+
|
376
|
+
public async getInstanceOperator(
|
377
|
+
systemId: string,
|
378
|
+
instanceId: string,
|
379
|
+
environment?: EnvironmentType,
|
380
|
+
ensureContainer: boolean = true
|
381
|
+
): Promise<InstanceOperator<any, any>> {
|
382
|
+
const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
|
383
|
+
if (!blockInstance) {
|
384
|
+
throw new Error(`Instance not found: ${systemId}/${instanceId}`);
|
385
|
+
}
|
386
|
+
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
387
|
+
const block = await assetManager.getAsset(blockRef, true);
|
388
|
+
if (!block) {
|
389
|
+
throw new Error(`Block not found: ${blockRef}`);
|
390
|
+
}
|
391
|
+
|
392
|
+
const operatorDefinition = await definitionsManager.getDefinition(block.kind);
|
393
|
+
|
394
|
+
if (!operatorDefinition) {
|
395
|
+
throw new Error(`Operator not found: ${block.kind}`);
|
396
|
+
}
|
397
|
+
|
398
|
+
if (operatorDefinition.definition.kind !== KIND_BLOCK_TYPE_OPERATOR) {
|
399
|
+
throw new Error(`Block is not an operator: ${blockRef}`);
|
400
|
+
}
|
401
|
+
|
402
|
+
if (!operatorDefinition.definition.spec.local) {
|
403
|
+
throw new Error(`Operator block has no local definition: ${blockRef}`);
|
404
|
+
}
|
405
|
+
|
406
|
+
const localConfig = operatorDefinition.definition.spec.local as LocalInstance;
|
407
|
+
const ports: { [key: string]: InstanceOperatorPort } = {};
|
408
|
+
|
409
|
+
if (ensureContainer) {
|
410
|
+
let instance = await this.start(systemId, instanceId);
|
411
|
+
if (instance instanceof Task) {
|
412
|
+
instance = await instance.wait();
|
413
|
+
}
|
414
|
+
|
415
|
+
const container = await containerManager.get(instance.pid as string);
|
416
|
+
if (!container) {
|
417
|
+
throw new Error(`Container not found: ${instance.pid}`);
|
418
|
+
}
|
419
|
+
|
420
|
+
const portInfo = await container.getPorts();
|
421
|
+
if (!portInfo) {
|
422
|
+
throw new Error(`No ports found for instance: ${instanceId}`);
|
423
|
+
}
|
424
|
+
|
425
|
+
Object.entries(portInfo).forEach(([key, value]) => {
|
426
|
+
ports[key] = {
|
427
|
+
protocol: value.protocol as 'udp' | 'tcp',
|
428
|
+
port: parseInt(value.hostPort),
|
429
|
+
};
|
430
|
+
});
|
431
|
+
} else {
|
432
|
+
// If we're not ensuring the container is running we just get the ports from the local config
|
433
|
+
const instancePorts = await getOperatorInstancePorts(systemId, instanceId, localConfig);
|
434
|
+
instancePorts.forEach((port) => {
|
435
|
+
ports[port.portType] = {
|
436
|
+
protocol: port.protocol as 'udp' | 'tcp',
|
437
|
+
port: port.hostPort,
|
438
|
+
};
|
439
|
+
});
|
440
|
+
}
|
441
|
+
|
442
|
+
const hostname = getRemoteHostForEnvironment(environment);
|
443
|
+
|
444
|
+
return {
|
445
|
+
hostname,
|
446
|
+
ports,
|
447
|
+
credentials: localConfig.credentials,
|
448
|
+
options: localConfig.options,
|
449
|
+
};
|
450
|
+
}
|
451
|
+
|
452
|
+
public async stop(systemId: string, instanceId: string) {
|
453
|
+
return this.stopInner(systemId, instanceId, true);
|
454
|
+
}
|
455
|
+
|
456
|
+
private async stopInner(
|
457
|
+
systemId: string,
|
458
|
+
instanceId: string,
|
459
|
+
changeDesired: boolean = false,
|
460
|
+
checkForSingleton: boolean = true
|
461
|
+
) {
|
462
|
+
if (checkForSingleton) {
|
463
|
+
const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
|
464
|
+
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
465
|
+
|
466
|
+
const blockAsset = await assetManager.getAsset(blockRef, true);
|
467
|
+
if (!blockAsset) {
|
468
|
+
throw new Error('Block not found: ' + blockRef);
|
469
|
+
}
|
470
|
+
|
471
|
+
if (await this.isSingletonOperator(blockAsset)) {
|
472
|
+
const instances = await this.getAllInstancesForKind(systemId, blockAsset.data.kind);
|
473
|
+
if (instances.length > 1) {
|
474
|
+
const promises = instances.map((id) => {
|
475
|
+
return this.stopInner(systemId, id, changeDesired, false);
|
476
|
+
});
|
477
|
+
|
478
|
+
await Promise.all(promises);
|
479
|
+
return;
|
480
|
+
}
|
481
|
+
}
|
482
|
+
}
|
483
|
+
|
484
|
+
return this.exclusive(systemId, instanceId, async () => {
|
485
|
+
systemId = normalizeKapetaUri(systemId);
|
486
|
+
const instance = this.getInstance(systemId, instanceId);
|
487
|
+
if (!instance) {
|
488
|
+
return;
|
489
|
+
}
|
490
|
+
|
491
|
+
if (instance.status === InstanceStatus.STOPPED) {
|
492
|
+
return;
|
493
|
+
}
|
494
|
+
|
495
|
+
if (instance.status === InstanceStatus.STOPPING) {
|
496
|
+
return;
|
497
|
+
}
|
498
|
+
|
499
|
+
if (changeDesired && instance.desiredStatus !== DesiredInstanceStatus.EXTERNAL) {
|
500
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
501
|
+
}
|
502
|
+
|
503
|
+
const wasFailed = instance.status === InstanceStatus.FAILED;
|
504
|
+
|
505
|
+
instance.status = InstanceStatus.STOPPING;
|
506
|
+
|
507
|
+
socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
508
|
+
console.log(
|
509
|
+
'Stopping instance: %s::%s [desired: %s] [intentional: %s]',
|
510
|
+
systemId,
|
511
|
+
instanceId,
|
512
|
+
instance.desiredStatus,
|
513
|
+
changeDesired
|
514
|
+
);
|
515
|
+
this.save();
|
516
|
+
|
517
|
+
try {
|
518
|
+
if (instance.type === 'docker') {
|
519
|
+
const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
520
|
+
const container = await containerManager.getContainerByName(containerName);
|
521
|
+
if (container) {
|
522
|
+
try {
|
523
|
+
if (wasFailed) {
|
524
|
+
await container.remove();
|
525
|
+
} else {
|
526
|
+
await container.stop();
|
527
|
+
}
|
528
|
+
instance.status = InstanceStatus.STOPPED;
|
529
|
+
socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
530
|
+
this.save();
|
531
|
+
} catch (e) {
|
532
|
+
console.error('Failed to stop container', e);
|
533
|
+
}
|
534
|
+
} else {
|
535
|
+
console.warn('Container not found', containerName);
|
536
|
+
}
|
537
|
+
return;
|
538
|
+
}
|
539
|
+
|
540
|
+
if (!instance.pid) {
|
541
|
+
instance.status = InstanceStatus.STOPPED;
|
542
|
+
this.save();
|
543
|
+
return;
|
544
|
+
}
|
545
|
+
|
546
|
+
process.kill(instance.pid as number, 'SIGTERM');
|
547
|
+
instance.status = InstanceStatus.STOPPED;
|
548
|
+
socketManager.emitSystemEvent(systemId, EVENT_STATUS_CHANGED, instance);
|
549
|
+
this.save();
|
550
|
+
} catch (e) {
|
551
|
+
console.error('Failed to stop process', e);
|
552
|
+
}
|
553
|
+
});
|
554
|
+
}
|
555
|
+
|
556
|
+
public async start(
|
557
|
+
systemId: string,
|
558
|
+
instanceId: string,
|
559
|
+
checkForSingleton: boolean = true
|
560
|
+
): Promise<InstanceInfo | Task<InstanceInfo>> {
|
561
|
+
systemId = normalizeKapetaUri(systemId);
|
562
|
+
const blockInstance = await assetManager.getBlockInstance(systemId, instanceId);
|
563
|
+
const blockRef = normalizeKapetaUri(blockInstance.block.ref);
|
564
|
+
|
565
|
+
const blockAsset = await assetManager.getAsset(blockRef, true);
|
566
|
+
if (!blockAsset) {
|
567
|
+
throw new Error('Block not found: ' + blockRef);
|
568
|
+
}
|
569
|
+
|
570
|
+
if (checkForSingleton && (await this.isSingletonOperator(blockAsset))) {
|
571
|
+
const instances = await this.getAllInstancesForKind(systemId, blockAsset.data.kind);
|
572
|
+
if (instances.length > 1) {
|
573
|
+
const promises = instances.map((id) => {
|
574
|
+
return this.start(systemId, id, false);
|
575
|
+
});
|
576
|
+
|
577
|
+
await Promise.all(promises);
|
578
|
+
return promises[0];
|
579
|
+
}
|
580
|
+
}
|
581
|
+
|
582
|
+
return this.exclusive(systemId, instanceId, async () => {
|
583
|
+
let existingInstance = this.getInstance(systemId, instanceId);
|
584
|
+
|
585
|
+
if (existingInstance && existingInstance.pid) {
|
586
|
+
const container = await containerManager.get(existingInstance.pid as string);
|
587
|
+
if (!container) {
|
588
|
+
// The container is not running
|
589
|
+
existingInstance = undefined;
|
590
|
+
}
|
591
|
+
}
|
592
|
+
|
593
|
+
if (existingInstance && existingInstance.pid) {
|
594
|
+
if (existingInstance.status === InstanceStatus.READY) {
|
595
|
+
// Instance is already running
|
596
|
+
return existingInstance;
|
597
|
+
}
|
598
|
+
|
599
|
+
if (
|
600
|
+
existingInstance.desiredStatus === DesiredInstanceStatus.RUN &&
|
601
|
+
existingInstance.status === InstanceStatus.STARTING
|
602
|
+
) {
|
603
|
+
// Internal instance is already starting - don't start it again
|
604
|
+
return existingInstance;
|
605
|
+
}
|
606
|
+
|
607
|
+
if (
|
608
|
+
existingInstance.owner === InstanceOwner.EXTERNAL &&
|
609
|
+
existingInstance.status === InstanceStatus.STARTING
|
610
|
+
) {
|
611
|
+
// External instance is already starting - don't start it again
|
612
|
+
return existingInstance;
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
let instance: InstanceInfo = {
|
617
|
+
systemId,
|
618
|
+
instanceId,
|
619
|
+
ref: blockRef,
|
620
|
+
name: blockAsset.data.metadata.name,
|
621
|
+
desiredStatus: DesiredInstanceStatus.RUN,
|
622
|
+
owner: InstanceOwner.INTERNAL,
|
623
|
+
type: existingInstance?.type ?? InstanceType.UNKNOWN,
|
624
|
+
status: InstanceStatus.STARTING,
|
625
|
+
startedAt: Date.now(),
|
626
|
+
};
|
627
|
+
|
628
|
+
console.log('Starting instance: %s::%s [desired: %s]', systemId, instanceId, instance.desiredStatus);
|
629
|
+
// Save the instance before starting it, so that we can track the status
|
630
|
+
await this.saveInternalInstance(instance);
|
631
|
+
|
632
|
+
const blockSpec = blockAsset.data.spec as BlockDefinitionSpec;
|
633
|
+
if (blockSpec.consumers) {
|
634
|
+
const promises = blockSpec.consumers.map(async (consumer) => {
|
635
|
+
const consumerUri = parseKapetaUri(consumer.kind);
|
636
|
+
const asset = await definitionsManager.getDefinition(consumer.kind);
|
637
|
+
if (!asset) {
|
638
|
+
// Definition not found
|
639
|
+
return Promise.resolve();
|
640
|
+
}
|
641
|
+
|
642
|
+
if (KIND_RESOURCE_OPERATOR.toLowerCase() !== asset.definition.kind.toLowerCase()) {
|
643
|
+
// Not an operator
|
644
|
+
return Promise.resolve();
|
645
|
+
}
|
646
|
+
// Check if the operator has a local definition, if not we skip it since we can't start it
|
647
|
+
if (!asset.definition.spec.local) {
|
648
|
+
console.log('Skipping operator since it as no local definition: %s', consumer.kind);
|
649
|
+
return Promise.resolve();
|
650
|
+
}
|
651
|
+
console.log('Ensuring resource: %s in %s', consumerUri.id, systemId);
|
652
|
+
return operatorManager.ensureOperator(systemId, consumerUri.fullName, consumerUri.version);
|
653
|
+
});
|
654
|
+
|
655
|
+
await Promise.all(promises);
|
656
|
+
}
|
657
|
+
|
658
|
+
if (existingInstance) {
|
659
|
+
// Check if the instance is already running - but after we've commmuicated the desired status
|
660
|
+
const currentStatus = await this.requestInstanceStatus(existingInstance);
|
661
|
+
|
662
|
+
if (currentStatus === InstanceStatus.READY) {
|
663
|
+
// Instance is already running
|
664
|
+
return existingInstance;
|
665
|
+
}
|
666
|
+
}
|
667
|
+
|
668
|
+
const resolvedConfig = await configManager.getConfigForBlockInstance(systemId, instanceId);
|
669
|
+
|
670
|
+
const task = taskManager.add(
|
671
|
+
`instance:start:${systemId}:${instanceId}`,
|
672
|
+
async () => {
|
673
|
+
const runner = new BlockInstanceRunner(systemId);
|
674
|
+
const startTime = Date.now();
|
675
|
+
try {
|
676
|
+
const processInfo = await runner.start(blockRef, instanceId, resolvedConfig);
|
677
|
+
|
678
|
+
instance.status = InstanceStatus.STARTING;
|
679
|
+
|
680
|
+
return this.saveInternalInstance({
|
681
|
+
...instance,
|
682
|
+
type: processInfo.type,
|
683
|
+
pid: processInfo.pid ?? -1,
|
684
|
+
health: null,
|
685
|
+
portType: processInfo.portType,
|
686
|
+
status: InstanceStatus.STARTING,
|
687
|
+
});
|
688
|
+
} catch (e: any) {
|
689
|
+
console.warn('Failed to start instance: ', systemId, instanceId, blockRef, e);
|
690
|
+
const logs: LogEntry[] = [
|
691
|
+
{
|
692
|
+
source: 'stdout',
|
693
|
+
level: 'ERROR',
|
694
|
+
message: e.message,
|
695
|
+
time: Date.now(),
|
696
|
+
},
|
697
|
+
];
|
698
|
+
|
699
|
+
const out = await this.saveInternalInstance({
|
700
|
+
...instance,
|
701
|
+
type: InstanceType.DOCKER,
|
702
|
+
health: null,
|
703
|
+
portType: DEFAULT_HEALTH_PORT_TYPE,
|
704
|
+
status: InstanceStatus.FAILED,
|
705
|
+
errorMessage: e.message ?? 'Failed to start - Check logs for details.',
|
706
|
+
});
|
707
|
+
|
708
|
+
socketManager.emitInstanceLog(systemId, instanceId, logs[0]);
|
709
|
+
|
710
|
+
return out;
|
711
|
+
}
|
712
|
+
},
|
713
|
+
{
|
714
|
+
name: `Starting instance: ${instance.name}`,
|
715
|
+
systemId,
|
716
|
+
}
|
717
|
+
);
|
718
|
+
|
719
|
+
return task;
|
720
|
+
});
|
721
|
+
}
|
722
|
+
|
723
|
+
/**
|
724
|
+
* Stops an instance but does not remove it from the list of active instances
|
725
|
+
*
|
726
|
+
* It will be started again next time the system checks the status of the instance
|
727
|
+
*
|
728
|
+
* We do it this way to not cause the user to wait for the instance to start again
|
729
|
+
*/
|
730
|
+
public async prepareForRestart(systemId: string, instanceId: string) {
|
731
|
+
systemId = normalizeKapetaUri(systemId);
|
732
|
+
|
733
|
+
console.log('Stopping instance during restart...', systemId, instanceId);
|
734
|
+
await this.stopInner(systemId, instanceId);
|
735
|
+
}
|
736
|
+
|
737
|
+
public async stopAll() {
|
738
|
+
return this.stopInstances(this._instances);
|
739
|
+
}
|
740
|
+
|
741
|
+
private async stopInstances(instances: InstanceInfo[]) {
|
742
|
+
const promises = instances.map((instance) => this.stop(instance.systemId, instance.instanceId));
|
743
|
+
await Promise.allSettled(promises);
|
744
|
+
this.save();
|
745
|
+
}
|
746
|
+
|
747
|
+
private save() {
|
748
|
+
try {
|
749
|
+
storageService.put(
|
750
|
+
'instances',
|
751
|
+
this._instances.map((instance) => {
|
752
|
+
return { ...instance };
|
753
|
+
})
|
754
|
+
);
|
755
|
+
} catch (e) {
|
756
|
+
console.error('Failed to save instances', this._instances, e);
|
757
|
+
}
|
758
|
+
}
|
759
|
+
|
760
|
+
private async checkInstances() {
|
761
|
+
//console.log('\n## Checking instances:');
|
762
|
+
let changed = false;
|
763
|
+
const all = [...this._instances];
|
764
|
+
while (all.length > 0) {
|
765
|
+
// Check a few instances at a time - docker doesn't like too many concurrent requests
|
766
|
+
const chunk = all.splice(0, 30);
|
767
|
+
const promises = chunk.map(async (oldInstance) => {
|
768
|
+
if (!oldInstance.systemId) {
|
769
|
+
return;
|
770
|
+
}
|
771
|
+
|
772
|
+
// Grab the latest here
|
773
|
+
const instance = this.getInstance(oldInstance.systemId, oldInstance.instanceId);
|
774
|
+
if (!instance) {
|
775
|
+
return;
|
776
|
+
}
|
777
|
+
|
778
|
+
instance.systemId = normalizeKapetaUri(instance.systemId);
|
779
|
+
if (instance.ref) {
|
780
|
+
instance.ref = normalizeKapetaUri(instance.ref);
|
781
|
+
}
|
782
|
+
|
783
|
+
if (instance.desiredStatus === DesiredInstanceStatus.RUN) {
|
784
|
+
// Check if the plan still exists and the instance is still in the plan
|
785
|
+
// - and that the block definition exists
|
786
|
+
try {
|
787
|
+
const plan = await assetManager.getAsset(instance.systemId, true, false);
|
788
|
+
if (!plan) {
|
789
|
+
console.log('Plan not found - reset to stop', instance.ref, instance.systemId);
|
790
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
791
|
+
changed = true;
|
792
|
+
return;
|
793
|
+
}
|
794
|
+
|
795
|
+
const planData = plan.data as Plan;
|
796
|
+
const planInstance = planData?.spec?.blocks?.find((b) => b.id === instance.instanceId);
|
797
|
+
if (!planInstance || !planInstance?.block?.ref) {
|
798
|
+
console.log('Plan instance not found - reset to stop', instance.ref, instance.systemId);
|
799
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
800
|
+
changed = true;
|
801
|
+
return;
|
802
|
+
}
|
803
|
+
|
804
|
+
const blockDef = await assetManager.getAsset(instance.ref, true, false);
|
805
|
+
if (!blockDef) {
|
806
|
+
console.log('Block definition not found - reset to stop', instance.ref, instance.systemId);
|
807
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
808
|
+
changed = true;
|
809
|
+
return;
|
810
|
+
}
|
811
|
+
} catch (e) {
|
812
|
+
console.warn('Failed to check assets', instance.systemId, e);
|
813
|
+
instance.desiredStatus = DesiredInstanceStatus.STOP;
|
814
|
+
return;
|
815
|
+
}
|
816
|
+
}
|
817
|
+
|
818
|
+
const newStatus = await this.requestInstanceStatus(instance);
|
819
|
+
/*
|
820
|
+
console.log('Check instance %s %s: [current: %s, new: %s, desired: %s]',
|
821
|
+
instance.systemId, instance.instanceId, instance.status, newStatus, instance.desiredStatus);
|
822
|
+
*/
|
823
|
+
|
824
|
+
if (newStatus === InstanceStatus.BUSY) {
|
825
|
+
// If instance is busy we skip it
|
826
|
+
//console.log('Instance %s %s is busy', instance.systemId, instance.instanceId);
|
827
|
+
return;
|
828
|
+
}
|
829
|
+
|
830
|
+
if (
|
831
|
+
instance.startedAt !== undefined &&
|
832
|
+
newStatus === InstanceStatus.UNHEALTHY &&
|
833
|
+
instance.startedAt + HEALTH_CHECK_TIMEOUT < Date.now() &&
|
834
|
+
instance.status === InstanceStatus.STARTING
|
835
|
+
) {
|
836
|
+
// If instance is starting we consider unhealthy an indication
|
837
|
+
// that it is still starting
|
838
|
+
//console.log('Instance %s %s is still starting', instance.systemId, instance.instanceId);
|
839
|
+
return;
|
840
|
+
}
|
841
|
+
|
842
|
+
if (instance.status !== newStatus) {
|
843
|
+
const oldStatus = instance.status;
|
844
|
+
const skipUpdate =
|
845
|
+
([InstanceStatus.READY, InstanceStatus.UNHEALTHY].includes(newStatus) &&
|
846
|
+
instance.status === InstanceStatus.STOPPING) ||
|
847
|
+
(newStatus === InstanceStatus.STOPPED &&
|
848
|
+
instance.status === InstanceStatus.STARTING &&
|
849
|
+
instance.desiredStatus === DesiredInstanceStatus.RUN);
|
850
|
+
|
851
|
+
if (!skipUpdate) {
|
852
|
+
const oldStatus = instance.status;
|
853
|
+
instance.status = newStatus;
|
854
|
+
console.log(
|
855
|
+
'Instance status changed: %s %s: %s -> %s',
|
856
|
+
instance.systemId,
|
857
|
+
instance.instanceId,
|
858
|
+
oldStatus,
|
859
|
+
instance.status
|
860
|
+
);
|
861
|
+
socketManager.emitSystemEvent(instance.systemId, EVENT_STATUS_CHANGED, instance);
|
862
|
+
changed = true;
|
863
|
+
}
|
864
|
+
}
|
865
|
+
|
866
|
+
if (
|
867
|
+
instance.desiredStatus === DesiredInstanceStatus.RUN &&
|
868
|
+
[InstanceStatus.STOPPED, InstanceStatus.STOPPING].includes(newStatus)
|
869
|
+
) {
|
870
|
+
//If the instance is stopped but we want it to run, start it
|
871
|
+
try {
|
872
|
+
await this.start(instance.systemId, instance.instanceId);
|
873
|
+
} catch (e: any) {
|
874
|
+
console.warn(
|
875
|
+
'Failed to start previously stopped instance',
|
876
|
+
instance.systemId,
|
877
|
+
instance.instanceId,
|
878
|
+
e
|
879
|
+
);
|
880
|
+
}
|
881
|
+
return;
|
882
|
+
}
|
883
|
+
|
884
|
+
if (
|
885
|
+
instance.desiredStatus === DesiredInstanceStatus.STOP &&
|
886
|
+
[InstanceStatus.READY, InstanceStatus.STARTING, InstanceStatus.UNHEALTHY].includes(newStatus)
|
887
|
+
) {
|
888
|
+
//If the instance is running but we want it to stop, stop it
|
889
|
+
try {
|
890
|
+
console.log(
|
891
|
+
'Stopping instance since it is its desired state',
|
892
|
+
instance.systemId,
|
893
|
+
instance.instanceId
|
894
|
+
);
|
895
|
+
await this.stopInner(instance.systemId, instance.instanceId);
|
896
|
+
} catch (e) {
|
897
|
+
console.warn('Failed to stop instance', instance.systemId, instance.instanceId, e);
|
898
|
+
}
|
899
|
+
return;
|
900
|
+
}
|
901
|
+
|
902
|
+
if (
|
903
|
+
instance.desiredStatus === DesiredInstanceStatus.RUN &&
|
904
|
+
instance.status !== newStatus &&
|
905
|
+
newStatus === InstanceStatus.UNHEALTHY
|
906
|
+
) {
|
907
|
+
//If the instance is unhealthy, try to restart it
|
908
|
+
console.log('Restarting unhealthy instance', instance);
|
909
|
+
try {
|
910
|
+
await this.prepareForRestart(instance.systemId, instance.instanceId);
|
911
|
+
} catch (e) {
|
912
|
+
console.warn('Failed to restart instance', instance.systemId, instance.instanceId, e);
|
913
|
+
}
|
914
|
+
}
|
915
|
+
});
|
916
|
+
|
917
|
+
await Promise.allSettled(promises);
|
918
|
+
}
|
919
|
+
|
920
|
+
if (changed) {
|
921
|
+
this.save();
|
922
|
+
}
|
923
|
+
|
924
|
+
//console.log('\n##\n');
|
925
|
+
}
|
926
|
+
|
927
|
+
private async getExternalStatus(instance: InstanceInfo): Promise<InstanceStatus> {
|
928
|
+
if (instance.type === InstanceType.DOCKER) {
|
929
|
+
const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
|
930
|
+
const container = await containerManager.getContainerByName(containerName);
|
931
|
+
if (!container) {
|
932
|
+
// If the container doesn't exist, we consider the instance stopped
|
933
|
+
return InstanceStatus.STOPPED;
|
934
|
+
}
|
935
|
+
const state = await container.status();
|
936
|
+
if (!state) {
|
937
|
+
return InstanceStatus.STOPPED;
|
938
|
+
}
|
939
|
+
|
940
|
+
const statusType = state.Status as DockerContainerStatus;
|
941
|
+
|
942
|
+
if (statusType === 'running') {
|
943
|
+
if (state.Health?.Status) {
|
944
|
+
const healthStatusType = state.Health.Status as DockerContainerHealth;
|
945
|
+
if (healthStatusType === 'healthy' || healthStatusType === 'none') {
|
946
|
+
return InstanceStatus.READY;
|
947
|
+
}
|
948
|
+
|
949
|
+
if (healthStatusType === 'starting') {
|
950
|
+
return InstanceStatus.STARTING;
|
951
|
+
}
|
952
|
+
|
953
|
+
if (healthStatusType === 'unhealthy') {
|
954
|
+
return InstanceStatus.UNHEALTHY;
|
955
|
+
}
|
956
|
+
}
|
957
|
+
return InstanceStatus.READY;
|
958
|
+
}
|
959
|
+
|
960
|
+
if (statusType === 'created') {
|
961
|
+
if (state.ExitCode !== undefined && state.ExitCode !== 0) {
|
962
|
+
// Failed during creation. Exit code is not always reliable though
|
963
|
+
if (state.Error) {
|
964
|
+
return InstanceStatus.FAILED;
|
965
|
+
} else {
|
966
|
+
return InstanceStatus.STOPPED;
|
967
|
+
}
|
968
|
+
}
|
969
|
+
return InstanceStatus.STARTING;
|
970
|
+
}
|
971
|
+
|
972
|
+
if (statusType === 'exited' || statusType === 'dead') {
|
973
|
+
if (!state.Error) {
|
974
|
+
// Exit code is not always reliable - if there is no error we assume it's stopped
|
975
|
+
return InstanceStatus.STOPPED;
|
976
|
+
}
|
977
|
+
return InstanceStatus.FAILED;
|
978
|
+
}
|
979
|
+
|
980
|
+
if (statusType === 'removing') {
|
981
|
+
return InstanceStatus.BUSY;
|
982
|
+
}
|
983
|
+
|
984
|
+
if (statusType === 'restarting') {
|
985
|
+
return InstanceStatus.BUSY;
|
986
|
+
}
|
987
|
+
|
988
|
+
if (statusType === 'paused') {
|
989
|
+
return InstanceStatus.BUSY;
|
990
|
+
}
|
991
|
+
|
992
|
+
return InstanceStatus.STOPPED;
|
993
|
+
}
|
994
|
+
|
995
|
+
if (!instance.pid) {
|
996
|
+
return InstanceStatus.STOPPED;
|
997
|
+
}
|
998
|
+
|
999
|
+
//Otherwise its just a normal process.
|
1000
|
+
//TODO: Handle for Windows
|
1001
|
+
try {
|
1002
|
+
if (process.kill(instance.pid as number, 0)) {
|
1003
|
+
return InstanceStatus.READY;
|
1004
|
+
}
|
1005
|
+
} catch (err: any) {
|
1006
|
+
if (err.code === 'EPERM') {
|
1007
|
+
return InstanceStatus.READY;
|
1008
|
+
}
|
1009
|
+
}
|
1010
|
+
|
1011
|
+
return InstanceStatus.STOPPED;
|
1012
|
+
}
|
1013
|
+
|
1014
|
+
private async requestInstanceStatus(instance: InstanceInfo): Promise<InstanceStatus> {
|
1015
|
+
const externalStatus = await this.getExternalStatus(instance);
|
1016
|
+
if (instance.type === InstanceType.DOCKER) {
|
1017
|
+
// For docker instances we can rely on docker status
|
1018
|
+
return externalStatus;
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
if (externalStatus === InstanceStatus.STOPPED) {
|
1022
|
+
return externalStatus;
|
1023
|
+
}
|
1024
|
+
|
1025
|
+
if (!instance.health) {
|
1026
|
+
//No health url means we assume it's healthy as soon as it's running
|
1027
|
+
return InstanceStatus.READY;
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
return new Promise((resolve) => {
|
1031
|
+
if (!instance.health) {
|
1032
|
+
resolve(InstanceStatus.READY);
|
1033
|
+
return;
|
1034
|
+
}
|
1035
|
+
request(instance.health, (err, response) => {
|
1036
|
+
if (err) {
|
1037
|
+
resolve(InstanceStatus.UNHEALTHY);
|
1038
|
+
return;
|
1039
|
+
}
|
1040
|
+
|
1041
|
+
if (response.statusCode > 399) {
|
1042
|
+
resolve(InstanceStatus.UNHEALTHY);
|
1043
|
+
return;
|
1044
|
+
}
|
1045
|
+
|
1046
|
+
resolve(InstanceStatus.READY);
|
1047
|
+
});
|
1048
|
+
});
|
1049
|
+
}
|
1050
|
+
|
1051
|
+
private async isSingletonOperator(blockAsset: EnrichedAsset): Promise<boolean> {
|
1052
|
+
const provider = await assetManager.getAsset(blockAsset.data.kind);
|
1053
|
+
if (!provider) {
|
1054
|
+
return false;
|
1055
|
+
}
|
1056
|
+
|
1057
|
+
if (parseKapetaUri(provider.kind).fullName === KIND_BLOCK_TYPE_OPERATOR) {
|
1058
|
+
const localConfig = provider.data.spec.local as LocalInstance;
|
1059
|
+
return localConfig.singleton ?? false;
|
1060
|
+
}
|
1061
|
+
|
1062
|
+
return false;
|
1063
|
+
}
|
1064
|
+
|
1065
|
+
private async getKindForAssetRef(assetRef: string): Promise<string | null> {
|
1066
|
+
const block = await assetManager.getAsset(assetRef);
|
1067
|
+
if (!block) {
|
1068
|
+
return null;
|
1069
|
+
}
|
1070
|
+
|
1071
|
+
return block.data.kind;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
/**
|
1075
|
+
* Get the kind of an asset. Use the maxDepth parameter to specify how deep to look for the
|
1076
|
+
* kind. For example, if maxDepth is 2, the method will look for the kind of the asset and then
|
1077
|
+
* the kind of the kind.
|
1078
|
+
* @param assetRef The asset reference
|
1079
|
+
* @param maxDepth The maximum depth to look for the kind
|
1080
|
+
* @returns The kind of the asset or null if not found
|
1081
|
+
*/
|
1082
|
+
private async getDeepKindForAssetRef(assetRef: string, maxDepth: number): Promise<string | null> {
|
1083
|
+
if (maxDepth <= 0) {
|
1084
|
+
return null;
|
1085
|
+
}
|
1086
|
+
|
1087
|
+
try {
|
1088
|
+
const asset = await assetManager.getAsset(assetRef);
|
1089
|
+
if (!asset || !asset.data.kind) {
|
1090
|
+
return null;
|
1091
|
+
}
|
1092
|
+
|
1093
|
+
if (maxDepth === 1) {
|
1094
|
+
return asset.data.kind;
|
1095
|
+
} else {
|
1096
|
+
// Recurse with the kind of the current block and one less depth
|
1097
|
+
return await this.getDeepKindForAssetRef(asset.data.kind, maxDepth - 1);
|
1098
|
+
}
|
1099
|
+
} catch (error) {
|
1100
|
+
console.error('Error fetching kind for assetRef:', assetRef, error);
|
1101
|
+
return null;
|
1102
|
+
}
|
1103
|
+
}
|
1104
|
+
|
1105
|
+
private async isUsingKind(ref: string, kind: string): Promise<boolean> {
|
1106
|
+
const assetKind = await this.getKindForAssetRef(ref);
|
1107
|
+
if (!assetKind) {
|
1108
|
+
return false;
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
return parseKapetaUri(assetKind).fullName === parseKapetaUri(kind).fullName;
|
1112
|
+
}
|
1113
|
+
|
1114
|
+
private async getAllInstancesForKind(systemId: string, kind: string): Promise<string[]> {
|
1115
|
+
const plan = await assetManager.getPlan(systemId);
|
1116
|
+
if (!plan?.spec?.blocks) {
|
1117
|
+
return [];
|
1118
|
+
}
|
1119
|
+
const out: string[] = [];
|
1120
|
+
for (const block of plan.spec.blocks) {
|
1121
|
+
if (await this.isUsingKind(block.block.ref, kind)) {
|
1122
|
+
out.push(block.id);
|
1123
|
+
}
|
1124
|
+
}
|
1125
|
+
|
1126
|
+
return out;
|
1127
|
+
}
|
1128
|
+
|
1129
|
+
/**
|
1130
|
+
* Get the ids for all block instances except the ones of the specified kind
|
1131
|
+
* @param systemId The plan reference id
|
1132
|
+
* @param kind The kind to exclude. Can be a string or an array of strings
|
1133
|
+
* @returns An array of block instance ids
|
1134
|
+
*/
|
1135
|
+
private async getAllInstancesExceptKind(systemId: string, kind: string | string[]): Promise<string[]> {
|
1136
|
+
const plan = await assetManager.getPlan(systemId);
|
1137
|
+
if (!plan?.spec?.blocks) {
|
1138
|
+
return [];
|
1139
|
+
}
|
1140
|
+
const out: string[] = [];
|
1141
|
+
const excludedKinds = kind instanceof Array ? kind : [kind];
|
1142
|
+
for (const block of plan.spec.blocks) {
|
1143
|
+
const blockKindOfKind = await this.getDeepKindForAssetRef(block.block.ref, 2);
|
1144
|
+
if (!blockKindOfKind) {
|
1145
|
+
continue;
|
1146
|
+
}
|
1147
|
+
|
1148
|
+
const shouldIncludeBlock =
|
1149
|
+
excludedKinds.some((excludedKind) => excludedKind === parseKapetaUri(blockKindOfKind).fullName) ===
|
1150
|
+
false;
|
1151
|
+
if (shouldIncludeBlock) {
|
1152
|
+
out.push(block.id);
|
1153
|
+
}
|
1154
|
+
}
|
1155
|
+
|
1156
|
+
return out;
|
1157
|
+
}
|
1158
|
+
}
|
1159
|
+
|
1160
|
+
export const instanceManager = new InstanceManager();
|