@kapeta/local-cluster-service 0.6.1 → 0.7.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.
Files changed (192) hide show
  1. package/.eslintrc.cjs +17 -0
  2. package/.github/workflows/main.yml +22 -22
  3. package/.prettierignore +4 -0
  4. package/.vscode/launch.json +2 -4
  5. package/CHANGELOG.md +14 -0
  6. package/definitions.d.ts +17 -35
  7. package/dist/cjs/index.d.ts +27 -0
  8. package/dist/cjs/index.js +126 -0
  9. package/dist/cjs/package.json +1 -0
  10. package/dist/cjs/src/assetManager.d.ts +31 -0
  11. package/dist/cjs/src/assetManager.js +153 -0
  12. package/dist/cjs/src/assets/routes.d.ts +3 -0
  13. package/dist/cjs/src/assets/routes.js +117 -0
  14. package/dist/cjs/src/clusterService.d.ts +40 -0
  15. package/dist/cjs/src/clusterService.js +114 -0
  16. package/dist/cjs/src/codeGeneratorManager.d.ts +8 -0
  17. package/dist/cjs/src/codeGeneratorManager.js +53 -0
  18. package/dist/cjs/src/config/routes.d.ts +3 -0
  19. package/dist/cjs/src/config/routes.js +126 -0
  20. package/dist/cjs/src/configManager.d.ts +36 -0
  21. package/dist/cjs/src/configManager.js +110 -0
  22. package/dist/cjs/src/containerManager.d.ts +89 -0
  23. package/dist/cjs/src/containerManager.js +365 -0
  24. package/dist/cjs/src/filesystem/routes.d.ts +3 -0
  25. package/dist/cjs/src/filesystem/routes.js +69 -0
  26. package/dist/cjs/src/filesystemManager.d.ts +15 -0
  27. package/dist/cjs/src/filesystemManager.js +87 -0
  28. package/dist/cjs/src/identities/routes.d.ts +3 -0
  29. package/dist/cjs/src/identities/routes.js +18 -0
  30. package/dist/cjs/src/instanceManager.d.ts +56 -0
  31. package/dist/cjs/src/instanceManager.js +424 -0
  32. package/dist/cjs/src/instances/routes.d.ts +3 -0
  33. package/dist/cjs/src/instances/routes.js +134 -0
  34. package/dist/cjs/src/middleware/cors.d.ts +2 -0
  35. package/dist/cjs/src/middleware/cors.js +10 -0
  36. package/dist/cjs/src/middleware/kapeta.d.ts +11 -0
  37. package/dist/cjs/src/middleware/kapeta.js +17 -0
  38. package/dist/cjs/src/middleware/stringBody.d.ts +5 -0
  39. package/dist/cjs/src/middleware/stringBody.js +14 -0
  40. package/dist/cjs/src/networkManager.d.ts +32 -0
  41. package/dist/cjs/src/networkManager.js +109 -0
  42. package/dist/cjs/src/operatorManager.d.ts +36 -0
  43. package/dist/cjs/src/operatorManager.js +165 -0
  44. package/dist/cjs/src/progressListener.d.ts +20 -0
  45. package/dist/cjs/src/progressListener.js +91 -0
  46. package/dist/cjs/src/providerManager.d.ts +9 -0
  47. package/dist/cjs/src/providerManager.js +51 -0
  48. package/dist/cjs/src/providers/routes.d.ts +3 -0
  49. package/dist/cjs/src/providers/routes.js +42 -0
  50. package/dist/cjs/src/proxy/routes.d.ts +3 -0
  51. package/dist/cjs/src/proxy/routes.js +111 -0
  52. package/dist/cjs/src/proxy/types/rest.d.ts +4 -0
  53. package/dist/cjs/src/proxy/types/rest.js +114 -0
  54. package/dist/cjs/src/proxy/types/web.d.ts +4 -0
  55. package/dist/cjs/src/proxy/types/web.js +53 -0
  56. package/dist/cjs/src/repositoryManager.d.ts +17 -0
  57. package/dist/cjs/src/repositoryManager.js +218 -0
  58. package/dist/cjs/src/serviceManager.d.ts +29 -0
  59. package/dist/cjs/src/serviceManager.js +99 -0
  60. package/dist/cjs/src/socketManager.d.ts +14 -0
  61. package/dist/cjs/src/socketManager.js +53 -0
  62. package/dist/cjs/src/storageService.d.ts +17 -0
  63. package/dist/cjs/src/storageService.js +74 -0
  64. package/dist/cjs/src/traffic/routes.d.ts +3 -0
  65. package/dist/cjs/src/traffic/routes.js +18 -0
  66. package/dist/cjs/src/types.d.ts +88 -0
  67. package/dist/cjs/src/types.js +2 -0
  68. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +29 -0
  69. package/dist/cjs/src/utils/BlockInstanceRunner.js +468 -0
  70. package/dist/cjs/src/utils/LogData.d.ts +19 -0
  71. package/dist/cjs/src/utils/LogData.js +43 -0
  72. package/dist/cjs/src/utils/pathTemplateParser.d.ts +26 -0
  73. package/dist/cjs/src/utils/pathTemplateParser.js +121 -0
  74. package/dist/cjs/src/utils/utils.d.ts +1 -0
  75. package/dist/cjs/src/utils/utils.js +18 -0
  76. package/dist/cjs/start.d.ts +1 -0
  77. package/dist/cjs/start.js +12 -0
  78. package/dist/esm/index.d.ts +27 -0
  79. package/dist/esm/index.js +121 -0
  80. package/dist/esm/package.json +1 -0
  81. package/dist/esm/src/assetManager.d.ts +31 -0
  82. package/{src → dist/esm/src}/assetManager.js +22 -60
  83. package/dist/esm/src/assets/routes.d.ts +3 -0
  84. package/{src → dist/esm/src}/assets/routes.js +21 -36
  85. package/dist/esm/src/clusterService.d.ts +40 -0
  86. package/{src → dist/esm/src}/clusterService.js +14 -37
  87. package/dist/esm/src/codeGeneratorManager.d.ts +8 -0
  88. package/{src → dist/esm/src}/codeGeneratorManager.js +15 -24
  89. package/dist/esm/src/config/routes.d.ts +3 -0
  90. package/{src → dist/esm/src}/config/routes.js +40 -89
  91. package/dist/esm/src/configManager.d.ts +36 -0
  92. package/{src → dist/esm/src}/configManager.js +11 -40
  93. package/dist/esm/src/containerManager.d.ts +89 -0
  94. package/{src → dist/esm/src}/containerManager.js +81 -182
  95. package/dist/esm/src/filesystem/routes.d.ts +3 -0
  96. package/dist/esm/src/filesystem/routes.js +64 -0
  97. package/dist/esm/src/filesystemManager.d.ts +15 -0
  98. package/{src → dist/esm/src}/filesystemManager.js +20 -28
  99. package/dist/esm/src/identities/routes.d.ts +3 -0
  100. package/dist/esm/src/identities/routes.js +13 -0
  101. package/dist/esm/src/instanceManager.d.ts +56 -0
  102. package/{src → dist/esm/src}/instanceManager.js +88 -179
  103. package/dist/esm/src/instances/routes.d.ts +3 -0
  104. package/{src → dist/esm/src}/instances/routes.js +31 -70
  105. package/dist/esm/src/middleware/cors.d.ts +2 -0
  106. package/{src → dist/esm/src}/middleware/cors.js +2 -3
  107. package/dist/esm/src/middleware/kapeta.d.ts +11 -0
  108. package/{src → dist/esm/src}/middleware/kapeta.js +3 -7
  109. package/dist/esm/src/middleware/stringBody.d.ts +5 -0
  110. package/{src → dist/esm/src}/middleware/stringBody.js +2 -3
  111. package/dist/esm/src/networkManager.d.ts +32 -0
  112. package/{src → dist/esm/src}/networkManager.js +16 -33
  113. package/dist/esm/src/operatorManager.d.ts +36 -0
  114. package/{src → dist/esm/src}/operatorManager.js +35 -91
  115. package/dist/esm/src/progressListener.d.ts +20 -0
  116. package/dist/esm/src/progressListener.js +88 -0
  117. package/dist/esm/src/providerManager.d.ts +9 -0
  118. package/dist/esm/src/providerManager.js +45 -0
  119. package/dist/esm/src/providers/routes.d.ts +3 -0
  120. package/{src → dist/esm/src}/providers/routes.js +10 -16
  121. package/dist/esm/src/proxy/routes.d.ts +3 -0
  122. package/dist/esm/src/proxy/routes.js +106 -0
  123. package/dist/esm/src/proxy/types/rest.d.ts +4 -0
  124. package/dist/esm/src/proxy/types/rest.js +107 -0
  125. package/dist/esm/src/proxy/types/web.d.ts +4 -0
  126. package/{src → dist/esm/src}/proxy/types/web.js +13 -35
  127. package/dist/esm/src/repositoryManager.d.ts +17 -0
  128. package/dist/esm/src/repositoryManager.js +212 -0
  129. package/dist/esm/src/serviceManager.d.ts +29 -0
  130. package/{src → dist/esm/src}/serviceManager.js +12 -42
  131. package/dist/esm/src/socketManager.d.ts +14 -0
  132. package/{src → dist/esm/src}/socketManager.js +19 -23
  133. package/dist/esm/src/storageService.d.ts +17 -0
  134. package/{src → dist/esm/src}/storageService.js +8 -27
  135. package/dist/esm/src/traffic/routes.d.ts +3 -0
  136. package/{src → dist/esm/src}/traffic/routes.js +4 -9
  137. package/dist/esm/src/types.d.ts +88 -0
  138. package/dist/esm/src/types.js +1 -0
  139. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +29 -0
  140. package/{src → dist/esm/src}/utils/BlockInstanceRunner.js +137 -256
  141. package/dist/esm/src/utils/LogData.d.ts +19 -0
  142. package/{src → dist/esm/src}/utils/LogData.js +11 -22
  143. package/dist/esm/src/utils/pathTemplateParser.d.ts +26 -0
  144. package/{src → dist/esm/src}/utils/pathTemplateParser.js +21 -40
  145. package/dist/esm/src/utils/utils.d.ts +1 -0
  146. package/dist/esm/src/utils/utils.js +11 -0
  147. package/dist/esm/start.d.ts +1 -0
  148. package/dist/esm/start.js +7 -0
  149. package/index.ts +147 -0
  150. package/package.json +106 -74
  151. package/src/assetManager.ts +191 -0
  152. package/src/assets/routes.ts +132 -0
  153. package/src/clusterService.ts +134 -0
  154. package/src/codeGeneratorManager.ts +57 -0
  155. package/src/config/routes.ts +159 -0
  156. package/src/configManager.ts +148 -0
  157. package/src/containerManager.ts +466 -0
  158. package/src/filesystem/routes.ts +74 -0
  159. package/src/filesystemManager.ts +93 -0
  160. package/src/identities/routes.ts +20 -0
  161. package/src/instanceManager.ts +503 -0
  162. package/src/instances/routes.ts +164 -0
  163. package/src/middleware/cors.ts +9 -0
  164. package/src/middleware/kapeta.ts +27 -0
  165. package/src/middleware/stringBody.ts +16 -0
  166. package/src/networkManager.ts +137 -0
  167. package/src/operatorManager.ts +221 -0
  168. package/src/progressListener.ts +102 -0
  169. package/src/{providerManager.js → providerManager.ts} +15 -31
  170. package/src/providers/routes.ts +46 -0
  171. package/src/proxy/routes.ts +148 -0
  172. package/src/proxy/types/{rest.js → rest.ts} +30 -30
  173. package/src/proxy/types/web.ts +60 -0
  174. package/src/{repositoryManager.js → repositoryManager.ts} +49 -73
  175. package/src/serviceManager.ts +120 -0
  176. package/src/socketManager.ts +57 -0
  177. package/src/storageService.ts +88 -0
  178. package/src/traffic/routes.ts +18 -0
  179. package/src/types.ts +97 -0
  180. package/src/utils/BlockInstanceRunner.ts +555 -0
  181. package/src/utils/LogData.ts +47 -0
  182. package/src/utils/pathTemplateParser.ts +138 -0
  183. package/src/utils/utils.ts +12 -0
  184. package/start.ts +8 -0
  185. package/tsconfig.json +13 -0
  186. package/index.js +0 -127
  187. package/src/filesystem/routes.js +0 -74
  188. package/src/identities/routes.js +0 -19
  189. package/src/progressListener.js +0 -82
  190. package/src/proxy/routes.js +0 -126
  191. package/src/utils/utils.js +0 -13
  192. package/start.js +0 -7
