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