@kapeta/local-cluster-service 0.0.0-96f91ef

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 (274) hide show
  1. package/.eslintrc.cjs +25 -0
  2. package/.github/workflows/check-license.yml +17 -0
  3. package/.github/workflows/main.yml +26 -0
  4. package/.prettierignore +4 -0
  5. package/.vscode/launch.json +19 -0
  6. package/CHANGELOG.md +920 -0
  7. package/LICENSE +38 -0
  8. package/README.md +36 -0
  9. package/definitions.d.ts +35 -0
  10. package/dist/cjs/index.d.ts +34 -0
  11. package/dist/cjs/index.js +263 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/src/RepositoryWatcher.d.ts +30 -0
  14. package/dist/cjs/src/RepositoryWatcher.js +332 -0
  15. package/dist/cjs/src/ai/aiClient.d.ts +20 -0
  16. package/dist/cjs/src/ai/aiClient.js +74 -0
  17. package/dist/cjs/src/ai/routes.d.ts +7 -0
  18. package/dist/cjs/src/ai/routes.js +37 -0
  19. package/dist/cjs/src/ai/transform.d.ts +11 -0
  20. package/dist/cjs/src/ai/transform.js +239 -0
  21. package/dist/cjs/src/ai/types.d.ts +40 -0
  22. package/dist/cjs/src/ai/types.js +2 -0
  23. package/dist/cjs/src/api.d.ts +7 -0
  24. package/dist/cjs/src/api.js +29 -0
  25. package/dist/cjs/src/assetManager.d.ts +41 -0
  26. package/dist/cjs/src/assetManager.js +274 -0
  27. package/dist/cjs/src/assets/routes.d.ts +7 -0
  28. package/dist/cjs/src/assets/routes.js +165 -0
  29. package/dist/cjs/src/attachments/routes.d.ts +7 -0
  30. package/dist/cjs/src/attachments/routes.js +72 -0
  31. package/dist/cjs/src/authManager.d.ts +16 -0
  32. package/dist/cjs/src/authManager.js +64 -0
  33. package/dist/cjs/src/cacheManager.d.ts +20 -0
  34. package/dist/cjs/src/cacheManager.js +51 -0
  35. package/dist/cjs/src/clusterService.d.ts +44 -0
  36. package/dist/cjs/src/clusterService.js +120 -0
  37. package/dist/cjs/src/codeGeneratorManager.d.ts +14 -0
  38. package/dist/cjs/src/codeGeneratorManager.js +93 -0
  39. package/dist/cjs/src/config/routes.d.ts +7 -0
  40. package/dist/cjs/src/config/routes.js +160 -0
  41. package/dist/cjs/src/configManager.d.ts +42 -0
  42. package/dist/cjs/src/configManager.js +136 -0
  43. package/dist/cjs/src/containerManager.d.ts +148 -0
  44. package/dist/cjs/src/containerManager.js +958 -0
  45. package/dist/cjs/src/definitionsManager.d.ts +20 -0
  46. package/dist/cjs/src/definitionsManager.js +171 -0
  47. package/dist/cjs/src/filesystem/routes.d.ts +7 -0
  48. package/dist/cjs/src/filesystem/routes.js +105 -0
  49. package/dist/cjs/src/filesystemManager.d.ts +27 -0
  50. package/dist/cjs/src/filesystemManager.js +118 -0
  51. package/dist/cjs/src/identities/routes.d.ts +7 -0
  52. package/dist/cjs/src/identities/routes.js +37 -0
  53. package/dist/cjs/src/instanceManager.d.ts +69 -0
  54. package/dist/cjs/src/instanceManager.js +910 -0
  55. package/dist/cjs/src/instances/routes.d.ts +7 -0
  56. package/dist/cjs/src/instances/routes.js +179 -0
  57. package/dist/cjs/src/middleware/cors.d.ts +6 -0
  58. package/dist/cjs/src/middleware/cors.js +14 -0
  59. package/dist/cjs/src/middleware/kapeta.d.ts +15 -0
  60. package/dist/cjs/src/middleware/kapeta.js +28 -0
  61. package/dist/cjs/src/middleware/stringBody.d.ts +9 -0
  62. package/dist/cjs/src/middleware/stringBody.js +18 -0
  63. package/dist/cjs/src/networkManager.d.ts +37 -0
  64. package/dist/cjs/src/networkManager.js +119 -0
  65. package/dist/cjs/src/operatorManager.d.ts +41 -0
  66. package/dist/cjs/src/operatorManager.js +211 -0
  67. package/dist/cjs/src/progressListener.d.ts +31 -0
  68. package/dist/cjs/src/progressListener.js +133 -0
  69. package/dist/cjs/src/providerManager.d.ts +11 -0
  70. package/dist/cjs/src/providerManager.js +84 -0
  71. package/dist/cjs/src/providers/routes.d.ts +7 -0
  72. package/dist/cjs/src/providers/routes.js +46 -0
  73. package/dist/cjs/src/proxy/routes.d.ts +7 -0
  74. package/dist/cjs/src/proxy/routes.js +115 -0
  75. package/dist/cjs/src/proxy/types/rest.d.ts +10 -0
  76. package/dist/cjs/src/proxy/types/rest.js +123 -0
  77. package/dist/cjs/src/proxy/types/web.d.ts +8 -0
  78. package/dist/cjs/src/proxy/types/web.js +61 -0
  79. package/dist/cjs/src/repositoryManager.d.ts +35 -0
  80. package/dist/cjs/src/repositoryManager.js +247 -0
  81. package/dist/cjs/src/serviceManager.d.ts +36 -0
  82. package/dist/cjs/src/serviceManager.js +106 -0
  83. package/dist/cjs/src/socketManager.d.ts +32 -0
  84. package/dist/cjs/src/socketManager.js +125 -0
  85. package/dist/cjs/src/storageService.d.ts +21 -0
  86. package/dist/cjs/src/storageService.js +81 -0
  87. package/dist/cjs/src/taskManager.d.ts +70 -0
  88. package/dist/cjs/src/taskManager.js +181 -0
  89. package/dist/cjs/src/tasks/routes.d.ts +7 -0
  90. package/dist/cjs/src/tasks/routes.js +39 -0
  91. package/dist/cjs/src/traffic/routes.d.ts +7 -0
  92. package/dist/cjs/src/traffic/routes.js +22 -0
  93. package/dist/cjs/src/types.d.ts +99 -0
  94. package/dist/cjs/src/types.js +39 -0
  95. package/dist/cjs/src/utils/BlockInstanceRunner.d.ts +28 -0
  96. package/dist/cjs/src/utils/BlockInstanceRunner.js +432 -0
  97. package/dist/cjs/src/utils/DefaultProviderInstaller.d.ts +15 -0
  98. package/dist/cjs/src/utils/DefaultProviderInstaller.js +136 -0
  99. package/dist/cjs/src/utils/InternalConfigProvider.d.ts +38 -0
  100. package/dist/cjs/src/utils/InternalConfigProvider.js +146 -0
  101. package/dist/cjs/src/utils/LogData.d.ts +23 -0
  102. package/dist/cjs/src/utils/LogData.js +46 -0
  103. package/dist/cjs/src/utils/commandLineUtils.d.ts +8 -0
  104. package/dist/cjs/src/utils/commandLineUtils.js +39 -0
  105. package/dist/cjs/src/utils/pathTemplateParser.d.ts +30 -0
  106. package/dist/cjs/src/utils/pathTemplateParser.js +135 -0
  107. package/dist/cjs/src/utils/utils.d.ts +40 -0
  108. package/dist/cjs/src/utils/utils.js +148 -0
  109. package/dist/cjs/start.d.ts +5 -0
  110. package/dist/cjs/start.js +17 -0
  111. package/dist/cjs/test/proxy/types/rest.test.d.ts +5 -0
  112. package/dist/cjs/test/proxy/types/rest.test.js +48 -0
  113. package/dist/cjs/test/utils/pathTemplateParser.test.d.ts +5 -0
  114. package/dist/cjs/test/utils/pathTemplateParser.test.js +27 -0
  115. package/dist/esm/index.d.ts +34 -0
  116. package/dist/esm/index.js +263 -0
  117. package/dist/esm/package.json +1 -0
  118. package/dist/esm/src/RepositoryWatcher.d.ts +30 -0
  119. package/dist/esm/src/RepositoryWatcher.js +332 -0
  120. package/dist/esm/src/ai/aiClient.d.ts +20 -0
  121. package/dist/esm/src/ai/aiClient.js +74 -0
  122. package/dist/esm/src/ai/routes.d.ts +7 -0
  123. package/dist/esm/src/ai/routes.js +37 -0
  124. package/dist/esm/src/ai/transform.d.ts +11 -0
  125. package/dist/esm/src/ai/transform.js +239 -0
  126. package/dist/esm/src/ai/types.d.ts +40 -0
  127. package/dist/esm/src/ai/types.js +2 -0
  128. package/dist/esm/src/api.d.ts +7 -0
  129. package/dist/esm/src/api.js +29 -0
  130. package/dist/esm/src/assetManager.d.ts +41 -0
  131. package/dist/esm/src/assetManager.js +274 -0
  132. package/dist/esm/src/assets/routes.d.ts +7 -0
  133. package/dist/esm/src/assets/routes.js +165 -0
  134. package/dist/esm/src/attachments/routes.d.ts +7 -0
  135. package/dist/esm/src/attachments/routes.js +72 -0
  136. package/dist/esm/src/authManager.d.ts +16 -0
  137. package/dist/esm/src/authManager.js +64 -0
  138. package/dist/esm/src/cacheManager.d.ts +20 -0
  139. package/dist/esm/src/cacheManager.js +51 -0
  140. package/dist/esm/src/clusterService.d.ts +44 -0
  141. package/dist/esm/src/clusterService.js +120 -0
  142. package/dist/esm/src/codeGeneratorManager.d.ts +14 -0
  143. package/dist/esm/src/codeGeneratorManager.js +93 -0
  144. package/dist/esm/src/config/routes.d.ts +7 -0
  145. package/dist/esm/src/config/routes.js +160 -0
  146. package/dist/esm/src/configManager.d.ts +42 -0
  147. package/dist/esm/src/configManager.js +136 -0
  148. package/dist/esm/src/containerManager.d.ts +148 -0
  149. package/dist/esm/src/containerManager.js +958 -0
  150. package/dist/esm/src/definitionsManager.d.ts +20 -0
  151. package/dist/esm/src/definitionsManager.js +171 -0
  152. package/dist/esm/src/filesystem/routes.d.ts +7 -0
  153. package/dist/esm/src/filesystem/routes.js +105 -0
  154. package/dist/esm/src/filesystemManager.d.ts +27 -0
  155. package/dist/esm/src/filesystemManager.js +118 -0
  156. package/dist/esm/src/identities/routes.d.ts +7 -0
  157. package/dist/esm/src/identities/routes.js +37 -0
  158. package/dist/esm/src/instanceManager.d.ts +69 -0
  159. package/dist/esm/src/instanceManager.js +910 -0
  160. package/dist/esm/src/instances/routes.d.ts +7 -0
  161. package/dist/esm/src/instances/routes.js +179 -0
  162. package/dist/esm/src/middleware/cors.d.ts +6 -0
  163. package/dist/esm/src/middleware/cors.js +14 -0
  164. package/dist/esm/src/middleware/kapeta.d.ts +15 -0
  165. package/dist/esm/src/middleware/kapeta.js +28 -0
  166. package/dist/esm/src/middleware/stringBody.d.ts +9 -0
  167. package/dist/esm/src/middleware/stringBody.js +18 -0
  168. package/dist/esm/src/networkManager.d.ts +37 -0
  169. package/dist/esm/src/networkManager.js +119 -0
  170. package/dist/esm/src/operatorManager.d.ts +41 -0
  171. package/dist/esm/src/operatorManager.js +211 -0
  172. package/dist/esm/src/progressListener.d.ts +31 -0
  173. package/dist/esm/src/progressListener.js +133 -0
  174. package/dist/esm/src/providerManager.d.ts +11 -0
  175. package/dist/esm/src/providerManager.js +84 -0
  176. package/dist/esm/src/providers/routes.d.ts +7 -0
  177. package/dist/esm/src/providers/routes.js +46 -0
  178. package/dist/esm/src/proxy/routes.d.ts +7 -0
  179. package/dist/esm/src/proxy/routes.js +115 -0
  180. package/dist/esm/src/proxy/types/rest.d.ts +10 -0
  181. package/dist/esm/src/proxy/types/rest.js +123 -0
  182. package/dist/esm/src/proxy/types/web.d.ts +8 -0
  183. package/dist/esm/src/proxy/types/web.js +61 -0
  184. package/dist/esm/src/repositoryManager.d.ts +35 -0
  185. package/dist/esm/src/repositoryManager.js +247 -0
  186. package/dist/esm/src/serviceManager.d.ts +36 -0
  187. package/dist/esm/src/serviceManager.js +106 -0
  188. package/dist/esm/src/socketManager.d.ts +32 -0
  189. package/dist/esm/src/socketManager.js +125 -0
  190. package/dist/esm/src/storageService.d.ts +21 -0
  191. package/dist/esm/src/storageService.js +81 -0
  192. package/dist/esm/src/taskManager.d.ts +70 -0
  193. package/dist/esm/src/taskManager.js +181 -0
  194. package/dist/esm/src/tasks/routes.d.ts +7 -0
  195. package/dist/esm/src/tasks/routes.js +39 -0
  196. package/dist/esm/src/traffic/routes.d.ts +7 -0
  197. package/dist/esm/src/traffic/routes.js +22 -0
  198. package/dist/esm/src/types.d.ts +99 -0
  199. package/dist/esm/src/types.js +39 -0
  200. package/dist/esm/src/utils/BlockInstanceRunner.d.ts +28 -0
  201. package/dist/esm/src/utils/BlockInstanceRunner.js +432 -0
  202. package/dist/esm/src/utils/DefaultProviderInstaller.d.ts +15 -0
  203. package/dist/esm/src/utils/DefaultProviderInstaller.js +136 -0
  204. package/dist/esm/src/utils/InternalConfigProvider.d.ts +38 -0
  205. package/dist/esm/src/utils/InternalConfigProvider.js +146 -0
  206. package/dist/esm/src/utils/LogData.d.ts +23 -0
  207. package/dist/esm/src/utils/LogData.js +46 -0
  208. package/dist/esm/src/utils/commandLineUtils.d.ts +8 -0
  209. package/dist/esm/src/utils/commandLineUtils.js +39 -0
  210. package/dist/esm/src/utils/pathTemplateParser.d.ts +30 -0
  211. package/dist/esm/src/utils/pathTemplateParser.js +135 -0
  212. package/dist/esm/src/utils/utils.d.ts +40 -0
  213. package/dist/esm/src/utils/utils.js +148 -0
  214. package/dist/esm/start.d.ts +5 -0
  215. package/dist/esm/start.js +17 -0
  216. package/dist/esm/test/proxy/types/rest.test.d.ts +5 -0
  217. package/dist/esm/test/proxy/types/rest.test.js +48 -0
  218. package/dist/esm/test/utils/pathTemplateParser.test.d.ts +5 -0
  219. package/dist/esm/test/utils/pathTemplateParser.test.js +27 -0
  220. package/index.ts +280 -0
  221. package/jest.config.js +8 -0
  222. package/package.json +134 -0
  223. package/src/RepositoryWatcher.ts +363 -0
  224. package/src/ai/aiClient.ts +93 -0
  225. package/src/ai/routes.ts +39 -0
  226. package/src/ai/transform.ts +275 -0
  227. package/src/ai/types.ts +45 -0
  228. package/src/api.ts +32 -0
  229. package/src/assetManager.ts +355 -0
  230. package/src/assets/routes.ts +183 -0
  231. package/src/attachments/routes.ts +79 -0
  232. package/src/authManager.ts +67 -0
  233. package/src/cacheManager.ts +59 -0
  234. package/src/clusterService.ts +142 -0
  235. package/src/codeGeneratorManager.ts +109 -0
  236. package/src/config/routes.ts +201 -0
  237. package/src/configManager.ts +180 -0
  238. package/src/containerManager.ts +1178 -0
  239. package/src/definitionsManager.ts +212 -0
  240. package/src/filesystem/routes.ts +123 -0
  241. package/src/filesystemManager.ts +133 -0
  242. package/src/identities/routes.ts +38 -0
  243. package/src/instanceManager.ts +1160 -0
  244. package/src/instances/routes.ts +203 -0
  245. package/src/middleware/cors.ts +14 -0
  246. package/src/middleware/kapeta.ts +41 -0
  247. package/src/middleware/stringBody.ts +21 -0
  248. package/src/networkManager.ts +148 -0
  249. package/src/operatorManager.ts +294 -0
  250. package/src/progressListener.ts +151 -0
  251. package/src/providerManager.ts +97 -0
  252. package/src/providers/routes.ts +51 -0
  253. package/src/proxy/routes.ts +153 -0
  254. package/src/proxy/types/rest.ts +172 -0
  255. package/src/proxy/types/web.ts +70 -0
  256. package/src/repositoryManager.ts +291 -0
  257. package/src/serviceManager.ts +133 -0
  258. package/src/socketManager.ts +138 -0
  259. package/src/storageService.ts +97 -0
  260. package/src/taskManager.ts +247 -0
  261. package/src/tasks/routes.ts +43 -0
  262. package/src/traffic/routes.ts +23 -0
  263. package/src/types.ts +112 -0
  264. package/src/utils/BlockInstanceRunner.ts +577 -0
  265. package/src/utils/DefaultProviderInstaller.ts +150 -0
  266. package/src/utils/InternalConfigProvider.ts +214 -0
  267. package/src/utils/LogData.ts +50 -0
  268. package/src/utils/commandLineUtils.ts +45 -0
  269. package/src/utils/pathTemplateParser.ts +157 -0
  270. package/src/utils/utils.ts +155 -0
  271. package/start.ts +14 -0
  272. package/test/proxy/types/rest.test.ts +54 -0
  273. package/test/utils/pathTemplateParser.test.ts +29 -0
  274. package/tsconfig.json +15 -0
