@kapeta/local-cluster-service 0.0.60
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/.github/workflows/pr-check.yml +18 -0
- package/.github/workflows/publish.yml +20 -0
- package/.vscode/launch.json +17 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/definitions.d.ts +42 -0
- package/index.js +127 -0
- package/package.json +45 -0
- package/src/assetManager.js +249 -0
- package/src/assets/routes.js +133 -0
- package/src/clusterService.js +134 -0
- package/src/codeGeneratorManager.js +40 -0
- package/src/config/routes.js +118 -0
- package/src/configManager.js +128 -0
- package/src/containerManager.js +279 -0
- package/src/filesystem/routes.js +74 -0
- package/src/filesystemManager.js +89 -0
- package/src/identities/routes.js +19 -0
- package/src/instanceManager.js +416 -0
- package/src/instances/routes.js +117 -0
- package/src/middleware/cors.js +7 -0
- package/src/middleware/kapeta.js +20 -0
- package/src/middleware/stringBody.js +11 -0
- package/src/networkManager.js +120 -0
- package/src/operatorManager.js +180 -0
- package/src/providerManager.js +84 -0
- package/src/providers/routes.js +26 -0
- package/src/proxy/routes.js +125 -0
- package/src/proxy/types/rest.js +163 -0
- package/src/proxy/types/web.js +83 -0
- package/src/serviceManager.js +117 -0
- package/src/socketManager.js +50 -0
- package/src/storageService.js +87 -0
- package/src/traffic/routes.js +18 -0
- package/src/utils/pathTemplateParser.js +116 -0
- package/start.js +7 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
const Router = require('express-promise-router').default;
|
2
|
+
const YAML = require('yaml');
|
3
|
+
const assetManager = require('../assetManager');
|
4
|
+
|
5
|
+
|
6
|
+
function parseBody(req) {
|
7
|
+
switch(req.headers['content-type']) {
|
8
|
+
case 'application/json':
|
9
|
+
case 'application/x-json':
|
10
|
+
case 'text/json':
|
11
|
+
return JSON.parse(req.stringBody);
|
12
|
+
|
13
|
+
case 'application/yaml':
|
14
|
+
case 'application/x-yaml':
|
15
|
+
case 'text/yaml':
|
16
|
+
case 'text/x-yaml':
|
17
|
+
default:
|
18
|
+
return YAML.parse(req.stringBody);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
const router = new Router();
|
23
|
+
|
24
|
+
router.use('/', require('../middleware/cors'));
|
25
|
+
router.use('/', require('../middleware/stringBody'));
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Get all local assets available
|
29
|
+
*/
|
30
|
+
router.get('/', (req, res) => {
|
31
|
+
res.send(assetManager.getAssets());
|
32
|
+
});
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Get single asset
|
36
|
+
*/
|
37
|
+
router.get('/read', (req, res) => {
|
38
|
+
if (!req.query.ref) {
|
39
|
+
res.status(400).send({error:'Query parameter "ref" is missing'});
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
|
43
|
+
try {
|
44
|
+
res.send(assetManager.getAsset(req.query.ref));
|
45
|
+
} catch(err) {
|
46
|
+
res.status(400).send({error: err.message});
|
47
|
+
}
|
48
|
+
|
49
|
+
});
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Creates a new local file and registers it as an asset
|
53
|
+
*/
|
54
|
+
router.post('/create', async (req, res) => {
|
55
|
+
if (!req.query.path) {
|
56
|
+
res.status(400).send({error:'Query parameter "path" is missing'});
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
const content = parseBody(req);
|
61
|
+
|
62
|
+
try {
|
63
|
+
const assets = await assetManager.createAsset(req.query.path, content);
|
64
|
+
|
65
|
+
res.status(200).send(assets);
|
66
|
+
} catch(err) {
|
67
|
+
console.log('Failed while creating asset', req.query.path, err.message);
|
68
|
+
res.status(400).send({error: err.message});
|
69
|
+
}
|
70
|
+
|
71
|
+
});
|
72
|
+
|
73
|
+
/**
|
74
|
+
* Updates reference with new content
|
75
|
+
*/
|
76
|
+
router.put('/update', async (req, res) => {
|
77
|
+
if (!req.query.ref) {
|
78
|
+
res.status(400).send({error:'Query parameter "ref" is missing'});
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
|
82
|
+
const content = parseBody(req);
|
83
|
+
|
84
|
+
try {
|
85
|
+
await assetManager.updateAsset(req.query.ref, content);
|
86
|
+
|
87
|
+
res.sendStatus(204);
|
88
|
+
} catch(err) {
|
89
|
+
console.log('Failed while updating asset', req.query.ref, err.message);
|
90
|
+
res.status(400).send({error: err.message});
|
91
|
+
}
|
92
|
+
|
93
|
+
});
|
94
|
+
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Unregisters an asset (doesn't delete the asset)
|
98
|
+
*/
|
99
|
+
router.delete('/', (req, res) => {
|
100
|
+
if (!req.query.ref) {
|
101
|
+
res.status(400).send({error:'Query parameter "ref" is missing'});
|
102
|
+
return;
|
103
|
+
}
|
104
|
+
|
105
|
+
try {
|
106
|
+
assetManager.unregisterAsset(req.query.ref);
|
107
|
+
|
108
|
+
res.status(204).send();
|
109
|
+
} catch(err) {
|
110
|
+
res.status(400).send({error: err.message});
|
111
|
+
}
|
112
|
+
});
|
113
|
+
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Registers an existing file as an asset
|
117
|
+
*/
|
118
|
+
router.put('/import', async (req, res) => {
|
119
|
+
if (!req.query.ref) {
|
120
|
+
res.status(400).send({error:'Query parameter "ref" is missing'});
|
121
|
+
return;
|
122
|
+
}
|
123
|
+
|
124
|
+
try {
|
125
|
+
const assets = await assetManager.importFile(req.query.ref);
|
126
|
+
|
127
|
+
res.status(200).send(assets);
|
128
|
+
} catch(err) {
|
129
|
+
res.status(400).send({error: err.message});
|
130
|
+
}
|
131
|
+
});
|
132
|
+
|
133
|
+
module.exports = router;
|
@@ -0,0 +1,134 @@
|
|
1
|
+
const net = require('net');
|
2
|
+
const DEFAULT_SERVER_PORT = 35100;
|
3
|
+
const DEFAULT_START_PORT = 40000;
|
4
|
+
const DEFAULT_HOST = '127.0.0.1';
|
5
|
+
|
6
|
+
class ClusterService {
|
7
|
+
|
8
|
+
constructor() {
|
9
|
+
this._port = DEFAULT_SERVER_PORT;
|
10
|
+
this._currentPort = DEFAULT_START_PORT;
|
11
|
+
this._initialized = false;
|
12
|
+
this._reservedPorts = [];
|
13
|
+
this._host = DEFAULT_HOST;
|
14
|
+
}
|
15
|
+
|
16
|
+
reservePort(port) {
|
17
|
+
const intPort = parseInt(port);
|
18
|
+
if (this._reservedPorts.indexOf(intPort) > -1) {
|
19
|
+
throw new Error('Port already reserved: ' + intPort);
|
20
|
+
}
|
21
|
+
|
22
|
+
this._reservedPorts.push(intPort);
|
23
|
+
}
|
24
|
+
|
25
|
+
async init() {
|
26
|
+
if (this._initialized) {
|
27
|
+
return;
|
28
|
+
}
|
29
|
+
|
30
|
+
this._initialized = true;
|
31
|
+
await this._findClusterServicePort();
|
32
|
+
|
33
|
+
}
|
34
|
+
|
35
|
+
async _findClusterServicePort() {
|
36
|
+
while(true) {
|
37
|
+
|
38
|
+
const isUsed = await this._checkIfPortIsUsed(this._port);
|
39
|
+
if (!isUsed) {
|
40
|
+
break;
|
41
|
+
}
|
42
|
+
|
43
|
+
this._port++;
|
44
|
+
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Gets next available port
|
51
|
+
* @return {Promise<number>}
|
52
|
+
*/
|
53
|
+
async getNextAvailablePort() {
|
54
|
+
while(true) {
|
55
|
+
|
56
|
+
while (this._reservedPorts.indexOf(this._currentPort) > -1) {
|
57
|
+
this._currentPort++;
|
58
|
+
}
|
59
|
+
|
60
|
+
const nextPort = this._currentPort++;
|
61
|
+
const isUsed = await this._checkIfPortIsUsed(nextPort);
|
62
|
+
if (!isUsed) {
|
63
|
+
return nextPort;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
_checkIfPortIsUsed(port, host=this._host) {
|
69
|
+
return new Promise((resolve, reject) => {
|
70
|
+
const server = net.createServer();
|
71
|
+
|
72
|
+
server.once('error', function(err) {
|
73
|
+
if (err.code === 'EADDRINUSE') {
|
74
|
+
server.close();
|
75
|
+
resolve(true);
|
76
|
+
return;
|
77
|
+
}
|
78
|
+
|
79
|
+
server.close();
|
80
|
+
reject(err);
|
81
|
+
});
|
82
|
+
|
83
|
+
server.once('listening', function() {
|
84
|
+
server.close();
|
85
|
+
resolve(false);
|
86
|
+
});
|
87
|
+
|
88
|
+
server.listen( port, host );
|
89
|
+
});
|
90
|
+
|
91
|
+
}
|
92
|
+
|
93
|
+
|
94
|
+
/**
|
95
|
+
* The port of this local cluster service itself
|
96
|
+
*/
|
97
|
+
getClusterServicePort() {
|
98
|
+
return this._port;
|
99
|
+
}
|
100
|
+
|
101
|
+
/*
|
102
|
+
*Gets the host name ( 127.0.0.1 ) on which Express JS is listening
|
103
|
+
*/
|
104
|
+
getClusterServiceHost() {
|
105
|
+
return this._host;
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Set the port to be used for this local service
|
110
|
+
* @param port
|
111
|
+
*/
|
112
|
+
setClusterServicePort(port) {
|
113
|
+
this._port = port;
|
114
|
+
}
|
115
|
+
|
116
|
+
setClusterServiceHost(host) {
|
117
|
+
this._host = host;
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* Gets that proxy path of a given request
|
122
|
+
*
|
123
|
+
* @param systemId
|
124
|
+
* @param consumerInstanceId
|
125
|
+
* @param consumerResourceName
|
126
|
+
* @param portType
|
127
|
+
* @return {string}
|
128
|
+
*/
|
129
|
+
getProxyPath(systemId, consumerInstanceId, consumerResourceName, portType) {
|
130
|
+
return `/proxy/${encodeURIComponent(systemId)}/${encodeURIComponent(consumerInstanceId)}/${encodeURIComponent(consumerResourceName)}/${encodeURIComponent(portType)}/`;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
module.exports = new ClusterService();
|
@@ -0,0 +1,40 @@
|
|
1
|
+
const Path = require('path');
|
2
|
+
|
3
|
+
const {registry:Targets, BlockCodeGenerator, CodeWriter} = require('@kapeta/codegen');
|
4
|
+
const ClusterConfiguration = require('@kapeta/local-cluster-config');
|
5
|
+
const TARGET_KIND = 'core/language-target';
|
6
|
+
const BLOCK_TYPE_KIND = 'core/block-type';
|
7
|
+
|
8
|
+
class CodeGeneratorManager {
|
9
|
+
|
10
|
+
reload() {
|
11
|
+
Targets.reset();
|
12
|
+
const languageTargets = ClusterConfiguration.getDefinitions(TARGET_KIND);
|
13
|
+
languageTargets.forEach((languageTarget) => {
|
14
|
+
const key = `${languageTarget.definition.metadata.name}:${languageTarget.version}`
|
15
|
+
Targets.register(key, require(languageTarget.path));
|
16
|
+
});
|
17
|
+
}
|
18
|
+
|
19
|
+
canGenerateCode(yamlContent) {
|
20
|
+
const blockTypes = ClusterConfiguration.getDefinitions(BLOCK_TYPE_KIND);
|
21
|
+
const blockTypeKinds = blockTypes.map(blockType => blockType.definition.metadata.name.toLowerCase() + ':' + blockType.version);
|
22
|
+
return yamlContent && yamlContent.kind && blockTypeKinds.indexOf(yamlContent.kind.toLowerCase()) > -1;
|
23
|
+
}
|
24
|
+
|
25
|
+
async generate(yamlFile, yamlContent) {
|
26
|
+
const baseDir = Path.dirname(yamlFile);
|
27
|
+
console.log('Generating code for path: %s', baseDir);
|
28
|
+
const codeGenerator = new BlockCodeGenerator(yamlContent);
|
29
|
+
|
30
|
+
const output = await codeGenerator.generate();
|
31
|
+
const writer = new CodeWriter(baseDir, {});
|
32
|
+
writer.write(output);
|
33
|
+
|
34
|
+
console.log('Code generated for path: %s', baseDir);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
const manager = new CodeGeneratorManager();
|
39
|
+
manager.reload();
|
40
|
+
module.exports = manager;
|
@@ -0,0 +1,118 @@
|
|
1
|
+
const Router = require('express-promise-router').default;
|
2
|
+
const YAML = require('yaml');
|
3
|
+
const configManager = require('../configManager');
|
4
|
+
const serviceManager = require('../serviceManager');
|
5
|
+
const operatorManager = require('../operatorManager');
|
6
|
+
|
7
|
+
const router = new Router();
|
8
|
+
|
9
|
+
router.use('/', require('../middleware/kapeta'));
|
10
|
+
router.use('/', require('../middleware/stringBody'));
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Returns the full configuration for a given service.
|
14
|
+
*/
|
15
|
+
router.get('/', (req, res) => {
|
16
|
+
//Get service YAML config
|
17
|
+
const config = configManager.getConfigForService(req.kapeta.systemId, req.kapeta.instanceId);
|
18
|
+
|
19
|
+
res.send(YAML.stringify(config));
|
20
|
+
});
|
21
|
+
|
22
|
+
/**
|
23
|
+
* Updates the full configuration for a given service.
|
24
|
+
*/
|
25
|
+
router.put('/', (req, res) => {
|
26
|
+
|
27
|
+
let config = YAML.parse(req.stringBody);
|
28
|
+
if (!config) {
|
29
|
+
config = {};
|
30
|
+
}
|
31
|
+
//Get service YAML config
|
32
|
+
configManager.setConfigForService(
|
33
|
+
req.kapeta.systemId,
|
34
|
+
req.kapeta.instanceId,
|
35
|
+
config
|
36
|
+
);
|
37
|
+
res.status(202).send({ok:true});
|
38
|
+
});
|
39
|
+
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Resolves and checks the identify of a block instance
|
43
|
+
*/
|
44
|
+
router.get('/identity', async (req, res) => {
|
45
|
+
|
46
|
+
|
47
|
+
const identity = {
|
48
|
+
systemId: req.kapeta.systemId,
|
49
|
+
instanceId: req.kapeta.instanceId
|
50
|
+
};
|
51
|
+
|
52
|
+
try {
|
53
|
+
|
54
|
+
if (!identity.systemId ||
|
55
|
+
!identity.instanceId) {
|
56
|
+
const {systemId, instanceId} = await configManager.resolveIdentity(req.kapeta.blockRef, identity.systemId);
|
57
|
+
identity.systemId = systemId;
|
58
|
+
identity.instanceId = instanceId;
|
59
|
+
} else {
|
60
|
+
await configManager.verifyIdentity(req.kapeta.blockRef, identity.systemId, identity.instanceId);
|
61
|
+
}
|
62
|
+
|
63
|
+
res.send(identity);
|
64
|
+
} catch(err) {
|
65
|
+
console.log(err);
|
66
|
+
|
67
|
+
res.send({error: err.message});
|
68
|
+
}
|
69
|
+
});
|
70
|
+
|
71
|
+
/**
|
72
|
+
* Services call this to request a free port. If a service has
|
73
|
+
* already called the endpoint the same port is returned.
|
74
|
+
*/
|
75
|
+
router.get('/provides/:type', async (req, res) => {
|
76
|
+
//Get service port
|
77
|
+
res.send('' + await serviceManager.ensureServicePort(
|
78
|
+
req.kapeta.systemId,
|
79
|
+
req.kapeta.instanceId,
|
80
|
+
req.params.type
|
81
|
+
));
|
82
|
+
});
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Used by services to get info for consumed operator resource.
|
86
|
+
*
|
87
|
+
* If the operator resource is not already available this will cause it to start an instance and
|
88
|
+
* assign port numbers to it etc.
|
89
|
+
*/
|
90
|
+
router.get('/consumes/resource/:resourceType/:portType/:name', async (req, res) => {
|
91
|
+
const operatorInfo = await operatorManager.getResourceInfo(
|
92
|
+
req.kapeta.systemId,
|
93
|
+
req.kapeta.instanceId,
|
94
|
+
req.params.resourceType,
|
95
|
+
req.params.portType,
|
96
|
+
req.params.name
|
97
|
+
);
|
98
|
+
|
99
|
+
res.send(operatorInfo);
|
100
|
+
});
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Used by services to get address for their clients.
|
104
|
+
*
|
105
|
+
* If the remote service is not already registered with a port - we do that here
|
106
|
+
* to handle clients for services that hasn't started yet.
|
107
|
+
*/
|
108
|
+
router.get('/consumes/:resourceName/:type', (req, res) => {
|
109
|
+
|
110
|
+
res.send(serviceManager.getConsumerAddress(
|
111
|
+
req.kapeta.systemId,
|
112
|
+
req.kapeta.instanceId,
|
113
|
+
req.params.resourceName,
|
114
|
+
req.params.type
|
115
|
+
));
|
116
|
+
});
|
117
|
+
|
118
|
+
module.exports = router;
|
@@ -0,0 +1,128 @@
|
|
1
|
+
const _ = require('lodash');
|
2
|
+
const storageService = require('./storageService');
|
3
|
+
const assetManager = require('./assetManager');
|
4
|
+
const {parseKapetaUri} = require("@kapeta/nodejs-utils");
|
5
|
+
|
6
|
+
class ConfigManager {
|
7
|
+
|
8
|
+
constructor() {
|
9
|
+
this._config = storageService.section('config');
|
10
|
+
}
|
11
|
+
|
12
|
+
_forSystem(systemId) {
|
13
|
+
if (!this._config[systemId]) {
|
14
|
+
this._config[systemId] = {};
|
15
|
+
}
|
16
|
+
|
17
|
+
return this._config[systemId];
|
18
|
+
}
|
19
|
+
|
20
|
+
setConfigForService(systemId, serviceId, config) {
|
21
|
+
const systemConfig = this._forSystem(systemId);
|
22
|
+
systemConfig[serviceId] = config || {};
|
23
|
+
|
24
|
+
storageService.put('config', systemId, systemConfig);
|
25
|
+
}
|
26
|
+
|
27
|
+
getConfigForService(systemId, serviceId) {
|
28
|
+
const systemConfig = this._forSystem(systemId);
|
29
|
+
|
30
|
+
if (!systemConfig[serviceId]) {
|
31
|
+
systemConfig[serviceId] = {};
|
32
|
+
}
|
33
|
+
|
34
|
+
if (!systemConfig[serviceId].kapeta) {
|
35
|
+
systemConfig[serviceId].kapeta = {};
|
36
|
+
}
|
37
|
+
|
38
|
+
return systemConfig[serviceId];
|
39
|
+
}
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Try to identify the plan and instance in a plan automatically based on the block reference
|
43
|
+
*
|
44
|
+
* It will:
|
45
|
+
* 1. Go through all plans available in the assets
|
46
|
+
* 2. Look through each plan and see if the plan is referencing the block
|
47
|
+
* 3. If only 1 plan references the block - assume that as the system id
|
48
|
+
* 4. If only 1 instance in 1 plan references the block - assume that as instance id
|
49
|
+
*
|
50
|
+
* In case multiple uses of the same block reference we will prompt to user to choose which instance they want to
|
51
|
+
* use.
|
52
|
+
*
|
53
|
+
* @param blockRef block reference
|
54
|
+
* @param [systemId] plan reference
|
55
|
+
* @returns {Promise<{systemId:string,instanceId:string}>}
|
56
|
+
*/
|
57
|
+
async resolveIdentity(blockRef, systemId) {
|
58
|
+
const planAssets = assetManager.getPlans();
|
59
|
+
|
60
|
+
const blockUri = parseKapetaUri(blockRef);
|
61
|
+
|
62
|
+
let matchingIdentities = [];
|
63
|
+
planAssets.forEach((planAsset) => {
|
64
|
+
if (systemId && planAsset.ref !== systemId) {
|
65
|
+
//Skip plans that do not match systemid if provided
|
66
|
+
return;
|
67
|
+
}
|
68
|
+
|
69
|
+
if (!planAsset.data.spec.blocks) {
|
70
|
+
return;
|
71
|
+
}
|
72
|
+
|
73
|
+
planAsset.data.spec.blocks.forEach((blockInstance) => {
|
74
|
+
const refUri = parseKapetaUri(blockInstance.block.ref);
|
75
|
+
if (refUri.equals(blockUri)) {
|
76
|
+
matchingIdentities.push({
|
77
|
+
systemId: planAsset.ref,
|
78
|
+
instanceId: blockInstance.id
|
79
|
+
});
|
80
|
+
}
|
81
|
+
});
|
82
|
+
});
|
83
|
+
|
84
|
+
if (matchingIdentities.length === 0) {
|
85
|
+
if (systemId) {
|
86
|
+
throw new Error(`No uses of block "${blockRef}" was found in plan: "${systemId}"`)
|
87
|
+
}
|
88
|
+
|
89
|
+
throw new Error(`No uses of block "${blockRef}" was found any known plan`);
|
90
|
+
}
|
91
|
+
|
92
|
+
if (matchingIdentities.length > 1) {
|
93
|
+
if (systemId) {
|
94
|
+
throw new Error(`Multiple uses of block "${blockRef}" was found in plan: "${systemId}". Please specify which instance in the plan you wish to run.`)
|
95
|
+
}
|
96
|
+
|
97
|
+
throw new Error(`Multiple uses of block "${blockRef}" was found in 1 or more plan. Please specify which instance in which plan you wish to run.`);
|
98
|
+
}
|
99
|
+
|
100
|
+
|
101
|
+
return matchingIdentities[0];
|
102
|
+
}
|
103
|
+
|
104
|
+
async verifyIdentity(blockRef, systemId, instanceId) {
|
105
|
+
const planAssets = await assetManager.getPlans();
|
106
|
+
|
107
|
+
let found = false;
|
108
|
+
planAssets.forEach((planAsset) => {
|
109
|
+
if (planAsset.ref !== systemId) {
|
110
|
+
//Skip plans that do not match systemid if provided
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
planAsset.data.spec.blocks.forEach((blockInstance) => {
|
115
|
+
if (blockInstance.id === instanceId &&
|
116
|
+
blockInstance.block.ref === blockRef) {
|
117
|
+
found = true;
|
118
|
+
}
|
119
|
+
});
|
120
|
+
});
|
121
|
+
|
122
|
+
if (!found) {
|
123
|
+
throw new Error(`Block "${blockRef}" was not found in plan: "${systemId}" using instance id ${instanceId}. Please verify that the provided information is accurate.`);
|
124
|
+
}
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
module.exports = new ConfigManager();
|