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