@@ -0,0 +1,1178 @@
1
+ /**
2
+ * Copyright 2023 Kapeta Inc.
3
+ * SPDX-License-Identifier: BUSL-1.1
4
+ */
5
+
6
+ import Path from 'path';
7
+ import { storageService } from './storageService';
8
+ import os from 'os';
9
+ import _ from 'lodash';
10
+ import FSExtra, { ReadStream } from 'fs-extra';
11
+ import Docker from 'dockerode';
12
+ import { parseKapetaUri } from '@kapeta/nodejs-utils';
13
+ import ClusterConfiguration from '@kapeta/local-cluster-config';
14
+ import uuid from 'node-uuid';
15
+ import md5 from 'md5';
16
+ import { getBlockInstanceContainerName } from './utils/utils';
17
+ import { DOCKER_HOST_INTERNAL, InstanceInfo, LogEntry, LogSource } from './types';
18
+ import { KapetaAPI } from '@kapeta/nodejs-api-client';
19
+ import { taskManager, Task } from './taskManager';
20
+ import { EventEmitter } from 'node:events';
21
+ import StreamValues from 'stream-json/streamers/StreamValues';
22
+ import { LocalInstanceHealth } from '@kapeta/schemas';
23
+
24
+ type StringMap = { [key: string]: string };
25
+
26
+ export type PortMap = {
27
+ [key: string]: {
28
+ containerPort: string;
29
+ protocol: string;
30
+ hostPort: string;
31
+ };
32
+ };
33
+
34
+ export interface DockerMounts {
35
+ Target: string;
36
+ Source: string;
37
+ Type: string;
38
+ ReadOnly: boolean;
39
+ Consistency: string;
40
+ Labels?: StringMap;
41
+ }
42
+
43
+ interface JSONProgress {
44
+ // Current is the current status and value of the progress made towards Total.
45
+ current: number;
46
+ // Total is the end value describing when we made 100% progress for an operation.
47
+ total: number;
48
+ // Start is the initial value for the operation.
49
+ start: number;
50
+ // HideCounts. if true, hides the progress count indicator (xB/yB).
51
+ hidecounts: boolean;
52
+ // Units is the unit to print for progress. It defaults to "bytes" if empty.
53
+ units: string;
54
+ }
55
+
56
+ interface JSONError {
57
+ code: number;
58
+ message: string;
59
+ }
60
+
61
+ export type DockerContainerStatus = 'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';
62
+ export type DockerContainerHealth = 'starting' | 'healthy' | 'unhealthy' | 'none';
63
+
64
+ interface JSONMessage<T = string> {
65
+ stream?: string;
66
+ status: T;
67
+ progressDetail?: JSONProgress;
68
+ progress?: string;
69
+ id: string;
70
+ from: string;
71
+ time: number;
72
+ timeNano: number;
73
+ errorDetail?: JSONError;
74
+ error?: string;
75
+ // Aux contains out-of-band data, such as digests for push signing and image id after building.
76
+ aux?: any;
77
+ }
78
+
79
+ export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
80
+ const NANO_SECOND = 1000000;
81
+ const HEALTH_CHECK_INTERVAL = 3000;
82
+ const HEALTH_CHECK_MAX = 100;
83
+ const LATEST_PULL_TIMEOUT = 1000 * 60 * 15; // 15 minutes
84
+ export const COMPOSE_LABEL_PROJECT = 'com.docker.compose.project';
85
+ export const COMPOSE_LABEL_SERVICE = 'com.docker.compose.service';
86
+
87
+ export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
88
+
89
+ enum DockerPullEventTypes {
90
+ PreparingPhase = 'Preparing',
91
+ WaitingPhase = 'Waiting',
92
+ PullingFsPhase = 'Pulling fs layer',
93
+ DownloadingPhase = 'Downloading',
94
+ DownloadCompletePhase = 'Download complete',
95
+ ExtractingPhase = 'Extracting',
96
+ VerifyingChecksumPhase = 'Verifying Checksum',
97
+ AlreadyExistsPhase = 'Already exists',
98
+ PullCompletePhase = 'Pull complete',
99
+ }
100
+
101
+ type DockerPullEventType = DockerPullEventTypes | string;
102
+
103
+ const processJsonStream = <T>(purpose: string, stream: NodeJS.ReadableStream, handler: (d: JSONMessage<T>) => void) =>
104
+ new Promise<void>((resolve, reject) => {
105
+ const jsonStream = StreamValues.withParser();
106
+ jsonStream.on('data', (data: any) => {
107
+ try {
108
+ handler(data.value as JSONMessage<T>);
109
+ } catch (e) {
110
+ console.error('Failed while processing data for stream: %s', purpose, e);
111
+ }
112
+ });
113
+ jsonStream.on('end', () => {
114
+ console.log('Docker stream ended: %s', purpose);
115
+ resolve();
116
+ });
117
+ jsonStream.on('error', (err) => {
118
+ console.error('Docker stream failed: %s', purpose, err);
119
+ reject(err);
120
+ });
121
+
122
+ stream.pipe(jsonStream);
123
+ });
124
+
125
+ class ContainerManager {
126
+ private _docker: Docker | null;
127
+ private _alive: boolean;
128
+ private _mountDir: string;
129
+ private _version: string;
130
+ private _lastDockerAccessCheck: number = 0;
131
+ private logStreams: { [p: string]: { stream?: ClosableLogStream; timer?: NodeJS.Timeout } } = {};
132
+ private _latestImagePulls: { [p: string]: number } = {};
133
+
134
+ constructor() {
135
+ this._docker = null;
136
+ this._alive = false;
137
+ this._version = '';
138
+ this._mountDir = Path.join(storageService.getKapetaBasedir(), 'mounts');
139
+ this._latestImagePulls = {};
140
+ FSExtra.mkdirpSync(this._mountDir);
141
+ }
142
+
143
+ async initialize() {
144
+ // Use the value from cluster-service.yml if configured
145
+ const dockerConfig = ClusterConfiguration.getDockerConfig();
146
+ const connectOptions: any[] =
147
+ Object.keys(dockerConfig).length > 0
148
+ ? [dockerConfig]
149
+ : [
150
+ // use defaults: DOCKER_HOST etc from env, if available
151
+ undefined,
152
+ // default linux
153
+ { socketPath: '/var/run/docker.sock' },
154
+ // default macOS
155
+ {
156
+ socketPath: Path.join(os.homedir(), '.docker/run/docker.sock'),
157
+ },
158
+ // Default http
159
+ { protocol: 'http', host: 'localhost', port: 2375 },
160
+ { protocol: 'https', host: 'localhost', port: 2376 },
161
+ { protocol: 'http', host: '127.0.0.1', port: 2375 },
162
+ { protocol: 'https', host: '127.0.0.1', port: 2376 },
163
+ ];
164
+ for (const opts of connectOptions) {
165
+ try {
166
+ const testClient = new Docker({
167
+ ...opts,
168
+ timeout: 1000, // 1 secs should be enough for a ping
169
+ });
170
+ await testClient.ping();
171
+ // If we get here - we have a working connection
172
+ // Now create a client with a longer timeout for all other operations
173
+ const client = new Docker({
174
+ ...opts,
175
+ timeout: 15 * 60 * 1000, //15 minutes should be enough for any operation
176
+ });
177
+ this._docker = client;
178
+ const versionInfo: any = await client.version();
179
+ this._version = versionInfo.Server?.Version ?? versionInfo.Version;
180
+ if (!this._version) {
181
+ console.warn('Failed to determine version from response', versionInfo);
182
+ this._version = '0.0.0';
183
+ }
184
+ this._alive = true;
185
+ console.log('Connected to docker daemon with version: %s', this._version);
186
+ return;
187
+ } catch (err) {
188
+ // silently ignore bad configs
189
+ }
190
+ }
191
+
192
+ throw new Error('Could not connect to docker daemon. Please make sure docker is running and working.');
193
+ }
194
+
195
+ async checkAlive() {
196
+ if (!this._docker) {
197
+ try {
198
+ await this.initialize();
199
+ } catch (e) {
200
+ this._alive = false;
201
+ }
202
+ return this._alive;
203
+ }
204
+
205
+ try {
206
+ await this._docker.ping();
207
+ this._alive = true;
208
+ } catch (e) {
209
+ this._alive = false;
210
+ }
211
+
212
+ return this._alive;
213
+ }
214
+
215
+ isAlive() {
216
+ return this._alive;
217
+ }
218
+
219
+ getMountPoint(systemId: string, ref: string, mountName: string) {
220
+ const kindUri = parseKapetaUri(ref);
221
+ const systemUri = parseKapetaUri(systemId);
222
+ return Path.join(
223
+ this._mountDir,
224
+ systemUri.handle,
225
+ systemUri.name,
226
+ systemUri.version,
227
+ kindUri.handle,
228
+ kindUri.name,
229
+ kindUri.version,
230
+ mountName
231
+ );
232
+ }
233
+
234
+ async createMounts(systemId: string, kind: string, mountOpts: StringMap | null | undefined): Promise<StringMap> {
235
+ const mounts: StringMap = {};
236
+
237
+ if (mountOpts) {
238
+ const mountOptList = Object.entries(mountOpts);
239
+ for (const [mountName, containerPath] of mountOptList) {
240
+ const hostPath = this.getMountPoint(systemId, kind, mountName);
241
+ await FSExtra.mkdirp(hostPath);
242
+ mounts[containerPath] = hostPath;
243
+ }
244
+ }
245
+
246
+ return mounts;
247
+ }
248
+
249
+ async createVolumes(
250
+ systemId: string,
251
+ serviceId: string,
252
+ mountOpts: StringMap | null | undefined
253
+ ): Promise<DockerMounts[]> {
254
+ const Mounts: DockerMounts[] = [];
255
+
256
+ if (mountOpts) {
257
+ const mountOptList = Object.entries(mountOpts);
258
+ for (const [mountName, containerPath] of mountOptList) {
259
+ const volumeName = `${systemId}_${serviceId}_${mountName}`.replace(/[^a-z0-9]/gi, '_');
260
+
261
+ Mounts.push({
262
+ Target: containerPath,
263
+ Source: volumeName,
264
+ Type: 'volume',
265
+ ReadOnly: false,
266
+ Consistency: 'consistent',
267
+ Labels: {
268
+ [COMPOSE_LABEL_PROJECT]: systemId.replace(/[^a-z0-9]/gi, '_'),
269
+ [COMPOSE_LABEL_SERVICE]: serviceId.replace(/[^a-z0-9]/gi, '_'),
270
+ },
271
+ });
272
+ }
273
+ }
274
+
275
+ return Mounts;
276
+ }
277
+
278
+ async ping() {
279
+ try {
280
+ const pingResult = await this.docker().ping();
281
+ if (pingResult !== 'OK') {
282
+ throw new Error(`Ping failed: ${pingResult}`);
283
+ }
284
+ } catch (e: any) {
285
+ throw new Error(
286
+ `Docker not running. Please start the docker daemon before running this command. Error: ${e.message}`
287
+ );
288
+ }
289
+ }
290
+
291
+ docker() {
292
+ if (!this._docker) {
293
+ throw new Error(`Docker not running`);
294
+ }
295
+ return this._docker;
296
+ }
297
+
298
+ async getContainerByName(containerName: string): Promise<ContainerInfo | undefined> {
299
+ // The container can be fetched by name or by id using the same API call
300
+ return this.get(containerName);
301
+ }
302
+
303
+ async pull(image: string) {
304
+ let [imageName, tag] = image.split(/:/);
305
+ if (!tag) {
306
+ tag = 'latest';
307
+ }
308
+
309
+ const imageTagList = (await this.docker().listImages({}))
310
+ .filter((imageData) => !!imageData.RepoTags)
311
+ .map((imageData) => imageData.RepoTags as string[]);
312
+
313
+ const imageExists = imageTagList.some((imageTags) => imageTags.includes(image));
314
+
315
+ if (tag === 'latest') {
316
+ if (imageExists && this._latestImagePulls[imageName]) {
317
+ const lastPull = this._latestImagePulls[imageName];
318
+ const timeSinceLastPull = Date.now() - lastPull;
319
+ if (timeSinceLastPull < LATEST_PULL_TIMEOUT) {
320
+ console.log(
321
+ 'Image found and was pulled %s seconds ago: %s',
322
+ Math.round(timeSinceLastPull / 1000),
323
+ image
324
+ );
325
+ // Last pull was less than the timeout - don't pull again
326
+ return false;
327
+ }
328
+ }
329
+ this._latestImagePulls[imageName] = Date.now();
330
+ } else if (imageExists) {
331
+ console.log('Image found: %s', image);
332
+ return false;
333
+ }
334
+
335
+ let friendlyImageName = image;
336
+ const imageParts = imageName.split('/');
337
+ if (imageParts.length > 2) {
338
+ //Strip the registry to make the name shorter
339
+ friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
340
+ }
341
+
342
+ const taskName = `Pulling image ${friendlyImageName}`;
343
+
344
+ const processor = async (task: Task) => {
345
+ const timeStarted = Date.now();
346
+ const api = new KapetaAPI();
347
+ const accessToken = api.hasToken() ? await api.getAccessToken() : null;
348
+
349
+ const auth =
350
+ accessToken && image.startsWith('docker.kapeta.com/')
351
+ ? {
352
+ username: 'kapeta',
353
+ password: accessToken,
354
+ serveraddress: 'docker.kapeta.com',
355
+ }
356
+ : {};
357
+
358
+ const stream = await this.docker().pull(image, {
359
+ authconfig: auth,
360
+ });
361
+
362
+ const chunks: {
363
+ [p: string]: {
364
+ downloading: {
365
+ total: number;
366
+ current: number;
367
+ };
368
+ extracting: {
369
+ total: number;
370
+ current: number;
371
+ };
372
+ done: boolean;
373
+ };
374
+ } = {};
375
+
376
+ let lastEmitted = Date.now();
377
+ await processJsonStream<DockerPullEventType>(`image:pull:${image}`, stream, (data) => {
378
+ if (!chunks[data.id]) {
379
+ chunks[data.id] = {
380
+ downloading: {
381
+ total: 0,
382
+ current: 0,
383
+ },
384
+ extracting: {
385
+ total: 0,
386
+ current: 0,
387
+ },
388
+ done: false,
389
+ };
390
+ }
391
+
392
+ const chunk = chunks[data.id];
393
+
394
+ if (data.stream) {
395
+ // Emit raw output to the task log
396
+ task.addLog(data.stream);
397
+ }
398
+
399
+ switch (data.status) {
400
+ case DockerPullEventTypes.PreparingPhase:
401
+ case DockerPullEventTypes.WaitingPhase:
402
+ case DockerPullEventTypes.PullingFsPhase:
403
+ //Do nothing
404
+ break;
405
+ case DockerPullEventTypes.DownloadingPhase:
406
+ case DockerPullEventTypes.VerifyingChecksumPhase:
407
+ chunk.downloading = {
408
+ total: data.progressDetail?.total ?? 0,
409
+ current: data.progressDetail?.current ?? 0,
410
+ };
411
+ break;
412
+ case DockerPullEventTypes.ExtractingPhase:
413
+ chunk.extracting = {
414
+ total: data.progressDetail?.total ?? 0,
415
+ current: data.progressDetail?.current ?? 0,
416
+ };
417
+ break;
418
+ case DockerPullEventTypes.DownloadCompletePhase:
419
+ chunk.downloading.current = chunks[data.id].downloading.total;
420
+ break;
421
+ case DockerPullEventTypes.PullCompletePhase:
422
+ chunk.extracting.current = chunks[data.id].extracting.total;
423
+ chunk.done = true;
424
+ break;
425
+ }
426
+
427
+ if (
428
+ data.status === DockerPullEventTypes.AlreadyExistsPhase ||
429
+ data.status.includes('Image is up to date') ||
430
+ data.status.includes('Downloaded newer image')
431
+ ) {
432
+ chunk.downloading.current = 1;
433
+ chunk.downloading.total = 1;
434
+ chunk.extracting.current = 1;
435
+ chunk.extracting.total = 1;
436
+ chunk.done = true;
437
+ }
438
+
439
+ const chunkList = Object.values(chunks);
440
+ let totals = {
441
+ downloading: {
442
+ total: 0,
443
+ current: 0,
444
+ },
445
+ extracting: {
446
+ total: 0,
447
+ current: 0,
448
+ },
449
+ percent: 0,
450
+ total: chunkList.length,
451
+ done: 0,
452
+ };
453
+
454
+ chunkList.forEach((chunk) => {
455
+ if (chunk.downloading.current > 0) {
456
+ totals.downloading.current += chunk.downloading.current;
457
+ }
458
+
459
+ if (chunk.downloading.total > 0) {
460
+ totals.downloading.total += chunk.downloading.total;
461
+ }
462
+
463
+ if (chunk.extracting.current > 0) {
464
+ totals.extracting.current += chunk.extracting.current;
465
+ }
466
+
467
+ if (chunk.extracting.total > 0) {
468
+ totals.extracting.total += chunk.extracting.total;
469
+ }
470
+
471
+ if (chunk.done) {
472
+ totals.done++;
473
+ }
474
+ });
475
+
476
+ totals.percent = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
477
+
478
+ task.metadata = {
479
+ ...task.metadata,
480
+ image,
481
+ progress: totals.percent,
482
+ status: totals,
483
+ timeTaken: Date.now() - timeStarted,
484
+ };
485
+
486
+ if (Date.now() - lastEmitted < 1000) {
487
+ return;
488
+ }
489
+ task.emitUpdate();
490
+ lastEmitted = Date.now();
491
+ //console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
492
+ });
493
+
494
+ task.metadata = {
495
+ ...task.metadata,
496
+ image,
497
+ progress: 100,
498
+ timeTaken: Date.now() - timeStarted,
499
+ };
500
+ task.emitUpdate();
501
+ };
502
+
503
+ const task = taskManager.add(`docker:image:pull:${image}`, processor, {
504
+ name: taskName,
505
+ image,
506
+ progress: -1,
507
+ group: 'docker:pull', //It's faster to pull images one at a time
508
+ });
509
+
510
+ await task.wait();
511
+
512
+ return true;
513
+ }
514
+
515
+ toDockerMounts(mounts: StringMap) {
516
+ const Mounts: DockerMounts[] = [];
517
+ _.forEach(mounts, (Source, Target) => {
518
+ Mounts.push({
519
+ Target,
520
+ Source: toLocalBindVolume(Source),
521
+ Type: 'bind',
522
+ ReadOnly: false,
523
+ Consistency: 'consistent',
524
+ });
525
+ });
526
+
527
+ return Mounts;
528
+ }
529
+
530
+ toDockerHealth(health: LocalInstanceHealth) {
531
+ return {
532
+ Test: ['CMD-SHELL', health.cmd],
533
+ Interval: health.interval ? health.interval * NANO_SECOND : 5000 * NANO_SECOND,
534
+ Timeout: health.timeout ? health.timeout * NANO_SECOND : 15000 * NANO_SECOND,
535
+ Retries: health.retries || 10,
536
+ };
537
+ }
538
+
539
+ private applyHash(dockerOpts: any) {
540
+ if (dockerOpts?.Labels?.HASH) {
541
+ delete dockerOpts.Labels.HASH;
542
+ }
543
+
544
+ const hash = md5(JSON.stringify(dockerOpts));
545
+
546
+ if (!dockerOpts.Labels) {
547
+ dockerOpts.Labels = {};
548
+ }
549
+ dockerOpts.Labels.HASH = hash;
550
+ }
551
+
552
+ public async ensureContainer(opts: any) {
553
+ return await this.createOrUpdateContainer(opts);
554
+ }
555
+
556
+ private async createOrUpdateContainer(opts: any) {
557
+ let imagePulled = await this.pull(opts.Image);
558
+
559
+ this.applyHash(opts);
560
+ if (!opts.name) {
561
+ console.log('Starting unnamed container: %s', opts.Image);
562
+ return this.startContainer(opts);
563
+ }
564
+ const container = await this.getContainerByName(opts.name);
565
+ if (imagePulled) {
566
+ // If image was pulled always recreate
567
+ console.log('New version of image was pulled: %s', opts.Image);
568
+ } else {
569
+ if (!container) {
570
+ console.log('Starting new container: %s', opts.name);
571
+ return this.startContainer(opts);
572
+ }
573
+
574
+ const containerData = await container.inspect();
575
+
576
+ if (containerData?.Config.Labels?.HASH === opts.Labels.HASH) {
577
+ if (!(await container.isRunning())) {
578
+ console.log('Starting previously created container: %s', opts.name);
579
+ await container.start();
580
+ } else {
581
+ console.log('Previously created container already running: %s', opts.name);
582
+ }
583
+ return container.native;
584
+ }
585
+ }
586
+
587
+ if (container) {
588
+ // Remove the container and start a new one
589
+ console.log('Replacing previously created container: %s', opts.name);
590
+ await container.remove({ force: true });
591
+ }
592
+
593
+ console.log('Starting new container: %s', opts.name);
594
+ return this.startContainer(opts);
595
+ }
596
+
597
+ private async startContainer(opts: any) {
598
+ const extraHosts = getExtraHosts(this._version);
599
+
600
+ if (extraHosts && extraHosts.length > 0) {
601
+ if (!opts.HostConfig) {
602
+ opts.HostConfig = {};
603
+ }
604
+
605
+ if (!opts.HostConfig.ExtraHosts) {
606
+ opts.HostConfig.ExtraHosts = [];
607
+ }
608
+
609
+ opts.HostConfig.ExtraHosts = opts.HostConfig.ExtraHosts.concat(extraHosts);
610
+ }
611
+
612
+ const dockerContainer = await this.docker().createContainer(opts);
613
+ await dockerContainer.start();
614
+ return dockerContainer;
615
+ }
616
+
617
+ async waitForReady(container: Docker.Container, attempt: number = 0): Promise<void> {
618
+ if (!attempt) {
619
+ attempt = 0;
620
+ }
621
+
622
+ if (attempt >= HEALTH_CHECK_MAX) {
623
+ throw new Error('Container did not become ready within the timeout');
624
+ }
625
+
626
+ if (await this._isReady(container)) {
627
+ return;
628
+ }
629
+
630
+ return new Promise((resolve, reject) => {
631
+ setTimeout(async () => {
632
+ try {
633
+ await this.waitForReady(container, attempt + 1);
634
+ resolve();
635
+ } catch (err) {
636
+ reject(err);
637
+ }
638
+ }, HEALTH_CHECK_INTERVAL);
639
+ });
640
+ }
641
+
642
+ async _isReady(container: Docker.Container) {
643
+ let info: Docker.ContainerInspectInfo;
644
+ try {
645
+ info = await container.inspect();
646
+ } catch (err) {
647
+ return false;
648
+ }
649
+
650
+ const state = info.State;
651
+
652
+ if (state.Status === 'exited' || state?.Status === 'removing' || state?.Status === 'dead') {
653
+ throw new Error('Container exited unexpectedly');
654
+ }
655
+
656
+ if (state.Health) {
657
+ // If container has health info - wait for it to become healthy
658
+ return state.Health.Status === 'healthy';
659
+ } else {
660
+ return state.Running ?? false;
661
+ }
662
+ }
663
+
664
+ async remove(container: Docker.Container, opts?: { force?: boolean }) {
665
+ const newName = 'deleting-' + uuid.v4();
666
+ // Rename the container first to avoid name conflicts if people start the same container
667
+ await container.rename({ name: newName });
668
+
669
+ const newContainer = this.docker().getContainer(newName);
670
+ await newContainer.remove({ force: !!opts?.force });
671
+ }
672
+
673
+ /**
674
+ *
675
+ * @param name
676
+ * @return {Promise<ContainerInfo>}
677
+ */
678
+ async get(name: string): Promise<ContainerInfo | undefined> {
679
+ let dockerContainer = null;
680
+
681
+ try {
682
+ dockerContainer = this.docker().getContainer(name);
683
+ await dockerContainer.stats();
684
+ } catch (err) {
685
+ //Ignore
686
+ dockerContainer = null;
687
+ }
688
+
689
+ if (!dockerContainer) {
690
+ return undefined;
691
+ }
692
+
693
+ return new ContainerInfo(dockerContainer);
694
+ }
695
+
696
+ async getLogs(instance: InstanceInfo): Promise<LogEntry[]> {
697
+ const containerName = await getBlockInstanceContainerName(instance.systemId, instance.instanceId);
698
+ const containerInfo = await this.getContainerByName(containerName);
699
+ if (!containerInfo) {
700
+ return [
701
+ {
702
+ source: 'stdout',
703
+ level: 'ERROR',
704
+ time: Date.now(),
705
+ message: 'Container not found',
706
+ },
707
+ ];
708
+ }
709
+
710
+ return await containerInfo.getLogs();
711
+ }
712
+
713
+ async stopLogListening(systemId: string, instanceId: string) {
714
+ const containerName = await getBlockInstanceContainerName(systemId, instanceId);
715
+ if (this.logStreams[containerName]) {
716
+ if (this.logStreams[containerName]?.timer) {
717
+ clearTimeout(this.logStreams[containerName].timer);
718
+ }
719
+ try {
720
+ const stream = this.logStreams[containerName].stream;
721
+ if (stream) {
722
+ await stream.close();
723
+ }
724
+ } catch (err) {
725
+ // Ignore
726
+ }
727
+ delete this.logStreams[containerName];
728
+ }
729
+ }
730
+
731
+ async ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void) {
732
+ const containerName = await getBlockInstanceContainerName(systemId, instanceId);
733
+ try {
734
+ if (this.logStreams[containerName]?.stream) {
735
+ // Already listening - will shut itself down
736
+ return;
737
+ }
738
+
739
+ if (this.logStreams[containerName]?.timer) {
740
+ clearTimeout(this.logStreams[containerName].timer);
741
+ }
742
+
743
+ const tryLater = () => {
744
+ this.logStreams[containerName] = {
745
+ timer: setTimeout(() => {
746
+ // Keep trying until user decides to not listen anymore
747
+ this.ensureLogListening(systemId, instanceId, handler);
748
+ }, 5000),
749
+ };
750
+ };
751
+
752
+ const containerInfo = await this.getContainerByName(containerName);
753
+ if (!containerInfo || !(await containerInfo.isRunning())) {
754
+ // Container not currently running - try again in 5 seconds
755
+ tryLater();
756
+ return;
757
+ }
758
+
759
+ const stream = await containerInfo.getLogStream();
760
+ stream.onLog((log) => {
761
+ try {
762
+ handler(log);
763
+ } catch (err) {
764
+ console.warn('Error handling log', err);
765
+ }
766
+ });
767
+ stream.onEnd(() => {
768
+ // We get here if the container is stopped
769
+ delete this.logStreams[containerName];
770
+ tryLater();
771
+ });
772
+ stream.onError((err) => {
773
+ // We get here if the container crashes
774
+ delete this.logStreams[containerName];
775
+ tryLater();
776
+ });
777
+
778
+ this.logStreams[containerName] = {
779
+ stream,
780
+ };
781
+ } catch (err) {
782
+ // Ignore
783
+ }
784
+ }
785
+
786
+ buildDockerImage(dockerFile: string, imageName: string) {
787
+ const taskName = `Building docker image: ${imageName}`;
788
+ const processor = async (task: Task) => {
789
+ const timeStarted = Date.now();
790
+ const stream = await this.docker().buildImage(
791
+ {
792
+ context: Path.dirname(dockerFile),
793
+ src: [Path.basename(dockerFile)],
794
+ },
795
+ {
796
+ t: imageName,
797
+ dockerfile: Path.basename(dockerFile),
798
+ }
799
+ );
800
+
801
+ await processJsonStream<string>(`image:build:${imageName}`, stream, (data) => {
802
+ if (data.stream) {
803
+ // Emit raw output to the task log
804
+ task.addLog(data.stream);
805
+ }
806
+ });
807
+ };
808
+
809
+ return taskManager.add(`docker:image:build:${imageName}`, processor, {
810
+ name: taskName,
811
+ });
812
+ }
813
+ }
814
+
815
+ function readLogBuffer(logBuffer: Buffer) {
816
+ const out: LogEntry[] = [];
817
+ let offset = 0;
818
+ while (offset < logBuffer.length) {
819
+ try {
820
+ // Read the docker log format - explained here:
821
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
822
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
823
+
824
+ // First byte is stream type
825
+ const streamTypeInt = logBuffer.readInt8(offset);
826
+ const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
827
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
828
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
829
+ break;
830
+ }
831
+
832
+ // Bytes 4-8 is frame size
833
+ const messageLength = logBuffer.readInt32BE(offset + 4);
834
+
835
+ // After that is the message - with the message length
836
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
837
+ const raw = dataWithoutStreamType.toString();
838
+
839
+ // Split the message into date and message
840
+ const firstSpaceIx = raw.indexOf(' ');
841
+ const dateString = raw.substring(0, firstSpaceIx);
842
+ const line = raw.substring(firstSpaceIx + 1);
843
+ offset = offset + messageLength + 8;
844
+ if (!dateString) {
845
+ break;
846
+ }
847
+ out.push({
848
+ time: new Date(dateString).getTime(),
849
+ message: line,
850
+ level: 'INFO',
851
+ source: streamType,
852
+ });
853
+ } catch (err) {
854
+ console.error('Error parsing log entry', err);
855
+ offset = logBuffer.length;
856
+ break;
857
+ }
858
+ }
859
+ return out;
860
+ }
861
+
862
+ class ClosableLogStream {
863
+ private readonly stream: FSExtra.ReadStream;
864
+
865
+ private readonly eventEmitter: EventEmitter;
866
+
867
+ constructor(stream: FSExtra.ReadStream) {
868
+ this.stream = stream;
869
+ this.eventEmitter = new EventEmitter();
870
+ stream.on('data', (data) => {
871
+ const logs = readLogBuffer(data as Buffer);
872
+ logs.forEach((log) => {
873
+ this.eventEmitter.emit('log', log);
874
+ });
875
+ });
876
+
877
+ stream.on('end', () => {
878
+ this.eventEmitter.emit('end');
879
+ });
880
+
881
+ stream.on('error', (error) => {
882
+ this.eventEmitter.emit('error', error);
883
+ });
884
+
885
+ stream.on('close', () => {
886
+ this.eventEmitter.emit('end');
887
+ });
888
+ }
889
+
890
+ onLog(listener: (log: LogEntry) => void) {
891
+ this.eventEmitter.on('log', listener);
892
+ return () => {
893
+ this.eventEmitter.removeListener('log', listener);
894
+ };
895
+ }
896
+
897
+ onEnd(listener: () => void) {
898
+ this.eventEmitter.on('end', listener);
899
+ return () => {
900
+ this.eventEmitter.removeListener('end', listener);
901
+ };
902
+ }
903
+
904
+ onError(listener: (error: Error) => void) {
905
+ this.eventEmitter.on('error', listener);
906
+ return () => {
907
+ this.eventEmitter.removeListener('error', listener);
908
+ };
909
+ }
910
+
911
+ close() {
912
+ return new Promise<void>((resolve, reject) => {
913
+ try {
914
+ this.stream.close((err) => {
915
+ if (err) {
916
+ console.warn('Error closing log stream', err);
917
+ }
918
+ resolve();
919
+ });
920
+ } catch (err) {
921
+ // Ignore
922
+ }
923
+ });
924
+ }
925
+ }
926
+
927
+ export class ContainerInfo {
928
+ private readonly _container: Docker.Container;
929
+
930
+ /**
931
+ *
932
+ * @param {Docker.Container} dockerContainer
933
+ */
934
+ constructor(dockerContainer: Docker.Container) {
935
+ /**
936
+ *
937
+ * @type {Docker.Container}
938
+ * @private
939
+ */
940
+ this._container = dockerContainer;
941
+ }
942
+
943
+ get native() {
944
+ return this._container;
945
+ }
946
+
947
+ async isRunning() {
948
+ const inspectResult = await this.inspect();
949
+
950
+ if (!inspectResult || !inspectResult.State) {
951
+ return false;
952
+ }
953
+
954
+ return inspectResult.State.Running || inspectResult.State.Restarting;
955
+ }
956
+
957
+ async start() {
958
+ if (await this.isRunning()) {
959
+ return;
960
+ }
961
+ await this._container.start();
962
+ }
963
+
964
+ async restart() {
965
+ if (!(await this.isRunning())) {
966
+ return this.start();
967
+ }
968
+ await this._container.restart();
969
+ }
970
+
971
+ async stop() {
972
+ if (!(await this.isRunning())) {
973
+ return;
974
+ }
975
+ await this._container.stop();
976
+ }
977
+
978
+ async remove(opts?: { force?: boolean }) {
979
+ await containerManager.remove(this._container, opts);
980
+ }
981
+
982
+ async getPort(type: string) {
983
+ const ports = await this.getPorts();
984
+
985
+ if (ports && ports[type]) {
986
+ return ports[type];
987
+ }
988
+
989
+ return null;
990
+ }
991
+
992
+ async inspect() {
993
+ try {
994
+ return await this._container.inspect();
995
+ } catch (err) {
996
+ return undefined;
997
+ }
998
+ }
999
+
1000
+ async status() {
1001
+ const result = await this.inspect();
1002
+
1003
+ return result?.State;
1004
+ }
1005
+
1006
+ async getPorts(): Promise<PortMap | false> {
1007
+ const inspectResult = await this.inspect();
1008
+
1009
+ if (!inspectResult || !inspectResult.Config || !inspectResult.Config.Labels) {
1010
+ return false;
1011
+ }
1012
+
1013
+ const portTypes: StringMap = {};
1014
+ const ports: PortMap = {};
1015
+
1016
+ _.forEach(inspectResult.Config.Labels, (portType, name) => {
1017
+ if (!name.startsWith(CONTAINER_LABEL_PORT_PREFIX)) {
1018
+ return;
1019
+ }
1020
+
1021
+ const hostPort = name.substring(CONTAINER_LABEL_PORT_PREFIX.length);
1022
+
1023
+ portTypes[hostPort] = portType;
1024
+ });
1025
+
1026
+ _.forEach(inspectResult.HostConfig.PortBindings, (portBindings, containerPortSpec) => {
1027
+ let [containerPort, protocol] = containerPortSpec.split(/\//);
1028
+
1029
+ const hostPort = portBindings[0].HostPort;
1030
+
1031
+ const portType = portTypes[hostPort];
1032
+
1033
+ ports[portType] = {
1034
+ containerPort,
1035
+ protocol,
1036
+ hostPort,
1037
+ };
1038
+ });
1039
+
1040
+ return ports;
1041
+ }
1042
+
1043
+ async getLogStream() {
1044
+ try {
1045
+ const logStream = (await this.native.logs({
1046
+ stdout: true,
1047
+ stderr: true,
1048
+ follow: true,
1049
+ tail: 0,
1050
+ timestamps: true,
1051
+ })) as ReadStream;
1052
+
1053
+ return new ClosableLogStream(logStream);
1054
+ } catch (err) {
1055
+ console.log('Error getting log stream', err);
1056
+ throw err;
1057
+ }
1058
+ }
1059
+
1060
+ async getLogs(): Promise<LogEntry[]> {
1061
+ const logs = await this.native.logs({
1062
+ stdout: true,
1063
+ stderr: true,
1064
+ follow: false,
1065
+ timestamps: true,
1066
+ });
1067
+
1068
+ const out = readLogBuffer(logs);
1069
+ if (out.length > 0) {
1070
+ return out;
1071
+ }
1072
+
1073
+ const status = await this.status();
1074
+ const healthLogs: LogEntry[] = status?.Health?.Log
1075
+ ? status?.Health?.Log.map((log) => {
1076
+ return {
1077
+ source: 'stdout',
1078
+ level: log.ExitCode === 0 ? 'INFO' : 'ERROR',
1079
+ time: Date.now(),
1080
+ message: 'Health check: ' + log.Output,
1081
+ };
1082
+ })
1083
+ : [];
1084
+
1085
+ if (status?.Running) {
1086
+ return [
1087
+ {
1088
+ source: 'stdout',
1089
+ level: 'INFO',
1090
+ time: Date.now(),
1091
+ message: 'Container is starting...',
1092
+ },
1093
+ ...healthLogs,
1094
+ ];
1095
+ }
1096
+
1097
+ if (status?.Restarting) {
1098
+ return [
1099
+ {
1100
+ source: 'stdout',
1101
+ level: 'INFO',
1102
+ time: Date.now(),
1103
+ message: 'Container is restarting...',
1104
+ },
1105
+ ...healthLogs,
1106
+ ];
1107
+ }
1108
+ if (status?.Paused) {
1109
+ return [
1110
+ {
1111
+ source: 'stdout',
1112
+ level: 'INFO',
1113
+ time: Date.now(),
1114
+ message: 'Container is paused...',
1115
+ },
1116
+ ...healthLogs,
1117
+ ];
1118
+ }
1119
+
1120
+ if (status?.Error) {
1121
+ return [
1122
+ {
1123
+ source: 'stderr',
1124
+ level: 'ERROR',
1125
+ time: Date.now(),
1126
+ message: 'Container failed to start:\n' + status.Error,
1127
+ },
1128
+ ...healthLogs,
1129
+ ];
1130
+ }
1131
+
1132
+ return [
1133
+ {
1134
+ source: 'stdout',
1135
+ level: 'INFO',
1136
+ time: Date.now(),
1137
+ message: 'Container not running',
1138
+ ...healthLogs,
1139
+ },
1140
+ ];
1141
+ }
1142
+ }
1143
+
1144
+ export function getExtraHosts(dockerVersion: string): string[] | undefined {
1145
+ if (process.platform !== 'darwin' && process.platform !== 'win32') {
1146
+ const [major, minor] = dockerVersion.split('.');
1147
+ if (parseInt(major) >= 20 && parseInt(minor) >= 10) {
1148
+ // Docker 20.10+ on Linux supports adding host.docker.internal to point to host-gateway
1149
+ return [`${DOCKER_HOST_INTERNAL}:host-gateway`];
1150
+ }
1151
+ // Docker versions lower than 20.10 needs an actual IP address. We use the default network bridge which
1152
+ // is always 172.17.0.1
1153
+ return [`${DOCKER_HOST_INTERNAL}:172.17.0.1`];
1154
+ }
1155
+
1156
+ return undefined;
1157
+ }
1158
+
1159
+ /**
1160
+ * Ensure that the volume is in the correct format for the docker daemon on the host
1161
+ *
1162
+ * Windows: c:\path\to\volume -> /c/path/to/volume
1163
+ * Linux: /path/to/volume -> /path/to/volume
1164
+ * Mac: /path/to/volume -> /path/to/volume
1165
+ */
1166
+ export function toLocalBindVolume(volume: string): string {
1167
+ if (process.platform === 'win32') {
1168
+ //On Windows we need to convert c:\ to /c/
1169
+ return volume
1170
+ .replace(/^([a-z]):\\/i, (match, drive) => {
1171
+ return '/' + drive.toLowerCase() + '/';
1172
+ })
1173
+ .replace(/\\(\S)/g, '/$1');
1174
+ }
1175
+ return volume;
1176
+ }
1177
+
1178
+ export const containerManager = new ContainerManager();