@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.
@@ -0,0 +1,180 @@
1
+ const ClusterConfiguration = require('@kapeta/local-cluster-config');
2
+ const serviceManager = require('./serviceManager');
3
+ const storageService = require('./storageService');
4
+ const containerManager = require('./containerManager');
5
+ const _ = require('lodash');
6
+ const mkdirp = require('mkdirp');
7
+ const Path = require('path');
8
+ const md5 = require('md5');
9
+
10
+ const KIND_OPERATOR = 'core/resource-type-operator';
11
+
12
+ class Operator {
13
+ constructor(data) {
14
+ this._data = data;
15
+ }
16
+
17
+ getData() {
18
+ return this._data;
19
+ }
20
+
21
+ getCredentials() {
22
+ return this._data.credentials;
23
+ }
24
+ }
25
+
26
+ class OperatorManager {
27
+
28
+ constructor() {
29
+ this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
30
+
31
+ mkdirp.sync(this._mountDir);
32
+ }
33
+
34
+ _getMountPoint(operatorType, mountName) {
35
+ return Path.join(this._mountDir, operatorType, mountName);
36
+ }
37
+
38
+ /**
39
+ * Get operator definition for resource type
40
+ *
41
+ * @param resourceType
42
+ * @return {Operator}
43
+ */
44
+ getOperator(resourceType) {
45
+ const operators = ClusterConfiguration.getDefinitions(KIND_OPERATOR);
46
+
47
+ const operator = _.find(operators, (operator) => operator.definition &&
48
+ operator.definition.metadata &&
49
+ operator.definition.metadata.name &&
50
+ operator.definition.metadata.name.toLowerCase() === resourceType.toLowerCase());
51
+
52
+ if (!operator) {
53
+ throw new Error('Unknown resource type: ' + resourceType);
54
+ }
55
+
56
+ if (!operator.definition.spec ||
57
+ !operator.definition.spec.local) {
58
+ throw new Error('Operator missing local definition: ' + resourceType);
59
+ }
60
+
61
+ return new Operator(operator.definition.spec.local);
62
+ }
63
+
64
+ /**
65
+ * Get information about a specific resource
66
+ *
67
+ * @param {string} systemId
68
+ * @param {string} fromServiceId
69
+ * @param {string} resourceType
70
+ * @param {string} portType
71
+ * @param {string} name
72
+ * @returns {Promise<{host: string, port: (*|string), type: *, protocol: *, credentials: *}>}
73
+ */
74
+ async getResourceInfo(systemId, fromServiceId, resourceType, portType, name) {
75
+
76
+ const operator = this.getOperator(resourceType);
77
+
78
+ const credentials = operator.getCredentials();
79
+
80
+ const container = await this.ensureResource(systemId, resourceType);
81
+
82
+ const portInfo = await container.getPort(portType);
83
+
84
+ if (!portInfo) {
85
+ throw new Error('Unknown resource port type : ' + resourceType + '#' + portType);
86
+ }
87
+
88
+ const dbName = name + '_' + fromServiceId.replace(/[^a-z0-9]/gi, '');
89
+
90
+ return {
91
+ host: 'localhost',
92
+ port: portInfo.hostPort,
93
+ type: portType,
94
+ protocol: portInfo.protocol,
95
+ options: {
96
+ dbName
97
+ },
98
+ credentials
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Ensure we have a running operator of given type
104
+ *
105
+ * @param systemId
106
+ * @param resourceType
107
+ * @return {Promise<ContainerInfo>}
108
+ */
109
+ async ensureResource(systemId, resourceType) {
110
+ const operator = this.getOperator(resourceType);
111
+
112
+ const operatorData = operator.getData();
113
+
114
+ const portTypes = Object.keys(operatorData.ports);
115
+
116
+ portTypes.sort();
117
+
118
+ const containerBaseName = 'kapeta-resource';
119
+
120
+ const nameParts = [resourceType.toLowerCase()];
121
+
122
+ const ports = {};
123
+
124
+ for(let i = 0 ; i < portTypes.length; i++) {
125
+ const portType = portTypes[i];
126
+ let containerPortInfo = operatorData.ports[portType];
127
+ const hostPort = await serviceManager.ensureServicePort(resourceType, portType);
128
+
129
+ if (typeof containerPortInfo === 'number' ||
130
+ typeof containerPortInfo === 'string') {
131
+ containerPortInfo = {port: containerPortInfo, type: 'tcp'};
132
+ }
133
+
134
+ if (!containerPortInfo.type) {
135
+ containerPortInfo.type = 'tcp';
136
+ }
137
+
138
+ const portId = containerPortInfo.port + '/' + containerPortInfo.type;
139
+ nameParts.push(portType + '-' + portId + '-' + hostPort);
140
+
141
+ ports[portId] = {
142
+ type: portType,
143
+ hostPort
144
+ };
145
+ }
146
+
147
+ const mounts = {};
148
+
149
+ _.forEach(operatorData.mounts, (containerPath, mountName) => {
150
+ const hostPath = this._getMountPoint(resourceType, mountName);
151
+ mkdirp.sync(hostPath);
152
+ mounts[containerPath] = hostPath;
153
+ });
154
+
155
+ const containerName = containerBaseName + '-' + md5(nameParts.join('_'));
156
+ let container = await containerManager.get(containerName);
157
+
158
+ const isRunning = container ? await container.isRunning() : false;
159
+ if (container && !isRunning) {
160
+ await container.start();
161
+ }
162
+
163
+ if (!container) {
164
+
165
+ container = await containerManager.run(
166
+ operatorData.image,
167
+ containerName,
168
+ {
169
+ mounts,
170
+ ports,
171
+ health: operatorData.health,
172
+ env: operatorData.env
173
+ });
174
+ }
175
+
176
+ return container;
177
+ }
178
+ }
179
+
180
+ module.exports = new OperatorManager();
@@ -0,0 +1,84 @@
1
+ const _ = require('lodash');
2
+ const FS = require('fs');
3
+ const Path = require('path');
4
+ const Glob = require("glob");
5
+
6
+ const ClusterConfiguration = require('@kapeta/local-cluster-config');
7
+
8
+ class ProviderManager {
9
+
10
+ constructor() {
11
+ this._assetCache = {};
12
+ }
13
+
14
+ getWebProviders() {
15
+ return ClusterConfiguration
16
+ .getProviderDefinitions()
17
+ .filter((providerDefinition) => providerDefinition.hasWeb)
18
+ }
19
+
20
+ getWebAssets() {
21
+ const webProviders = this.getWebProviders();
22
+
23
+ let providerFiles = [];
24
+ webProviders.map((webProvider) => {
25
+ return Glob.sync('web/**/*.js', {cwd: webProvider.path}).map((file) => {
26
+ return {webProvider, file};
27
+ });
28
+ }).forEach((webFiles) => {
29
+ providerFiles.push(...webFiles);
30
+ });
31
+
32
+ return providerFiles;
33
+ }
34
+
35
+ loadAssets() {
36
+ this.getWebAssets().forEach((asset) => {
37
+ const providerId = asset.webProvider.definition.metadata.name;
38
+ const file = asset.file;
39
+ const assetId = `${providerId}/${asset.webProvider.version}/${file}`;
40
+ this._assetCache[assetId] = Path.join(asset.webProvider.path, file);
41
+ })
42
+ }
43
+
44
+
45
+ /**
46
+ * Returns all public (web) javascript for available providers.
47
+ *
48
+ * Provides frontend / applications with the implementation of the frontends for the
49
+ * providers.
50
+ *
51
+ */
52
+ getPublicJS() {
53
+ this.loadAssets();
54
+ const includes = Object.keys(this._assetCache).map((assetId) => {
55
+ return `${ClusterConfiguration.getClusterServiceAddress()}/providers/asset/${assetId}`
56
+ });
57
+
58
+ return `Kapeta.setPluginPaths(${JSON.stringify(includes)});`
59
+ }
60
+
61
+ getAsset(id) {
62
+ if (_.isEmpty(this._assetCache)) {
63
+ this.loadAssets();
64
+ }
65
+ if (this._assetCache[id]) {
66
+ return FS.readFileSync(this._assetCache[id]).toString();
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+
72
+ const providerDefinitions = ClusterConfiguration.getProviderDefinitions();
73
+
74
+ if (providerDefinitions.length > 0) {
75
+ console.log('## Loaded the following providers ##');
76
+ providerDefinitions.forEach(providerDefinition => {
77
+ console.log(' - %s[%s:%s]', providerDefinition.definition.kind, providerDefinition.definition.metadata.name, providerDefinition.version);
78
+ console.log(' from %s', providerDefinition.path);
79
+ })
80
+ } else {
81
+ console.log('## No providers found ##');
82
+ }
83
+
84
+ module.exports = new ProviderManager();
@@ -0,0 +1,26 @@
1
+ const Router = require('express-promise-router').default;
2
+ const providerManager = require('../providerManager');
3
+
4
+ const router = new Router();
5
+
6
+ router.use('/', require('../middleware/cors'));
7
+
8
+ /**
9
+ * Get all local assets available
10
+ */
11
+ router.get('/all.js', (req, res) => {
12
+ res.send(providerManager.getPublicJS());
13
+ });
14
+
15
+ router.get('/asset/*', (req, res) => {
16
+ const assetId = req.params[0];
17
+ const result = providerManager.getAsset(assetId)
18
+ if (!result) {
19
+ res.status(404).send('');
20
+ } else {
21
+ res.send(result);
22
+ }
23
+ });
24
+
25
+
26
+ module.exports = router;
@@ -0,0 +1,125 @@
1
+ const Router = require('express-promise-router').default;
2
+ const _ = require('lodash');
3
+
4
+ const router = new Router();
5
+ const serviceManager = require('../serviceManager');
6
+ const clusterService = require('../clusterService');
7
+ const assetManager = require('../assetManager');
8
+
9
+ /**
10
+ * @var {{[key:string]:ProxyRequestHandler}}
11
+ */
12
+ const TYPE_HANDLERS = {
13
+ rest: require('./types/rest'),
14
+ web: require('./types/web')
15
+ };
16
+
17
+ function getResource(resources, resourceName) {
18
+ return _.find(resources, (resource) => {
19
+ return (resource.metadata.name.toLowerCase() === resourceName.toLowerCase());
20
+ });
21
+ }
22
+
23
+ router.use('/:systemId/:consumerInstanceId/:consumerResourceName', require('../middleware/stringBody'));
24
+
25
+ router.all('/:systemId/:consumerInstanceId/:consumerResourceName/:type/*', async (req, res) => {
26
+
27
+ try {
28
+
29
+ const typeHandler = TYPE_HANDLERS[req.params.type.toLowerCase()];
30
+ if (!typeHandler) {
31
+ res.status(401).send({error: 'Unknown connection type: ' + req.params.type});
32
+ return;
33
+ }
34
+
35
+ const plan = await assetManager.getPlan(req.params.systemId);
36
+
37
+ // We can find the connection by the consumer information alone since
38
+ // only 1 provider can be connected to a consumer resource at a time
39
+ 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();
42
+ });
43
+
44
+ if (!connection) {
45
+ res.status(401).send({error:`No connection found for consumer "${req.params.consumerInstanceId}::${req.params.consumerResourceName}"`});
46
+ return;
47
+ }
48
+
49
+ const toBlockInstance = _.find(plan.spec.blocks, (blockInstance) => {
50
+ return blockInstance.id.toLowerCase() === connection.to.blockId.toLowerCase();
51
+ });
52
+
53
+ if (!toBlockInstance) {
54
+ res.status(401).send({error:`Block instance not found "${req.params.consumerInstanceId}`});
55
+ return;
56
+ }
57
+
58
+ const toBlockAsset = await assetManager.getAsset(toBlockInstance.block.ref);
59
+
60
+ const toResource = getResource(toBlockAsset.data.spec.consumers, req.params.consumerResourceName);
61
+
62
+ if (!toResource) {
63
+ res.status(401).send({error:`Block resource not found "${req.params.consumerInstanceId}::${req.params.consumerResourceName}`});
64
+ return;
65
+ }
66
+
67
+ const basePath = clusterService.getProxyPath(
68
+ req.params.systemId,
69
+ req.params.consumerInstanceId,
70
+ req.params.consumerResourceName,
71
+ req.params.type
72
+ );
73
+
74
+ /*
75
+ Get the path the consumer requested.
76
+ Note that this might not match the path the destination is expecting so we need to identify the method
77
+ that is being called and identify the destination path from the connection.
78
+ */
79
+ const consumerPath = req.originalUrl.substr(basePath.length - 1);
80
+
81
+ const fromBlockInstance = _.find(plan.spec.blocks, (blockInstance) => {
82
+ return blockInstance.id.toLowerCase() === connection.from.blockId.toLowerCase();
83
+ });
84
+
85
+ if (!fromBlockInstance) {
86
+ res.status(401).send({error:`Block instance not found "${connection.from.blockId}`});
87
+ return;
88
+ }
89
+
90
+ const fromBlockAsset = await assetManager.getAsset(fromBlockInstance.block.ref);
91
+
92
+ const fromResource = getResource(fromBlockAsset.data.spec.providers, connection.from.resourceName);
93
+
94
+ if (!fromResource) {
95
+ res.status(401).send({error:`Block resource not found "${connection.from.blockId}::${connection.from.resourceName}`});
96
+ return;
97
+ }
98
+
99
+
100
+ //Get target address
101
+ let address = await serviceManager.getProviderAddress(
102
+ req.params.systemId,
103
+ connection.from.blockId,
104
+ req.params.type
105
+ );
106
+
107
+ while(address.endsWith('/')) {
108
+ address = address.substr(0, address.length - 1);
109
+ }
110
+
111
+ typeHandler(req, res, {
112
+ consumerPath,
113
+ address,
114
+ toResource,
115
+ fromResource,
116
+ connection
117
+ });
118
+
119
+ } catch(err) {
120
+ res.status(400).send({error: err.message});
121
+ }
122
+
123
+ });
124
+
125
+ module.exports = router;
@@ -0,0 +1,163 @@
1
+ const request = require('request');
2
+ const Path = require('path');
3
+ const _ = require('lodash');
4
+
5
+ const pathTemplateParser = require('../../utils/pathTemplateParser');
6
+ const networkManager = require('../../networkManager');
7
+ const socketManager = require('../../socketManager');
8
+
9
+
10
+ function getRestMethodId(restResource, httpMethod, httpPath) {
11
+ return _.findKey(restResource.spec.methods, (method) => {
12
+ let methodType = method.method ? method.method.toUpperCase() : 'GET';
13
+
14
+ if (methodType !== httpMethod.toUpperCase()) {
15
+ return false;
16
+ }
17
+
18
+ let path = method.path;
19
+
20
+ if (restResource.spec.basePath) {
21
+ path = Path.join(restResource.spec.basePath, path);
22
+ }
23
+
24
+ const pathTemplate = pathTemplateParser(path);
25
+
26
+ return pathTemplate.matches(httpPath);
27
+
28
+ });
29
+ }
30
+
31
+ /**
32
+ *
33
+ * @param req {Request}
34
+ * @param opts {ProxyRequestInfo}
35
+ * @return {{consumerMethod: *, providerMethod: *}}
36
+ */
37
+ function resolveMethods(req, opts) {
38
+ const consumerMethodId = getRestMethodId(opts.toResource, req.method, opts.consumerPath);
39
+
40
+ if (!consumerMethodId) {
41
+ throw new Error(
42
+ `Consumer method not found for path "${req.method} ${opts.consumerPath}" in resource "${req.params.consumerInstanceId}::${req.params.consumerResourceName}`
43
+ );
44
+ }
45
+
46
+ const consumerMethod = _.cloneDeep(opts.toResource.spec.methods[consumerMethodId]);
47
+
48
+ if (!consumerMethod) {
49
+ throw new Error(
50
+ `Consumer method not found for path "${req.method} ${opts.consumerPath}" in resource "${req.params.consumerInstanceId}::${req.params.consumerResourceName}`
51
+ );
52
+ }
53
+
54
+ consumerMethod.id = consumerMethodId;
55
+
56
+ const providerMethodId = _.findKey(opts.connection.mapping, (mapping) => {
57
+ return mapping.targetId === consumerMethodId;
58
+ });
59
+
60
+ if (!providerMethodId) {
61
+ throw new Error(`Connection contained no mapping for consumer method "${consumerMethodId}`);
62
+ }
63
+
64
+ const providerMethod = _.cloneDeep(opts.fromResource.spec.methods[providerMethodId]);
65
+
66
+ if (!providerMethod) {
67
+ throw new Error(
68
+ `Provider method not found "${providerMethodId}" in resource "${opts.connection.from.blockId}::${opts.connection.from.resourceName}`
69
+ );
70
+ }
71
+
72
+ providerMethod.id = providerMethodId;
73
+
74
+ return {
75
+ consumerMethod,
76
+ providerMethod
77
+ };
78
+ }
79
+
80
+ /**
81
+ *
82
+ * @param req {Request}
83
+ * @param res {Response}
84
+ * @param opts {ProxyRequestInfo}
85
+ */
86
+ module.exports = function proxyRestRequest(req, res, opts) {
87
+
88
+ let {consumerMethod, providerMethod} = resolveMethods(req, opts);
89
+
90
+ const consumerPathTemplate = pathTemplateParser(consumerMethod.path);
91
+ const providerPathTemplate = pathTemplateParser(providerMethod.path);
92
+
93
+ const pathVariables = consumerPathTemplate.parse(opts.consumerPath);
94
+
95
+ let providerPath = providerPathTemplate.create(pathVariables);
96
+
97
+ if (!providerPath.startsWith('/')) {
98
+ providerPath = '/' + providerPath;
99
+ }
100
+
101
+ const requestHeaders = _.clone(req.headers);
102
+
103
+ delete requestHeaders['content-length'];
104
+ delete requestHeaders['content-encoding'];
105
+ delete requestHeaders['connection'];
106
+ delete requestHeaders['host'];
107
+ delete requestHeaders['origin'];
108
+
109
+
110
+ console.log('Route to provider: %s => %s', opts.consumerPath, opts.address + providerPath);
111
+
112
+ const reqOpts = {
113
+ method: providerMethod.method || 'GET',
114
+ url: opts.address + providerPath,
115
+ body: req.stringBody,
116
+ headers: requestHeaders
117
+ };
118
+
119
+ const traffic = networkManager.addRequest(
120
+ req.params.systemId,
121
+ opts.connection,
122
+ reqOpts,
123
+ consumerMethod.id,
124
+ providerMethod.id
125
+ );
126
+
127
+ socketManager.emit(traffic.connectionId, 'traffic_start', traffic);
128
+
129
+ request(reqOpts, function(err, response, responseBody) {
130
+ if (err) {
131
+ traffic.asError(err);
132
+ socketManager.emit(traffic.connectionId, 'traffic_end', traffic);
133
+
134
+ res.status(500).send({error: '' + err});
135
+ return;
136
+ }
137
+
138
+ const responseHeaders = _.clone(response.headers);
139
+
140
+ delete responseHeaders['content-length'];
141
+ delete responseHeaders['content-encoding'];
142
+ delete responseHeaders['connection'];
143
+
144
+ res.set(responseHeaders);
145
+
146
+ res.status(response.statusCode);
147
+
148
+ traffic.withResponse({
149
+ code: response.statusCode,
150
+ headers: response.headers,
151
+ body: responseBody
152
+ });
153
+
154
+ socketManager.emit(traffic.connectionId, 'traffic_end', traffic);
155
+
156
+ if (responseBody) {
157
+ res.send(responseBody);
158
+ } else {
159
+ res.end();
160
+ }
161
+ });
162
+
163
+ };
@@ -0,0 +1,83 @@
1
+ const request = require('request');
2
+ const _ = require('lodash');
3
+
4
+ const networkManager = require('../../networkManager');
5
+ const socketManager = require('../../socketManager');
6
+
7
+ /**
8
+ *
9
+ * @param req {Request}
10
+ * @param res {Response}
11
+ * @param opts {ProxyRequestInfo}
12
+ */
13
+ module.exports = function proxyRestRequest(req, res, opts) {
14
+
15
+ console.log('Route to provider: %s => %s', opts.consumerPath, opts.address);
16
+
17
+ const requestHeaders = _.clone(req.headers);
18
+
19
+ delete requestHeaders['content-length'];
20
+ delete requestHeaders['content-encoding'];
21
+ delete requestHeaders['connection'];
22
+ delete requestHeaders['host'];
23
+ delete requestHeaders['origin'];
24
+
25
+ const sourceBasePath = opts.fromResource.spec.path;
26
+ const targetBasePath = opts.toResource.spec.path;
27
+ let path = opts.consumerPath;
28
+ if (opts.consumerPath.startsWith(sourceBasePath)) {
29
+ path = path.replace(sourceBasePath, targetBasePath);
30
+ }
31
+
32
+
33
+ const reqOpts = {
34
+ method: req.method,
35
+ url: opts.address + path,
36
+ headers: requestHeaders,
37
+ body: req.stringBody
38
+ };
39
+
40
+ console.log('reqOpts', reqOpts);
41
+
42
+ const traffic = networkManager.addRequest(
43
+ req.params.systemId,
44
+ opts.connection,
45
+ reqOpts
46
+ );
47
+
48
+ socketManager.emit(traffic.connectionId, 'traffic_start', traffic);
49
+
50
+ request(reqOpts, function(err, response, responseBody) {
51
+ if (err) {
52
+ traffic.asError(err);
53
+ socketManager.emit(traffic.connectionId, 'traffic_end', traffic);
54
+
55
+ res.status(500).send({error: '' + err});
56
+ return;
57
+ }
58
+
59
+ const responseHeaders = _.clone(response.headers);
60
+
61
+ delete responseHeaders['content-length'];
62
+ delete responseHeaders['content-encoding'];
63
+ delete responseHeaders['connection'];
64
+
65
+ res.set(responseHeaders);
66
+ res.status(response.statusCode);
67
+
68
+ traffic.withResponse({
69
+ code: response.statusCode,
70
+ headers: response.headers,
71
+ body: responseBody
72
+ });
73
+
74
+ socketManager.emit(traffic.connectionId, 'traffic_end', traffic);
75
+
76
+ if (responseBody) {
77
+ res.send(responseBody);
78
+ } else {
79
+ res.end();
80
+ }
81
+ });
82
+
83
+ };