@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,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();
|