@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.
Files changed (274) hide show
  1. package/.eslintrc.cjs +25 -0
  2. package/.github/workflows/check-license.yml +17 -0
  3. package/.github/workflows/main.yml +26 -0
  4. package/.prettierignore +4 -0
  5. package/.vscode/launch.json +19 -0
  6. package/CHANGELOG.md +920 -0
  7. package/LICENSE +38 -0
  8. package/README.md +36 -0
  9. package/definitions.d.ts +35 -0
  10. package/dist/cjs/index.d.ts +34 -0
  11. package/dist/cjs/index.js +263 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/src/RepositoryWatcher.d.ts +30 -0
  14. package/dist/cjs/src/RepositoryWatcher.js +332 -0
  15. package/dist/cjs/src/ai/aiClient.d.ts +20 -0
  16. package/dist/cjs/src/ai/aiClient.js +74 -0
  17. package/dist/cjs/src/ai/routes.d.ts +7 -0
  18. package/dist/cjs/src/ai/routes.js +37 -0
  19. package/dist/cjs/src/ai/transform.d.ts +11 -0
  20. package/dist/cjs/src/ai/transform.js +239 -0
  21. package/dist/cjs/src/ai/types.d.ts +40 -0
  22. package/dist/cjs/src/ai/types.js +2 -0
  23. package/dist/cjs/src/api.d.ts +7 -0
  24. package/dist/cjs/src/api.js +29 -0
  25. package/dist/cjs/src/assetManager.d.ts +41 -0
  26. package/dist/cjs/src/assetManager.js +274 -0
  27. package/dist/cjs/src/assets/routes.d.ts +7 -0
  28. package/dist/cjs/src/assets/routes.js +165 -0
  29. package/dist/cjs/src/attachments/routes.d.ts +7 -0
  30. package/dist/cjs/src/attachments/routes.js +72 -0
  31. package/dist/cjs/src/authManager.d.ts +16 -0
  32. package/dist/cjs/src/authManager.js +64 -0
  33. package/dist/cjs/src/cacheManager.d.ts +20 -0
  34. package/dist/cjs/src/cacheManager.js +51 -0
  35. package/dist/cjs/src/clusterService.d.ts +44 -0
  36. package/dist/cjs/src/clusterService.js +120 -0
  37. package/dist/cjs/src/codeGeneratorManager.d.ts +14 -0
  38. package/dist/cjs/src/codeGeneratorManager.js +93 -0
  39. package/dist/cjs/src/config/routes.d.ts +7 -0
  40. package/dist/cjs/src/config/routes.js +160 -0
  41. package/dist/cjs/src/configManager.d.ts +42 -0
  42. package/dist/cjs/src/configManager.js +136 -0
  43. package/dist/cjs/src/containerManager.d.ts +148 -0
  44. package/dist/cjs/src/containerManager.js +958 -0
  45. package/dist/cjs/src/definitionsManager.d.ts +20 -0
  46. package/dist/cjs/src/definitionsManager.js +171 -0
  47. package/dist/cjs/src/filesystem/routes.d.ts +7 -0
  48. package/dist/cjs/src/filesystem/routes.js +105 -0
  49. package/dist/cjs/src/filesystemManager.d.ts +27 -0
  50. package/dist/cjs/src/filesystemManager.js +118 -0
  51. package/dist/cjs/src/identities/routes.d.ts +7 -0
  52. package/dist/cjs/src/identities/routes.js +37 -0
  53. package/dist/cjs/src/instanceManager.d.ts +69 -0
  54. package/dist/cjs/src/instanceManager.js +910 -0
  55. package/dist/cjs/src/instances/routes.d.ts +7 -0
  56. package/dist/cjs/src/instances/routes.js +179 -0
  57. package/dist/cjs/src/middleware/cors.d.ts +6 -0
  58. package/dist/cjs/src/middleware/cors.js +14 -0
  59. package/dist/cjs/src/middleware/kapeta.d.ts +15 -0
  60. package/dist/cjs/src/middleware/kapeta.js +28 -0
  61. package/dist/cjs/src/middleware/stringBody.d.ts +9 -0
  62. package/dist/cjs/src/middleware/stringBody.js +18 -0
  63. package/dist/cjs/src/networkManager.d.ts +37 -0
  64. package/dist/cjs/src/networkManager.js +119 -0
  65. package/dist/cjs/src/operatorManager.d.ts +41 -0
  66. package/dist/cjs/src/operatorManager.js +211 -0
  67. package/dist/cjs/src/progressListener.d.ts +31 -0
  68. package/dist/cjs/src/progressListener.js +133 -0
  69. package/dist/cjs/src/providerManager.d.ts +11 -0
  70. package/dist/cjs/src/providerManager.js +84 -0
  71. package/dist/cjs/src/providers/routes.d.ts +7 -0
  72. package/dist/cjs/src/providers/routes.js +46 -0
  73. package/dist/cjs/src/proxy/routes.d.ts +7 -0
  74. package/dist/cjs/src/proxy/routes.js +115 -0
  75. package/dist/cjs/src/proxy/types/rest.d.ts +10 -0
  76. package/dist/cjs/src/proxy/types/rest.js +123 -0
  77. package/dist/cjs/src/proxy/types/web.d.ts +8 -0
  78. package/dist/cjs/src/proxy/types/web.js +61 -0
  79. package/dist/cjs/src/repositoryManager.d.ts +35 -0
  80. package/dist/cjs/src/repositoryManager.js +247 -0
  81. package/dist/cjs/src/serviceManager.d.ts +36 -0
  82. package/dist/cjs/src/serviceManager.js +106 -0
  83. package/dist/cjs/src/socketManager.d.ts +32 -0
  84. package/dist/cjs/src/socketManager.js +125 -0
  85. package/dist/cjs/src/storageService.d.ts +21 -0
  86. package/dist/cjs/src/storageService.js +81 -0
  87. package/dist/cjs/src/taskManager.d.ts +70 -0
  88. package/dist/cjs/src/taskManager.js +181 -0
  89. package/dist/cjs/src/tasks/routes.d.ts +7 -0
  90. package/dist/cjs/src/tasks/routes.js +39 -0
  91. package/dist/cjs/src/traffic/routes.d.ts +7 -0
  92. package/dist/cjs/src/traffic/routes.js +22 -0
  93. package/dist/cjs/src/types.d.ts +99 -0
  94. package/dist/cjs/src/types.js +39 -0
  95. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +28 -0
  96. package/dist/cjs/src/utils/BlockInstanceRunner.js +432 -0
  97. package/dist/cjs/src/utils/DefaultProviderInstaller.d.ts +15 -0
  98. package/dist/cjs/src/utils/DefaultProviderInstaller.js +136 -0
  99. package/dist/cjs/src/utils/InternalConfigProvider.d.ts +38 -0
  100. package/dist/cjs/src/utils/InternalConfigProvider.js +146 -0
  101. package/dist/cjs/src/utils/LogData.d.ts +23 -0
  102. package/dist/cjs/src/utils/LogData.js +46 -0
  103. package/dist/cjs/src/utils/commandLineUtils.d.ts +8 -0
  104. package/dist/cjs/src/utils/commandLineUtils.js +39 -0
  105. package/dist/cjs/src/utils/pathTemplateParser.d.ts +30 -0
  106. package/dist/cjs/src/utils/pathTemplateParser.js +135 -0
  107. package/dist/cjs/src/utils/utils.d.ts +40 -0
  108. package/dist/cjs/src/utils/utils.js +148 -0
  109. package/dist/cjs/start.d.ts +5 -0
  110. package/dist/cjs/start.js +17 -0
  111. package/dist/cjs/test/proxy/types/rest.test.d.ts +5 -0
  112. package/dist/cjs/test/proxy/types/rest.test.js +48 -0
  113. package/dist/cjs/test/utils/pathTemplateParser.test.d.ts +5 -0
  114. package/dist/cjs/test/utils/pathTemplateParser.test.js +27 -0
  115. package/dist/esm/index.d.ts +34 -0
  116. package/dist/esm/index.js +263 -0
  117. package/dist/esm/package.json +1 -0
  118. package/dist/esm/src/RepositoryWatcher.d.ts +30 -0
  119. package/dist/esm/src/RepositoryWatcher.js +332 -0
  120. package/dist/esm/src/ai/aiClient.d.ts +20 -0
  121. package/dist/esm/src/ai/aiClient.js +74 -0
  122. package/dist/esm/src/ai/routes.d.ts +7 -0
  123. package/dist/esm/src/ai/routes.js +37 -0
  124. package/dist/esm/src/ai/transform.d.ts +11 -0
  125. package/dist/esm/src/ai/transform.js +239 -0
  126. package/dist/esm/src/ai/types.d.ts +40 -0
  127. package/dist/esm/src/ai/types.js +2 -0
  128. package/dist/esm/src/api.d.ts +7 -0
  129. package/dist/esm/src/api.js +29 -0
  130. package/dist/esm/src/assetManager.d.ts +41 -0
  131. package/dist/esm/src/assetManager.js +274 -0
  132. package/dist/esm/src/assets/routes.d.ts +7 -0
  133. package/dist/esm/src/assets/routes.js +165 -0
  134. package/dist/esm/src/attachments/routes.d.ts +7 -0
  135. package/dist/esm/src/attachments/routes.js +72 -0
  136. package/dist/esm/src/authManager.d.ts +16 -0
  137. package/dist/esm/src/authManager.js +64 -0
  138. package/dist/esm/src/cacheManager.d.ts +20 -0
  139. package/dist/esm/src/cacheManager.js +51 -0
  140. package/dist/esm/src/clusterService.d.ts +44 -0
  141. package/dist/esm/src/clusterService.js +120 -0
  142. package/dist/esm/src/codeGeneratorManager.d.ts +14 -0
  143. package/dist/esm/src/codeGeneratorManager.js +93 -0
  144. package/dist/esm/src/config/routes.d.ts +7 -0
  145. package/dist/esm/src/config/routes.js +160 -0
  146. package/dist/esm/src/configManager.d.ts +42 -0
  147. package/dist/esm/src/configManager.js +136 -0
  148. package/dist/esm/src/containerManager.d.ts +148 -0
  149. package/dist/esm/src/containerManager.js +958 -0
  150. package/dist/esm/src/definitionsManager.d.ts +20 -0
  151. package/dist/esm/src/definitionsManager.js +171 -0
  152. package/dist/esm/src/filesystem/routes.d.ts +7 -0
  153. package/dist/esm/src/filesystem/routes.js +105 -0
  154. package/dist/esm/src/filesystemManager.d.ts +27 -0
  155. package/dist/esm/src/filesystemManager.js +118 -0
  156. package/dist/esm/src/identities/routes.d.ts +7 -0
  157. package/dist/esm/src/identities/routes.js +37 -0
  158. package/dist/esm/src/instanceManager.d.ts +69 -0
  159. package/dist/esm/src/instanceManager.js +910 -0
  160. package/dist/esm/src/instances/routes.d.ts +7 -0
  161. package/dist/esm/src/instances/routes.js +179 -0
  162. package/dist/esm/src/middleware/cors.d.ts +6 -0
  163. package/dist/esm/src/middleware/cors.js +14 -0
  164. package/dist/esm/src/middleware/kapeta.d.ts +15 -0
  165. package/dist/esm/src/middleware/kapeta.js +28 -0
  166. package/dist/esm/src/middleware/stringBody.d.ts +9 -0
  167. package/dist/esm/src/middleware/stringBody.js +18 -0
  168. package/dist/esm/src/networkManager.d.ts +37 -0
  169. package/dist/esm/src/networkManager.js +119 -0
  170. package/dist/esm/src/operatorManager.d.ts +41 -0
  171. package/dist/esm/src/operatorManager.js +211 -0
  172. package/dist/esm/src/progressListener.d.ts +31 -0
  173. package/dist/esm/src/progressListener.js +133 -0
  174. package/dist/esm/src/providerManager.d.ts +11 -0
  175. package/dist/esm/src/providerManager.js +84 -0
  176. package/dist/esm/src/providers/routes.d.ts +7 -0
  177. package/dist/esm/src/providers/routes.js +46 -0
  178. package/dist/esm/src/proxy/routes.d.ts +7 -0
  179. package/dist/esm/src/proxy/routes.js +115 -0
  180. package/dist/esm/src/proxy/types/rest.d.ts +10 -0
  181. package/dist/esm/src/proxy/types/rest.js +123 -0
  182. package/dist/esm/src/proxy/types/web.d.ts +8 -0
  183. package/dist/esm/src/proxy/types/web.js +61 -0
  184. package/dist/esm/src/repositoryManager.d.ts +35 -0
  185. package/dist/esm/src/repositoryManager.js +247 -0
  186. package/dist/esm/src/serviceManager.d.ts +36 -0
  187. package/dist/esm/src/serviceManager.js +106 -0
  188. package/dist/esm/src/socketManager.d.ts +32 -0
  189. package/dist/esm/src/socketManager.js +125 -0
  190. package/dist/esm/src/storageService.d.ts +21 -0
  191. package/dist/esm/src/storageService.js +81 -0
  192. package/dist/esm/src/taskManager.d.ts +70 -0
  193. package/dist/esm/src/taskManager.js +181 -0
  194. package/dist/esm/src/tasks/routes.d.ts +7 -0
  195. package/dist/esm/src/tasks/routes.js +39 -0
  196. package/dist/esm/src/traffic/routes.d.ts +7 -0
  197. package/dist/esm/src/traffic/routes.js +22 -0
  198. package/dist/esm/src/types.d.ts +99 -0
  199. package/dist/esm/src/types.js +39 -0
  200. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +28 -0
  201. package/dist/esm/src/utils/BlockInstanceRunner.js +432 -0
  202. package/dist/esm/src/utils/DefaultProviderInstaller.d.ts +15 -0
  203. package/dist/esm/src/utils/DefaultProviderInstaller.js +136 -0
  204. package/dist/esm/src/utils/InternalConfigProvider.d.ts +38 -0
  205. package/dist/esm/src/utils/InternalConfigProvider.js +146 -0
  206. package/dist/esm/src/utils/LogData.d.ts +23 -0
  207. package/dist/esm/src/utils/LogData.js +46 -0
  208. package/dist/esm/src/utils/commandLineUtils.d.ts +8 -0
  209. package/dist/esm/src/utils/commandLineUtils.js +39 -0
  210. package/dist/esm/src/utils/pathTemplateParser.d.ts +30 -0
  211. package/dist/esm/src/utils/pathTemplateParser.js +135 -0
  212. package/dist/esm/src/utils/utils.d.ts +40 -0
  213. package/dist/esm/src/utils/utils.js +148 -0
  214. package/dist/esm/start.d.ts +5 -0
  215. package/dist/esm/start.js +17 -0
  216. package/dist/esm/test/proxy/types/rest.test.d.ts +5 -0
  217. package/dist/esm/test/proxy/types/rest.test.js +48 -0
  218. package/dist/esm/test/utils/pathTemplateParser.test.d.ts +5 -0
  219. package/dist/esm/test/utils/pathTemplateParser.test.js +27 -0
  220. package/index.ts +280 -0
  221. package/jest.config.js +8 -0
  222. package/package.json +134 -0
  223. package/src/RepositoryWatcher.ts +363 -0
  224. package/src/ai/aiClient.ts +93 -0
  225. package/src/ai/routes.ts +39 -0
  226. package/src/ai/transform.ts +275 -0
  227. package/src/ai/types.ts +45 -0
  228. package/src/api.ts +32 -0
  229. package/src/assetManager.ts +355 -0
  230. package/src/assets/routes.ts +183 -0
  231. package/src/attachments/routes.ts +79 -0
  232. package/src/authManager.ts +67 -0
  233. package/src/cacheManager.ts +59 -0
  234. package/src/clusterService.ts +142 -0
  235. package/src/codeGeneratorManager.ts +109 -0
  236. package/src/config/routes.ts +201 -0
  237. package/src/configManager.ts +180 -0
  238. package/src/containerManager.ts +1178 -0
  239. package/src/definitionsManager.ts +212 -0
  240. package/src/filesystem/routes.ts +123 -0
  241. package/src/filesystemManager.ts +133 -0
  242. package/src/identities/routes.ts +38 -0
  243. package/src/instanceManager.ts +1160 -0
  244. package/src/instances/routes.ts +203 -0
  245. package/src/middleware/cors.ts +14 -0
  246. package/src/middleware/kapeta.ts +41 -0
  247. package/src/middleware/stringBody.ts +21 -0
  248. package/src/networkManager.ts +148 -0
  249. package/src/operatorManager.ts +294 -0
  250. package/src/progressListener.ts +151 -0
  251. package/src/providerManager.ts +97 -0
  252. package/src/providers/routes.ts +51 -0
  253. package/src/proxy/routes.ts +153 -0
  254. package/src/proxy/types/rest.ts +172 -0
  255. package/src/proxy/types/web.ts +70 -0
  256. package/src/repositoryManager.ts +291 -0
  257. package/src/serviceManager.ts +133 -0
  258. package/src/socketManager.ts +138 -0
  259. package/src/storageService.ts +97 -0
  260. package/src/taskManager.ts +247 -0
  261. package/src/tasks/routes.ts +43 -0
  262. package/src/traffic/routes.ts +23 -0
  263. package/src/types.ts +112 -0
  264. package/src/utils/BlockInstanceRunner.ts +577 -0
  265. package/src/utils/DefaultProviderInstaller.ts +150 -0
  266. package/src/utils/InternalConfigProvider.ts +214 -0
  267. package/src/utils/LogData.ts +50 -0
  268. package/src/utils/commandLineUtils.ts +45 -0
  269. package/src/utils/pathTemplateParser.ts +157 -0
  270. package/src/utils/utils.ts +155 -0
  271. package/start.ts +14 -0
  272. package/test/proxy/types/rest.test.ts +54 -0
  273. package/test/utils/pathTemplateParser.test.ts +29 -0
  274. 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();