@kapeta/local-cluster-service 0.0.76 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -100,6 +100,16 @@ router.put('/', async (req, res) => {
100
100
 
101
101
  let instance = JSON.parse(req.stringBody);
102
102
 
103
+ if (req.kapeta.environment === 'docker') {
104
+ //A bit hacky but we want to avoid overwriting the docker PID with a process PID
105
+ const oldInstance = instanceManager.getInstance(
106
+ req.kapeta.systemId,
107
+ req.kapeta.instanceId
108
+ );
109
+ if (oldInstance) {
110
+ instance.pid = oldInstance.pid;
111
+ }
112
+ }
103
113
  await instanceManager.registerInstance(
104
114
  req.kapeta.systemId,
105
115
  req.kapeta.instanceId,
@@ -3,10 +3,10 @@ class NetworkManager {
3
3
 
4
4
  static toConnectionId(connection) {
5
5
  return [
6
- connection.from.blockId,
7
- connection.from.resourceName,
8
- connection.to.blockId,
9
- connection.to.resourceName
6
+ connection.provider.blockId,
7
+ connection.provider.resourceName,
8
+ connection.consumer.blockId,
9
+ connection.consumer.resourceName
10
10
  ].join('_');
11
11
  }
12
12
 
@@ -65,8 +65,8 @@ class NetworkManager {
65
65
  const traffic = new Traffic(connection, request, consumerMethodId, providerMethodId);
66
66
 
67
67
  this._ensureConnection(systemId, traffic.connectionId).push(traffic);
68
- this._ensureSource(systemId, connection.from.blockId).push(traffic);
69
- this._ensureTarget(systemId, connection.to.blockId).push(traffic);
68
+ this._ensureSource(systemId, connection.provider.blockId).push(traffic);
69
+ this._ensureTarget(systemId, connection.consumer.blockId).push(traffic);
70
70
 
71
71
  return traffic;
72
72
  }
@@ -34,11 +34,12 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
34
34
 
35
35
  const plan = await assetManager.getPlan(req.params.systemId);
36
36
 
37
+
37
38
  // We can find the connection by the consumer information alone since
38
39
  // only 1 provider can be connected to a consumer resource at a time
39
40
  const connection = _.find(plan.spec.connections, (connection) => {
40
- return connection.to.blockId.toLowerCase() === req.params.consumerInstanceId.toLowerCase() &&
41
- connection.to.resourceName.toLowerCase() === req.params.consumerResourceName.toLowerCase();
41
+ return connection.consumer.blockId.toLowerCase() === req.params.consumerInstanceId.toLowerCase() &&
42
+ connection.consumer.resourceName.toLowerCase() === req.params.consumerResourceName.toLowerCase();
42
43
  });
43
44
 
44
45
  if (!connection) {
@@ -47,7 +48,7 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
47
48
  }
48
49
 
49
50
  const toBlockInstance = _.find(plan.spec.blocks, (blockInstance) => {
50
- return blockInstance.id.toLowerCase() === connection.to.blockId.toLowerCase();
51
+ return blockInstance.id.toLowerCase() === connection.consumer.blockId.toLowerCase();
51
52
  });
52
53
 
53
54
  if (!toBlockInstance) {
@@ -57,9 +58,9 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
57
58
 
58
59
  const toBlockAsset = await assetManager.getAsset(toBlockInstance.block.ref);
59
60
 
60
- const toResource = getResource(toBlockAsset.data.spec.consumers, req.params.consumerResourceName);
61
+ const consumerResource = getResource(toBlockAsset.data.spec.consumers, req.params.consumerResourceName);
61
62
 
62
- if (!toResource) {
63
+ if (!consumerResource) {
63
64
  res.status(401).send({error:`Block resource not found "${req.params.consumerInstanceId}::${req.params.consumerResourceName}`});
64
65
  return;
65
66
  }
@@ -76,23 +77,23 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
76
77
  Note that this might not match the path the destination is expecting so we need to identify the method
77
78
  that is being called and identify the destination path from the connection.
78
79
  */
79
- const consumerPath = req.originalUrl.substr(basePath.length - 1);
80
+ const consumerPath = req.originalUrl.substring(basePath.length - 1);
80
81
 
81
82
  const fromBlockInstance = _.find(plan.spec.blocks, (blockInstance) => {
82
- return blockInstance.id.toLowerCase() === connection.from.blockId.toLowerCase();
83
+ return blockInstance.id.toLowerCase() === connection.provider.blockId.toLowerCase();
83
84
  });
84
85
 
85
86
  if (!fromBlockInstance) {
86
- res.status(401).send({error:`Block instance not found "${connection.from.blockId}`});
87
+ res.status(401).send({error:`Block instance not found "${connection.provider.blockId}`});
87
88
  return;
88
89
  }
89
90
 
90
91
  const fromBlockAsset = await assetManager.getAsset(fromBlockInstance.block.ref);
91
92
 
92
- const fromResource = getResource(fromBlockAsset.data.spec.providers, connection.from.resourceName);
93
+ const providerResource = getResource(fromBlockAsset.data.spec.providers, connection.provider.resourceName);
93
94
 
94
- if (!fromResource) {
95
- res.status(401).send({error:`Block resource not found "${connection.from.blockId}::${connection.from.resourceName}`});
95
+ if (!providerResource) {
96
+ res.status(401).send({error:`Block resource not found "${connection.provider.blockId}::${connection.provider.resourceName}`});
96
97
  return;
97
98
  }
98
99
 
@@ -100,7 +101,7 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
100
101
  //Get target address
101
102
  let address = await serviceManager.getProviderAddress(
102
103
  req.params.systemId,
103
- connection.from.blockId,
104
+ connection.provider.blockId,
104
105
  req.params.type
105
106
  );
106
107
 
@@ -111,12 +112,13 @@ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async
111
112
  typeHandler(req, res, {
112
113
  consumerPath,
113
114
  address,
114
- toResource,
115
- fromResource,
115
+ consumerResource,
116
+ providerResource,
116
117
  connection
117
118
  });
118
119
 
119
120
  } catch(err) {
121
+ console.warn("Failed to process proxy request", err);
120
122
  res.status(400).send({error: err.message});
121
123
  }
122
124
 
@@ -35,7 +35,7 @@ function getRestMethodId(restResource, httpMethod, httpPath) {
35
35
  * @return {{consumerMethod: *, providerMethod: *}}
36
36
  */
37
37
  function resolveMethods(req, opts) {
38
- const consumerMethodId = getRestMethodId(opts.toResource, req.method, opts.consumerPath);
38
+ const consumerMethodId = getRestMethodId(opts.consumerResource, req.method, opts.consumerPath);
39
39
 
40
40
  if (!consumerMethodId) {
41
41
  throw new Error(
@@ -43,7 +43,7 @@ function resolveMethods(req, opts) {
43
43
  );
44
44
  }
45
45
 
46
- const consumerMethod = _.cloneDeep(opts.toResource.spec.methods[consumerMethodId]);
46
+ const consumerMethod = _.cloneDeep(opts.consumerResource.spec.methods[consumerMethodId]);
47
47
 
48
48
  if (!consumerMethod) {
49
49
  throw new Error(
@@ -61,11 +61,11 @@ function resolveMethods(req, opts) {
61
61
  throw new Error(`Connection contained no mapping for consumer method "${consumerMethodId}`);
62
62
  }
63
63
 
64
- const providerMethod = _.cloneDeep(opts.fromResource.spec.methods[providerMethodId]);
64
+ const providerMethod = _.cloneDeep(opts.providerResource.spec.methods[providerMethodId]);
65
65
 
66
66
  if (!providerMethod) {
67
67
  throw new Error(
68
- `Provider method not found "${providerMethodId}" in resource "${opts.connection.from.blockId}::${opts.connection.from.resourceName}`
68
+ `Provider method not found "${providerMethodId}" in resource "${opts.connection.provider.blockId}::${opts.connection.provider.resourceName}`
69
69
  );
70
70
  }
71
71
 
@@ -106,8 +106,7 @@ module.exports = function proxyRestRequest(req, res, opts) {
106
106
  delete requestHeaders['host'];
107
107
  delete requestHeaders['origin'];
108
108
 
109
-
110
- console.log('Route to provider: %s => %s', opts.consumerPath, opts.address + providerPath);
109
+ console.log('Proxy request to provider: %s => %s [rest]', opts.consumerPath, opts.address + providerPath);
111
110
 
112
111
  const reqOpts = {
113
112
  method: providerMethod.method || 'GET',
@@ -12,7 +12,7 @@ const socketManager = require('../../socketManager');
12
12
  */
13
13
  module.exports = function proxyRestRequest(req, res, opts) {
14
14
 
15
- console.log('Route to provider: %s => %s', opts.consumerPath, opts.address);
15
+ console.log('Proxy request to provider: %s => %s [web]', opts.consumerPath, opts.address);
16
16
 
17
17
  const requestHeaders = _.clone(req.headers);
18
18
 
@@ -22,8 +22,8 @@ module.exports = function proxyRestRequest(req, res, opts) {
22
22
  delete requestHeaders['host'];
23
23
  delete requestHeaders['origin'];
24
24
 
25
- const sourceBasePath = opts.fromResource.spec.path;
26
- const targetBasePath = opts.toResource.spec.path;
25
+ const sourceBasePath = opts.providerResource.spec.path;
26
+ const targetBasePath = opts.consumerResource.spec.path;
27
27
  let path = opts.consumerPath;
28
28
  if (opts.consumerPath.startsWith(sourceBasePath)) {
29
29
  path = path.replace(sourceBasePath, targetBasePath);
@@ -37,8 +37,6 @@ module.exports = function proxyRestRequest(req, res, opts) {
37
37
  body: req.stringBody
38
38
  };
39
39
 
40
- console.log('reqOpts', reqOpts);
41
-
42
40
  const traffic = networkManager.addRequest(
43
41
  req.params.systemId,
44
42
  opts.connection,
@@ -166,7 +166,8 @@ class RepositoryManager {
166
166
  return null;
167
167
  }
168
168
 
169
- const installedAsset = ClusterConfiguration.getDefinitions().find(d =>
169
+ const definitions = ClusterConfiguration.getDefinitions();
170
+ const installedAsset = definitions.find(d =>
170
171
  d.definition.metadata.name === fullName &&
171
172
  d.version === version);
172
173
 
@@ -179,18 +180,28 @@ class RepositoryManager {
179
180
  return;
180
181
  }
181
182
 
182
- const assetVersion = await this._registryService.getVersion(fullName, version);
183
- if (!assetVersion) {
184
- this._cache[ref] = false;
185
- return;
183
+ try {
184
+ const assetVersion = await this._registryService.getVersion(fullName, version);
185
+ if (!assetVersion) {
186
+ this._cache[ref] = false;
187
+ return;
188
+ }
189
+ } catch (e) {
190
+ console.warn(`Unable to resolve asset: ${ref}`, e);
191
+ if (installedAsset) {
192
+ return;
193
+ }
194
+ throw e;
186
195
  }
187
196
 
188
197
  this._cache[ref] = true;
189
198
  if (!installedAsset) {
199
+ console.log(`Auto-installing missing asset: ${ref}`);
190
200
  await this._install([ref]);
191
201
  } else {
192
202
  //Ensure dependencies are installed
193
203
  const refs = assetVersion.dependencies.map((dep) => dep.name);
204
+ console.log(`Auto-installing dependencies: ${refs.join(', ')}`);
194
205
  await this._install(refs);
195
206
  }
196
207
 
@@ -73,7 +73,6 @@ class ServiceManager {
73
73
 
74
74
  const portTypeSection = service[portType];
75
75
 
76
-
77
76
  return portTypeSection.port;
78
77
  }
79
78
 
@@ -0,0 +1,435 @@
1
+ const {spawn} = require('node:child_process');
2
+ const FS = require('node:fs');
3
+ const Path = require('node:path');
4
+ const {Docker} = require('node-docker-api');
5
+ const ClusterConfig = require("@kapeta/local-cluster-config");
6
+ const {readYML} = require("./utils");
7
+ const {parseKapetaUri} = require("@kapeta/nodejs-utils");
8
+ const serviceManager = require("../serviceManager");
9
+ const containerManager = require("../containerManager");
10
+ const LogData = require("./LogData");
11
+ const EventEmitter = require("events");
12
+ const {execSync} = require("child_process");
13
+
14
+ const KIND_BLOCK_TYPE_OPERATOR = 'core/block-type-operator';
15
+ const KAPETA_SYSTEM_ID = "KAPETA_SYSTEM_ID";
16
+ const KAPETA_BLOCK_REF = "KAPETA_BLOCK_REF";
17
+ const KAPETA_INSTANCE_ID = "KAPETA_INSTANCE_ID";
18
+ /**
19
+ * Needed when running local docker containers as part of plan
20
+ * @type {string[]}
21
+ */
22
+ const DOCKER_ENV_VARS = [
23
+ `KAPETA_LOCAL_SERVER=0.0.0.0`,
24
+ `KAPETA_LOCAL_CLUSTER_HOST=host.docker.internal`,
25
+ `KAPETA_ENVIRONMENT_TYPE=docker`,
26
+ ]
27
+
28
+ function md5(data) {
29
+ return require('crypto').createHash('md5').update(data).digest("hex");
30
+ }
31
+
32
+
33
+ class BlockInstanceRunner {
34
+ /**
35
+ * @param {string} [planReference]
36
+ * @param {BlockInstanceInfo[]} [instances]
37
+ */
38
+ constructor(planReference) {
39
+ /**
40
+ *
41
+ * @type {string}
42
+ * @private
43
+ */
44
+ this._systemId = planReference ?? '';
45
+ }
46
+
47
+
48
+
49
+ /**
50
+ * Start a block
51
+ *
52
+ * @param {string} blockRef
53
+ * @param {string} instanceId
54
+ * @param {any} configuration
55
+ * @returns {Promise<ProcessInfo>}
56
+ */
57
+ async start(blockRef, instanceId, configuration) {
58
+ return this._execute({
59
+ ref: blockRef,
60
+ id: instanceId,
61
+ configuration
62
+ });
63
+ }
64
+
65
+ /**
66
+ *
67
+ * @param {BlockInstanceInfo} blockInstance
68
+ * @return {Promise<ProcessInfo>}
69
+ * @private
70
+ */
71
+ async _execute(blockInstance) {
72
+ const env = Object.assign({}, process.env);
73
+
74
+ if (this._systemId) {
75
+ env[KAPETA_SYSTEM_ID] = this._systemId;
76
+ }
77
+
78
+ if (blockInstance.ref) {
79
+ env[KAPETA_BLOCK_REF] = blockInstance.ref;
80
+ }
81
+
82
+ if (blockInstance.id) {
83
+ env[KAPETA_INSTANCE_ID] = blockInstance.id;
84
+ }
85
+
86
+ const blockUri = parseKapetaUri(blockInstance.ref);
87
+
88
+ if (!blockUri.version) {
89
+ blockUri.version = 'local';
90
+ }
91
+
92
+ const definition = ClusterConfig.getDefinitions().find(definitions => {
93
+ const ref = `${definitions.definition.metadata.name}:${definitions.version}`
94
+ return parseKapetaUri(ref).id === blockUri.id;
95
+ });
96
+
97
+ if (!definition) {
98
+ throw new Error(`Block definition not found: ${blockUri.id}`);
99
+ }
100
+
101
+ const kindUri = parseKapetaUri(definition.definition.kind);
102
+
103
+ const provider = ClusterConfig.getProviderDefinitions().find(provider => {
104
+ const ref = `${provider.definition.metadata.name}:${provider.version}`
105
+ return parseKapetaUri(ref).id === kindUri.id;
106
+ });
107
+
108
+ if (!provider) {
109
+ throw new Error(`Kind not found: ${kindUri.id}`);
110
+ }
111
+
112
+ /**
113
+ * @type {ProcessDetails}
114
+ */
115
+ let processDetails;
116
+
117
+ if (provider.definition.kind === KIND_BLOCK_TYPE_OPERATOR) {
118
+ processDetails = await this._startOperatorProcess(blockInstance, blockUri, provider, env);
119
+ } else {
120
+ if (blockUri.version === 'local') {
121
+ processDetails = await this._startLocalProcess(blockInstance, blockUri, env);
122
+ } else {
123
+ processDetails = await this._startDockerProcess(blockInstance, blockUri, env);
124
+ }
125
+ }
126
+
127
+ return {
128
+ ...blockInstance,
129
+ ...processDetails
130
+ };
131
+ }
132
+
133
+
134
+ /**
135
+ * Starts local process
136
+ * @param {BlockInstanceInfo} blockInstance
137
+ * @param {BlockInfo} blockInfo
138
+ * @param {EnvironmentVariables} env
139
+ * @return {ProcessDetails}
140
+ * @private
141
+ */
142
+ _startLocalProcess(blockInstance, blockInfo, env) {
143
+ const baseDir = ClusterConfig.getRepositoryAssetPath(
144
+ blockInfo.handle,
145
+ blockInfo.name,
146
+ blockInfo.version
147
+ );
148
+
149
+ if (!FS.existsSync(baseDir)) {
150
+ throw new Error(
151
+ `Local block not registered correctly - expected symlink here: ${baseDir}.\n` +
152
+ `Make sure you've run "blockctl registry link" in your local directory to connect it to Kapeta`
153
+ );
154
+ }
155
+
156
+ const startScript = Path.resolve(baseDir, 'scripts/start.sh');
157
+ if (!FS.existsSync(startScript)) {
158
+ throw new Error(
159
+ `Start script did not exist for local block.\n` +
160
+ `Expected runnable start script here: ${startScript}`
161
+ )
162
+ }
163
+
164
+ const logs = new LogData();
165
+ const childProcess = spawn(startScript, [], {
166
+ cwd: baseDir,
167
+ env,
168
+ detached: true,
169
+ stdio: [
170
+ 'pipe', 'pipe', 'pipe'
171
+ ]
172
+ });
173
+
174
+ logs.addLog(`Starting block ${blockInstance.ref} using script ${startScript}`);
175
+ const outputEvents = new EventEmitter();
176
+ /**
177
+ *
178
+ * @type {ProcessDetails}
179
+ */
180
+ const out = {
181
+ type: 'local',
182
+ pid: childProcess.pid,
183
+ output: outputEvents,
184
+ stderr: childProcess.stderr,
185
+ logs: () => {
186
+ return logs.getLogs();
187
+ },
188
+ stop: () => {
189
+ childProcess.kill('SIGTERM');
190
+ }
191
+ };
192
+
193
+ childProcess.stdout.on('data', (data) => {
194
+ logs.addLog(data.toString());
195
+ outputEvents.emit('data', data);
196
+ });
197
+
198
+ childProcess.stderr.on('data', (data) => {
199
+ logs.addLog(data.toString());
200
+ outputEvents.emit('data', data);
201
+ });
202
+
203
+ childProcess.on('exit', (code) => {
204
+ logs.addLog(`Block ${blockInstance.ref} exited with code: ${code}`);
205
+ outputEvents.emit('exit', code);
206
+ });
207
+
208
+ return out;
209
+ }
210
+
211
+ async _handleContainer(container, logs , deleteOnExit = false) {
212
+ const logStream = await container.logs({
213
+ follow: true,
214
+ stdout: true,
215
+ stderr: true,
216
+ tail: LogData.MAX_LINES
217
+ })
218
+
219
+ const outputEvents = new EventEmitter();
220
+ logStream.on('data', (data) => {
221
+ logs.addLog(data.toString());
222
+ outputEvents.emit('data', data);
223
+ });
224
+
225
+ logStream.on('error', (data) => {
226
+ logs.addLog(data.toString());
227
+ outputEvents.emit('data', data);
228
+ });
229
+
230
+ logStream.on('close', async () => {
231
+ const status = await container.status();
232
+ if (deleteOnExit) {
233
+ try {
234
+ await container.delete()
235
+ } catch (e) {}
236
+ }
237
+ outputEvents.emit('exit', status.data?.State?.ExitCode ?? 0);
238
+ });
239
+ /**
240
+ *
241
+ * @type {ProcessDetails}
242
+ */
243
+ return {
244
+ type: 'docker',
245
+ pid: container.id,
246
+ output: outputEvents,
247
+ stop: async () => {
248
+ if (!container) {
249
+ return;
250
+ }
251
+
252
+ try {
253
+ await container.stop();
254
+ if (deleteOnExit) {
255
+ await container.delete();
256
+ }
257
+ } catch (e) {}
258
+ container = null;
259
+ },
260
+ logs: () => {
261
+ return logs.getLogs();
262
+ }
263
+ };
264
+ }
265
+
266
+
267
+ /**
268
+ * Starts local process using docker
269
+ * @param {BlockInstanceInfo} blockInstance
270
+ * @param {BlockInfo} blockInfo
271
+ * @param {EnvironmentVariables} env
272
+ * @return {Promise<ProcessDetails>}
273
+ * @private
274
+ */
275
+ async _startDockerProcess(blockInstance, blockInfo, env) {
276
+ const {versionFile} = ClusterConfig.getRepositoryAssetInfoPath(
277
+ blockInfo.handle,
278
+ blockInfo.name,
279
+ blockInfo.version
280
+ );
281
+
282
+ const versionYml = versionFile;
283
+ if (!FS.existsSync(versionYml)) {
284
+ throw new Error(`Did not find version info at the expected path: ${versionYml}`);
285
+ }
286
+
287
+ const versionInfo = readYML(versionYml);
288
+
289
+ if (versionInfo?.artifact?.type !== 'docker') {
290
+ throw new Error(`Unsupported artifact type: ${versionInfo?.artifact?.type}`);
291
+ }
292
+ const dockerImage = versionInfo?.artifact?.details?.primary;
293
+ if (!dockerImage) {
294
+ throw new Error(`Missing docker image information: ${JSON.stringify(versionInfo?.artifact?.details)}`);
295
+ }
296
+
297
+ const containerName = `kapeta-block-instance-${blockInstance.id}`;
298
+ const logs = new LogData();
299
+ let container = await containerManager.getContainerByName(containerName);
300
+
301
+ if (container) {
302
+ if (container.data.State === 'running') {
303
+ logs.addLog(`Found existing running container for block: ${containerName}`);
304
+ } else {
305
+ logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
306
+ await container.start();
307
+ }
308
+ } else {
309
+ logs.addLog(`Creating new container for block: ${containerName}`);
310
+ container = await containerManager.startContainer({
311
+ Image: dockerImage,
312
+ name: containerName,
313
+ Binds: [
314
+ `${ClusterConfig.getKapetaBasedir()}:${ClusterConfig.getKapetaBasedir()}`
315
+ ],
316
+ Labels: {
317
+ 'instance': blockInstance.id
318
+ },
319
+ Env: [
320
+ ...DOCKER_ENV_VARS,
321
+ ...Object.entries(env).map(([key, value]) => `${key}=${value}`)
322
+ ]
323
+ });
324
+ }
325
+
326
+ return this._handleContainer(container, logs);
327
+ }
328
+
329
+ async _startOperatorProcess(blockInstance, blockUri, providerDefinition, env) {
330
+ const {assetFile} = ClusterConfig.getRepositoryAssetInfoPath(
331
+ blockUri.handle,
332
+ blockUri.name,
333
+ blockUri.version
334
+ );
335
+
336
+ const kapetaYmlPath = assetFile;
337
+ if (!FS.existsSync(kapetaYmlPath)) {
338
+ throw new Error(`Did not find kapeta.yml at the expected path: ${kapetaYmlPath}`);
339
+ }
340
+
341
+ const spec = providerDefinition.definition.spec;
342
+ const providerRef = `${providerDefinition.definition.metadata.name}:${providerDefinition.version}`;
343
+
344
+ if (!spec?.local?.image) {
345
+ throw new Error(`Provider did not have local image: ${providerRef}`);
346
+ }
347
+
348
+ const dockerImage = spec?.local?.image;
349
+
350
+ try {
351
+ await containerManager.pull(dockerImage);
352
+ } catch (e) {
353
+ console.warn('Failed to pull image. Continuing...', e);
354
+ }
355
+
356
+ const containerName = `kapeta-block-instance-${md5(blockInstance.id)}`;
357
+ const logs = new LogData();
358
+ let container = await containerManager.getContainerByName(containerName);
359
+
360
+ if (container) {
361
+ if (container.data.State === 'running') {
362
+ logs.addLog(`Found existing running container for block: ${containerName}`);
363
+ } else {
364
+ if (container.data.State?.ExitCode > 0) {
365
+ logs.addLog(`Container exited with code: ${container.data.State.ExitCode}. Deleting...`);
366
+ try {
367
+ await container.delete()
368
+ } catch (e) {}
369
+ container = null;
370
+ } else {
371
+ logs.addLog(`Found existing container for block: ${containerName}. Starting now`);
372
+ try {
373
+ await container.start();
374
+ } catch (e) {
375
+ console.warn('Failed to start container. Deleting...', e);
376
+ try {
377
+ await container.delete()
378
+ } catch (e) {}
379
+ container = null;
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ if (!container) {
386
+ const ExposedPorts = {};
387
+ const addonEnv = {};
388
+ const PortBindings = {};
389
+ const promises = Object.entries(spec.local.ports)
390
+ .map(async ([portType, value]) => {
391
+ const dockerPort = `${value.port}/${value.type}`;
392
+ ExposedPorts[dockerPort] = {};
393
+ addonEnv[`KAPETA_LOCAL_SERVER_PORT_${portType.toUpperCase()}`] = value.port;
394
+ const publicPort = await serviceManager.ensureServicePort(this._systemId, blockInstance.id, portType);
395
+ PortBindings[dockerPort] = [
396
+ {
397
+ HostIp: "127.0.0.1", //No public
398
+ HostPort: `${publicPort}`
399
+ }
400
+ ];
401
+ });
402
+
403
+ await Promise.all(promises);
404
+
405
+ logs.addLog(`Creating new container for block: ${containerName}`);
406
+ container = await containerManager.startContainer({
407
+ Image: dockerImage,
408
+ name: containerName,
409
+ ExposedPorts,
410
+ HostConfig: {
411
+ Binds: [
412
+ `${kapetaYmlPath}:/kapeta.yml:ro`,
413
+ `${ClusterConfig.getKapetaBasedir()}:${ClusterConfig.getKapetaBasedir()}`
414
+ ],
415
+ PortBindings
416
+ },
417
+ Labels: {
418
+ 'instance': blockInstance.id
419
+ },
420
+ Env: [
421
+ `KAPETA_INSTANCE_NAME=${blockInstance.ref}`,
422
+ ...DOCKER_ENV_VARS,
423
+ ...Object.entries({
424
+ ...env,
425
+ ...addonEnv
426
+ }).map(([key, value]) => `${key}=${value}`)
427
+ ]
428
+ });
429
+ }
430
+
431
+ return this._handleContainer(container, logs, true);
432
+ }
433
+ }
434
+
435
+ module.exports = BlockInstanceRunner;