@@ -0,0 +1,148 @@
1
+ import { EnrichedAsset } from './assetManager';
2
+ import { BlockInstance } from '@kapeta/schemas';
3
+ import { storageService } from './storageService';
4
+ import { assetManager } from './assetManager';
5
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
6
+
7
+ type AnyMap = { [key: string]: any };
8
+
9
+ interface MatchedIdentity {
10
+ systemId: string;
11
+ instanceId: string;
12
+ }
13
+
14
+ class ConfigManager {
15
+ private _config: AnyMap;
16
+
17
+ constructor() {
18
+ this._config = storageService.section('config');
19
+ }
20
+
21
+ _forSystem(systemId: string) {
22
+ if (!this._config[systemId]) {
23
+ this._config[systemId] = {};
24
+ }
25
+
26
+ return this._config[systemId];
27
+ }
28
+
29
+ setConfigForSystem(systemId: string, config: AnyMap) {
30
+ const systemConfig = config || {};
31
+
32
+ storageService.put('config', systemId, systemConfig);
33
+ }
34
+
35
+ getConfigForSystem(systemId: string): AnyMap {
36
+ return this._forSystem(systemId);
37
+ }
38
+
39
+ setConfigForSection(systemId: string, sectionId: string, config: AnyMap) {
40
+ let systemConfig = this._forSystem(systemId);
41
+ systemConfig[sectionId] = config || {};
42
+
43
+ storageService.put('config', systemId, systemConfig);
44
+ }
45
+
46
+ getConfigForSection(systemId: string, sectionId: string) {
47
+ const systemConfig = this._forSystem(systemId);
48
+
49
+ if (!systemConfig[sectionId]) {
50
+ systemConfig[sectionId] = {};
51
+ }
52
+
53
+ return systemConfig[sectionId];
54
+ }
55
+
56
+ /**
57
+ * Try to identify the plan and instance in a plan automatically based on the block reference
58
+ *
59
+ * It will:
60
+ * 1. Go through all plans available in the assets
61
+ * 2. Look through each plan and see if the plan is referencing the block
62
+ * 3. If only 1 plan references the block - assume that as the system id
63
+ * 4. If only 1 instance in 1 plan references the block - assume that as instance id
64
+ *
65
+ * In case multiple uses of the same block reference we will prompt to user to choose which instance they want to
66
+ * use.
67
+ *
68
+ * @param blockRef block reference
69
+ * @param [systemId] plan reference
70
+ * @returns {Promise<{systemId:string,instanceId:string}>}
71
+ */
72
+ async resolveIdentity(blockRef: string, systemId?: string) {
73
+ const planAssets = assetManager.getPlans();
74
+
75
+ const blockUri = parseKapetaUri(blockRef);
76
+
77
+ let matchingIdentities: MatchedIdentity[] = [];
78
+ planAssets.forEach((planAsset: EnrichedAsset) => {
79
+ if (systemId && planAsset.ref !== systemId) {
80
+ //Skip plans that do not match systemid if provided
81
+ return;
82
+ }
83
+
84
+ if (!planAsset.data.spec.blocks) {
85
+ return;
86
+ }
87
+
88
+ planAsset.data.spec.blocks.forEach((blockInstance: BlockInstance) => {
89
+ const refUri = parseKapetaUri(blockInstance.block.ref);
90
+ if (refUri.equals(blockUri)) {
91
+ matchingIdentities.push({
92
+ systemId: planAsset.ref,
93
+ instanceId: blockInstance.id,
94
+ });
95
+ }
96
+ });
97
+ });
98
+
99
+ if (matchingIdentities.length === 0) {
100
+ if (systemId) {
101
+ throw new Error(`No uses of block "${blockRef}" was found in plan: "${systemId}"`);
102
+ }
103
+
104
+ throw new Error(`No uses of block "${blockRef}" was found in any known plan`);
105
+ }
106
+
107
+ if (matchingIdentities.length > 1) {
108
+ if (systemId) {
109
+ throw new Error(
110
+ `Multiple uses of block "${blockRef}" was found in plan: "${systemId}". Please specify which instance in the plan you wish to run.`
111
+ );
112
+ }
113
+
114
+ throw new Error(
115
+ `Multiple uses of block "${blockRef}" was found in 1 or more plan. Please specify which instance in which plan you wish to run.`
116
+ );
117
+ }
118
+
119
+ return matchingIdentities[0];
120
+ }
121
+
122
+ async verifyIdentity(blockRef: string, systemId: string, instanceId: string) {
123
+ const planAssets = assetManager.getPlans();
124
+ const systemUri = systemId ? parseKapetaUri(systemId) : null;
125
+ const blockUri = parseKapetaUri(blockRef);
126
+ let found = false;
127
+ planAssets.forEach((planAsset: EnrichedAsset) => {
128
+ if (systemUri && !parseKapetaUri(planAsset.ref).equals(systemUri)) {
129
+ //Skip plans that do not match systemid if provided
130
+ return;
131
+ }
132
+
133
+ planAsset.data.spec.blocks.forEach((blockInstance: BlockInstance) => {
134
+ if (blockInstance.id === instanceId && parseKapetaUri(blockInstance.block.ref).equals(blockUri)) {
135
+ found = true;
136
+ }
137
+ });
138
+ });
139
+
140
+ if (!found) {
141
+ throw new Error(
142
+ `Block "${blockRef}" was not found in plan: "${systemId}" using instance id ${instanceId}. Please verify that the provided information is accurate.`
143
+ );
144
+ }
145
+ }
146
+ }
147
+
148
+ export const configManager = new ConfigManager();
@@ -0,0 +1,466 @@
1
+ import Path from 'path';
2
+ import { storageService } from './storageService';
3
+ import os from 'os';
4
+ import _ from 'lodash';
5
+ import FSExtra, { ReadStream } from 'fs-extra';
6
+ import { Docker } from 'node-docker-api';
7
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
8
+ import ClusterConfiguration from '@kapeta/local-cluster-config';
9
+ import { Container } from 'node-docker-api/lib/container';
10
+
11
+ type StringMap = { [key: string]: string };
12
+
13
+ export type PortMap = {
14
+ [key: string]: {
15
+ containerPort: string;
16
+ protocol: string;
17
+ hostPort: string;
18
+ };
19
+ };
20
+
21
+ export interface DockerMounts {
22
+ Target: string;
23
+ Source: string;
24
+ Type: string;
25
+ ReadOnly: boolean;
26
+ Consistency: string;
27
+ }
28
+
29
+ interface Health {
30
+ cmd: string;
31
+ interval?: number;
32
+ timeout?: number;
33
+ retries?: number;
34
+ }
35
+
36
+ const LABEL_PORT_PREFIX = 'kapeta_port-';
37
+ const NANO_SECOND = 1000000;
38
+ const HEALTH_CHECK_INTERVAL = 2000;
39
+ const HEALTH_CHECK_MAX = 30;
40
+ const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
41
+ const IMAGE_PULL_CACHE: { [key: string]: number } = {};
42
+
43
+ const promisifyStream = (stream: ReadStream) =>
44
+ new Promise((resolve, reject) => {
45
+ stream.on('data', (d) => console.log(d.toString()));
46
+ stream.on('end', resolve);
47
+ stream.on('error', reject);
48
+ });
49
+
50
+ class ContainerManager {
51
+ private _docker: Docker | null;
52
+ private _alive: boolean;
53
+ private _mountDir: string;
54
+
55
+ constructor() {
56
+ this._docker = null;
57
+ this._alive = false;
58
+ this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
59
+ FSExtra.mkdirpSync(this._mountDir);
60
+ }
61
+
62
+ async initialize() {
63
+ // Use the value from cluster-service.yml if configured
64
+ const dockerConfig = ClusterConfiguration.getDockerConfig();
65
+ const connectOptions =
66
+ Object.keys(dockerConfig).length > 0
67
+ ? [dockerConfig]
68
+ : [
69
+ // use defaults: DOCKER_HOST etc from env, if available
70
+ undefined,
71
+ // default linux
72
+ { socketPath: '/var/run/docker.sock' },
73
+ // default macOS
74
+ {
75
+ socketPath: Path.join(os.homedir(), '.docker/run/docker.sock'),
76
+ },
77
+ // Default http
78
+ { protocol: 'http', host: 'localhost', port: 2375 },
79
+ { protocol: 'https', host: 'localhost', port: 2376 },
80
+ { protocol: 'http', host: '127.0.0.1', port: 2375 },
81
+ { protocol: 'https', host: '127.0.0.1', port: 2376 },
82
+ ];
83
+ for (const opts of connectOptions) {
84
+ try {
85
+ const client = new Docker(opts);
86
+ await client.ping();
87
+ this._docker = client;
88
+ this._alive = true;
89
+ return;
90
+ } catch (err) {
91
+ // silently ignore bad configs
92
+ }
93
+ }
94
+
95
+ throw new Error('Could not connect to docker daemon. Please make sure docker is running and working.');
96
+ }
97
+
98
+ isAlive() {
99
+ return this._alive;
100
+ }
101
+
102
+ getMountPoint(kind: string, mountName: string) {
103
+ const kindUri = parseKapetaUri(kind);
104
+ return Path.join(this._mountDir, kindUri.handle, kindUri.name, mountName);
105
+ }
106
+
107
+ createMounts(kind: string, mountOpts: StringMap): StringMap {
108
+ const mounts: StringMap = {};
109
+
110
+ _.forEach(mountOpts, (containerPath, mountName) => {
111
+ const hostPath = this.getMountPoint(kind, mountName);
112
+ FSExtra.mkdirpSync(hostPath);
113
+ mounts[containerPath] = hostPath;
114
+ });
115
+ return mounts;
116
+ }
117
+
118
+ async ping() {
119
+ try {
120
+ const pingResult = await this.docker().ping();
121
+ if (pingResult !== 'OK') {
122
+ throw new Error(`Ping failed: ${pingResult}`);
123
+ }
124
+ } catch (e: any) {
125
+ throw new Error(
126
+ `Docker not running. Please start the docker daemon before running this command. Error: ${e.message}`
127
+ );
128
+ }
129
+ }
130
+
131
+ docker() {
132
+ if (!this._docker) {
133
+ throw new Error(`Docker not running`);
134
+ }
135
+ return this._docker;
136
+ }
137
+
138
+ async getContainerByName(containerName: string): Promise<Container | undefined> {
139
+ const containers = await this.docker().container.list({ all: true });
140
+ return containers.find((container) => {
141
+ return (container.data as any).Names.indexOf(`/${containerName}`) > -1;
142
+ });
143
+ }
144
+
145
+ async pull(image: string, cacheForMS: number = IMAGE_PULL_CACHE_TTL) {
146
+ let [imageName, tag] = image.split(/:/);
147
+ if (!tag) {
148
+ tag = 'latest';
149
+ }
150
+
151
+ if (tag !== 'latest') {
152
+ if (IMAGE_PULL_CACHE[image]) {
153
+ const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
154
+ if (timeSince < cacheForMS) {
155
+ return;
156
+ }
157
+ }
158
+
159
+ const imageTagList = (await this.docker().image.list())
160
+ .map((image) => image.data as any)
161
+ .filter((imageData) => !!imageData.RepoTags)
162
+ .map((imageData) => imageData.RepoTags as string[]);
163
+
164
+ if (imageTagList.some((imageTags) => imageTags.indexOf(image) > -1)) {
165
+ console.log('Image found: %s', image);
166
+ return;
167
+ }
168
+ console.log('Image not found: %s', image);
169
+ }
170
+
171
+ console.log('Pulling image: %s', image);
172
+ await this.docker()
173
+ .image.create(
174
+ {},
175
+ {
176
+ fromImage: imageName,
177
+ tag: tag,
178
+ }
179
+ )
180
+ .then((stream) => promisifyStream(stream as ReadStream));
181
+
182
+ IMAGE_PULL_CACHE[image] = Date.now();
183
+
184
+ console.log('Image pulled: %s', image);
185
+ }
186
+
187
+ toDockerMounts(mounts: StringMap) {
188
+ const Mounts: DockerMounts[] = [];
189
+ _.forEach(mounts, (Source, Target) => {
190
+ Mounts.push({
191
+ Target,
192
+ Source,
193
+ Type: 'bind',
194
+ ReadOnly: false,
195
+ Consistency: 'consistent',
196
+ });
197
+ });
198
+
199
+ return Mounts;
200
+ }
201
+
202
+ toDockerHealth(health: Health) {
203
+ return {
204
+ Test: ['CMD-SHELL', health.cmd],
205
+ Interval: health.interval ? health.interval * NANO_SECOND : 5000 * NANO_SECOND,
206
+ Timeout: health.timeout ? health.timeout * NANO_SECOND : 15000 * NANO_SECOND,
207
+ Retries: health.retries || 10,
208
+ };
209
+ }
210
+
211
+ async run(
212
+ image: string,
213
+ name: string,
214
+ opts: { ports: {}; mounts: {}; env: {}; cmd: string; health: Health }
215
+ ): Promise<ContainerInfo> {
216
+ const PortBindings: { [key: string]: any } = {};
217
+ const Env: string[] = [];
218
+ const Labels: StringMap = {
219
+ kapeta: 'true',
220
+ };
221
+
222
+ await this.pull(image);
223
+
224
+ const ExposedPorts: { [key: string]: any } = {};
225
+
226
+ _.forEach(opts.ports, (portInfo: any, containerPort) => {
227
+ ExposedPorts['' + containerPort] = {};
228
+ PortBindings['' + containerPort] = [
229
+ {
230
+ HostPort: '' + portInfo.hostPort,
231
+ HostIp: '127.0.0.1',
232
+ },
233
+ ];
234
+
235
+ Labels[LABEL_PORT_PREFIX + portInfo.hostPort] = portInfo.type;
236
+ });
237
+
238
+ const Mounts = this.toDockerMounts(opts.mounts);
239
+
240
+ _.forEach(opts.env, (value, name) => {
241
+ Env.push(name + '=' + value);
242
+ });
243
+
244
+ let HealthCheck = undefined;
245
+
246
+ if (opts.health) {
247
+ HealthCheck = this.toDockerHealth(opts.health);
248
+ }
249
+ const dockerContainer = await this.startContainer({
250
+ name: name,
251
+ Image: image,
252
+ Hostname: name + '.kapeta',
253
+ Labels,
254
+ Cmd: opts.cmd,
255
+
256
+ ExposedPorts,
257
+ Env,
258
+ HealthCheck,
259
+ HostConfig: {
260
+ PortBindings,
261
+ Mounts,
262
+ },
263
+ });
264
+
265
+ if (opts.health) {
266
+ await this.waitForHealthy(dockerContainer);
267
+ }
268
+
269
+ return new ContainerInfo(dockerContainer);
270
+ }
271
+
272
+ async startContainer(opts: any) {
273
+ const dockerContainer = await this.docker().container.create(opts);
274
+ await dockerContainer.start();
275
+ return dockerContainer;
276
+ }
277
+
278
+ async waitForReady(container: Container, attempt: number = 0): Promise<void> {
279
+ if (!attempt) {
280
+ attempt = 0;
281
+ }
282
+
283
+ if (attempt >= HEALTH_CHECK_MAX) {
284
+ throw new Error('Container did not become ready within the timeout');
285
+ }
286
+
287
+ if (await this._isReady(container)) {
288
+ return;
289
+ }
290
+
291
+ return new Promise((resolve, reject) => {
292
+ setTimeout(async () => {
293
+ try {
294
+ await this.waitForReady(container, attempt + 1);
295
+ resolve();
296
+ } catch (err) {
297
+ reject(err);
298
+ }
299
+ }, HEALTH_CHECK_INTERVAL);
300
+ });
301
+ }
302
+
303
+ async waitForHealthy(container: Container, attempt?: number): Promise<void> {
304
+ if (!attempt) {
305
+ attempt = 0;
306
+ }
307
+
308
+ if (attempt >= HEALTH_CHECK_MAX) {
309
+ throw new Error('Container did not become healthy within the timeout');
310
+ }
311
+
312
+ if (await this._isHealthy(container)) {
313
+ return;
314
+ }
315
+
316
+ return new Promise((resolve, reject) => {
317
+ setTimeout(async () => {
318
+ try {
319
+ await this.waitForHealthy(container, (attempt ?? 0) + 1);
320
+ resolve();
321
+ } catch (err) {
322
+ reject(err);
323
+ }
324
+ }, HEALTH_CHECK_INTERVAL);
325
+ });
326
+ }
327
+
328
+ async _isReady(container: Container) {
329
+ const info: Container = await container.status();
330
+ const infoData: any = info?.data;
331
+ if (infoData?.State?.Status === 'exited') {
332
+ throw new Error('Container exited unexpectedly');
333
+ }
334
+ return infoData?.State?.Running ?? false;
335
+ }
336
+
337
+ async _isHealthy(container: Container) {
338
+ const info = await container.status();
339
+ const infoData: any = info?.data;
340
+ return infoData?.State?.Health?.Status === 'healthy';
341
+ }
342
+
343
+ /**
344
+ *
345
+ * @param name
346
+ * @return {Promise<ContainerInfo>}
347
+ */
348
+ async get(name: string): Promise<ContainerInfo | null> {
349
+ let dockerContainer = null;
350
+
351
+ try {
352
+ dockerContainer = await this.docker().container.get(name);
353
+ await dockerContainer.status();
354
+ } catch (err) {
355
+ //Ignore
356
+ dockerContainer = null;
357
+ }
358
+
359
+ if (!dockerContainer) {
360
+ return null;
361
+ }
362
+
363
+ return new ContainerInfo(dockerContainer);
364
+ }
365
+ }
366
+
367
+ export class ContainerInfo {
368
+ private readonly _container: Container;
369
+ /**
370
+ *
371
+ * @param {Container} dockerContainer
372
+ */
373
+ constructor(dockerContainer: Container) {
374
+ /**
375
+ *
376
+ * @type {Container}
377
+ * @private
378
+ */
379
+ this._container = dockerContainer;
380
+ }
381
+
382
+ get native() {
383
+ return this._container;
384
+ }
385
+
386
+ async isRunning() {
387
+ const inspectResult = await this.getStatus();
388
+
389
+ if (!inspectResult || !inspectResult.State) {
390
+ return false;
391
+ }
392
+
393
+ return inspectResult.State.Running || inspectResult.State.Restarting;
394
+ }
395
+
396
+ async start() {
397
+ await this._container.start();
398
+ }
399
+
400
+ async restart() {
401
+ await this._container.restart();
402
+ }
403
+
404
+ async stop() {
405
+ await this._container.stop();
406
+ }
407
+
408
+ async remove(opts?: { force?: boolean }) {
409
+ await this._container.delete({ force: !!opts?.force });
410
+ }
411
+
412
+ async getPort(type: string) {
413
+ const ports = await this.getPorts();
414
+
415
+ if (ports && ports[type]) {
416
+ return ports[type];
417
+ }
418
+
419
+ return null;
420
+ }
421
+
422
+ async getStatus() {
423
+ const result = await this._container.status();
424
+
425
+ return result ? (result.data as any) : null;
426
+ }
427
+
428
+ async getPorts(): Promise<PortMap | false> {
429
+ const inspectResult = await this.getStatus();
430
+
431
+ if (!inspectResult || !inspectResult.Config || !inspectResult.Config.Labels) {
432
+ return false;
433
+ }
434
+
435
+ const portTypes: StringMap = {};
436
+ const ports: PortMap = {};
437
+
438
+ _.forEach(inspectResult.Config.Labels, (portType, name) => {
439
+ if (!name.startsWith(LABEL_PORT_PREFIX)) {
440
+ return;
441
+ }
442
+
443
+ const hostPort = name.substr(LABEL_PORT_PREFIX.length);
444
+
445
+ portTypes[hostPort] = portType;
446
+ });
447
+
448
+ _.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
449
+ let [containerPort, protocol] = containerPortSpec.split(/\//);
450
+
451
+ const hostPort = portBindings[0].HostPort;
452
+
453
+ const portType = portTypes[hostPort];
454
+
455
+ ports[portType] = {
456
+ containerPort,
457
+ protocol,
458
+ hostPort,
459
+ };
460
+ });
461
+
462
+ return ports;
463
+ }
464
+ }
465
+
466
+ export const containerManager = new ContainerManager();
@@ -0,0 +1,74 @@
1
+ import Router from 'express-promise-router';
2
+ import { stringBody, StringBodyRequest } from '../middleware/stringBody';
3
+ import { filesystemManager } from '../filesystemManager';
4
+ import { corsHandler } from '../middleware/cors';
5
+ import { NextFunction, Request, Response } from 'express';
6
+
7
+ let router = Router();
8
+
9
+ router.use('/', corsHandler);
10
+
11
+ router.get('/root', (req: Request, res: Response) => {
12
+ res.send(filesystemManager.getRootFolder());
13
+ });
14
+
15
+ router.get('/project/root', (req: Request, res: Response) => {
16
+ res.send(filesystemManager.getProjectRootFolder());
17
+ });
18
+
19
+ router.use('/project/root', stringBody);
20
+
21
+ router.post('/project/root', (req: StringBodyRequest, res: Response) => {
22
+ filesystemManager.setProjectRootFolder(req.stringBody ?? '');
23
+ res.sendStatus(204);
24
+ });
25
+
26
+ router.use('/', (req: Request, res: Response, next: NextFunction) => {
27
+ if (!req.query.path) {
28
+ res.status(400).send({ error: 'Missing required query parameter "path"' });
29
+ return;
30
+ }
31
+ next();
32
+ });
33
+
34
+ router.get('/list', async (req: Request, res: Response) => {
35
+ let pathArg = req.query.path as string;
36
+
37
+ try {
38
+ res.send(await filesystemManager.readDirectory(pathArg));
39
+ } catch (err) {
40
+ res.status(400).send({ error: '' + err });
41
+ }
42
+ });
43
+
44
+ router.get('/readfile', async (req: Request, res: Response) => {
45
+ let pathArg = req.query.path as string;
46
+ try {
47
+ res.send(await filesystemManager.readFile(pathArg));
48
+ } catch (err) {
49
+ res.status(400).send({ error: '' + err });
50
+ }
51
+ });
52
+
53
+ router.put('/mkdir', async (req: Request, res: Response) => {
54
+ let pathArg = req.query.path as string;
55
+ try {
56
+ await filesystemManager.createFolder(pathArg);
57
+ res.sendStatus(204);
58
+ } catch (err) {
59
+ res.status(400).send({ error: '' + err });
60
+ }
61
+ });
62
+
63
+ router.use('/writefile', stringBody);
64
+ router.post('/writefile', async (req: StringBodyRequest, res: Response) => {
65
+ let pathArg = req.query.path as string;
66
+ try {
67
+ await filesystemManager.writeFile(pathArg, req.stringBody ?? '');
68
+ res.sendStatus(204);
69
+ } catch (err) {
70
+ res.status(400).send({ error: '' + err });
71
+ }
72
+ });
73
+
74
+ export default router;