@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.
- package/CHANGELOG.md +14 -0
- package/definitions.d.ts +4 -4
- package/index.js +2 -2
- package/package.json +2 -3
- package/src/assetManager.js +4 -2
- package/src/containerManager.js +288 -243
- package/src/instanceManager.js +33 -9
- package/src/instances/routes.js +10 -0
- package/src/networkManager.js +6 -6
- package/src/proxy/routes.js +16 -14
- package/src/proxy/types/rest.js +5 -6
- package/src/proxy/types/web.js +3 -5
- package/src/repositoryManager.js +16 -5
- package/src/serviceManager.js +0 -1
- package/src/utils/BlockInstanceRunner.js +435 -0
- package/src/utils/LogData.js +50 -0
- package/src/utils/utils.js +13 -0
package/src/instances/routes.js
CHANGED
@@ -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,
|
package/src/networkManager.js
CHANGED
@@ -3,10 +3,10 @@ class NetworkManager {
|
|
3
3
|
|
4
4
|
static toConnectionId(connection) {
|
5
5
|
return [
|
6
|
-
connection.
|
7
|
-
connection.
|
8
|
-
connection.
|
9
|
-
connection.
|
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.
|
69
|
-
this._ensureTarget(systemId, connection.
|
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
|
}
|
package/src/proxy/routes.js
CHANGED
@@ -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.
|
41
|
-
connection.
|
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.
|
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
|
61
|
+
const consumerResource = getResource(toBlockAsset.data.spec.consumers, req.params.consumerResourceName);
|
61
62
|
|
62
|
-
if (!
|
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.
|
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.
|
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.
|
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
|
93
|
+
const providerResource = getResource(fromBlockAsset.data.spec.providers, connection.provider.resourceName);
|
93
94
|
|
94
|
-
if (!
|
95
|
-
res.status(401).send({error:`Block resource not found "${connection.
|
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.
|
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
|
-
|
115
|
-
|
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
|
|
package/src/proxy/types/rest.js
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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',
|
package/src/proxy/types/web.js
CHANGED
@@ -12,7 +12,7 @@ const socketManager = require('../../socketManager');
|
|
12
12
|
*/
|
13
13
|
module.exports = function proxyRestRequest(req, res, opts) {
|
14
14
|
|
15
|
-
console.log('
|
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.
|
26
|
-
const targetBasePath = opts.
|
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,
|
package/src/repositoryManager.js
CHANGED
@@ -166,7 +166,8 @@ class RepositoryManager {
|
|
166
166
|
return null;
|
167
167
|
}
|
168
168
|
|
169
|
-
const
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
|
package/src/serviceManager.js
CHANGED
@@ -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;
|