@kapeta/local-cluster-service 0.12.0 → 0.13.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/CHANGELOG.md +14 -0
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/src/assetManager.d.ts +3 -1
- package/dist/cjs/src/assetManager.js +20 -4
- package/dist/cjs/src/assets/routes.js +22 -1
- package/dist/cjs/src/containerManager.d.ts +1 -1
- package/dist/cjs/src/containerManager.js +132 -122
- package/dist/cjs/src/instanceManager.d.ts +4 -3
- package/dist/cjs/src/instanceManager.js +87 -60
- package/dist/cjs/src/instances/routes.js +21 -11
- package/dist/cjs/src/operatorManager.d.ts +5 -3
- package/dist/cjs/src/operatorManager.js +34 -22
- package/dist/cjs/src/providerManager.js +1 -1
- package/dist/cjs/src/repositoryManager.d.ts +2 -4
- package/dist/cjs/src/repositoryManager.js +51 -66
- package/dist/cjs/src/socketManager.js +1 -1
- package/dist/cjs/src/taskManager.d.ts +64 -0
- package/dist/cjs/src/taskManager.js +163 -0
- package/dist/cjs/src/tasks/routes.d.ts +3 -0
- package/dist/cjs/src/tasks/routes.js +35 -0
- package/dist/cjs/src/utils/BlockInstanceRunner.js +0 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/src/assetManager.d.ts +3 -1
- package/dist/esm/src/assetManager.js +20 -4
- package/dist/esm/src/assets/routes.js +22 -1
- package/dist/esm/src/containerManager.d.ts +1 -1
- package/dist/esm/src/containerManager.js +132 -122
- package/dist/esm/src/instanceManager.d.ts +4 -3
- package/dist/esm/src/instanceManager.js +87 -60
- package/dist/esm/src/instances/routes.js +21 -11
- package/dist/esm/src/operatorManager.d.ts +5 -3
- package/dist/esm/src/operatorManager.js +34 -22
- package/dist/esm/src/providerManager.js +1 -1
- package/dist/esm/src/repositoryManager.d.ts +2 -4
- package/dist/esm/src/repositoryManager.js +51 -66
- package/dist/esm/src/socketManager.js +1 -1
- package/dist/esm/src/taskManager.d.ts +64 -0
- package/dist/esm/src/taskManager.js +159 -0
- package/dist/esm/src/tasks/routes.d.ts +3 -0
- package/dist/esm/src/tasks/routes.js +30 -0
- package/dist/esm/src/utils/BlockInstanceRunner.js +0 -1
- package/index.ts +2 -0
- package/package.json +1 -1
- package/src/assetManager.ts +28 -4
- package/src/assets/routes.ts +23 -1
- package/src/containerManager.ts +153 -142
- package/src/instanceManager.ts +116 -70
- package/src/instances/routes.ts +20 -12
- package/src/operatorManager.ts +46 -26
- package/src/providerManager.ts +1 -1
- package/src/repositoryManager.ts +65 -63
- package/src/socketManager.ts +1 -1
- package/src/taskManager.ts +225 -0
- package/src/tasks/routes.ts +38 -0
- package/src/utils/BlockInstanceRunner.ts +0 -4
@@ -0,0 +1,163 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.taskManager = exports.Task = exports.TaskStatus = void 0;
|
4
|
+
/**
|
5
|
+
* Class that handles processing background tasks.
|
6
|
+
*/
|
7
|
+
const socketManager_1 = require("./socketManager");
|
8
|
+
const EVENT_TASK_UPDATED = 'task-updated';
|
9
|
+
const EVENT_TASK_ADDED = 'task-added';
|
10
|
+
const EVENT_TASK_REMOVED = 'task-removed';
|
11
|
+
var TaskStatus;
|
12
|
+
(function (TaskStatus) {
|
13
|
+
TaskStatus["PENDING"] = "PENDING";
|
14
|
+
TaskStatus["RUNNING"] = "RUNNING";
|
15
|
+
TaskStatus["COMPLETED"] = "COMPLETED";
|
16
|
+
TaskStatus["FAILED"] = "FAILED";
|
17
|
+
})(TaskStatus || (exports.TaskStatus = TaskStatus = {}));
|
18
|
+
class Task {
|
19
|
+
data;
|
20
|
+
constructor(task) {
|
21
|
+
this.data = task;
|
22
|
+
}
|
23
|
+
get id() {
|
24
|
+
return this.data.id;
|
25
|
+
}
|
26
|
+
get status() {
|
27
|
+
return this.data.status;
|
28
|
+
}
|
29
|
+
get errorMessage() {
|
30
|
+
return this.data.errorMessage;
|
31
|
+
}
|
32
|
+
get metadata() {
|
33
|
+
return this.data.metadata;
|
34
|
+
}
|
35
|
+
get future() {
|
36
|
+
return this.data.future;
|
37
|
+
}
|
38
|
+
get run() {
|
39
|
+
return this.data.run;
|
40
|
+
}
|
41
|
+
set status(status) {
|
42
|
+
this.data.status = status;
|
43
|
+
}
|
44
|
+
set errorMessage(errorMessage) {
|
45
|
+
this.data.errorMessage = errorMessage;
|
46
|
+
}
|
47
|
+
set metadata(metadata) {
|
48
|
+
this.data.metadata = metadata;
|
49
|
+
}
|
50
|
+
emitUpdate() {
|
51
|
+
socketManager_1.socketManager.emitGlobal(EVENT_TASK_UPDATED, this.toData());
|
52
|
+
}
|
53
|
+
async wait() {
|
54
|
+
return this.future.promise;
|
55
|
+
}
|
56
|
+
toData() {
|
57
|
+
return { ...this.data };
|
58
|
+
}
|
59
|
+
}
|
60
|
+
exports.Task = Task;
|
61
|
+
function createFuture() {
|
62
|
+
let resolve = () => { };
|
63
|
+
let reject = () => { };
|
64
|
+
const promise = new Promise((res, rej) => {
|
65
|
+
resolve = res;
|
66
|
+
reject = rej;
|
67
|
+
});
|
68
|
+
return {
|
69
|
+
promise,
|
70
|
+
resolve,
|
71
|
+
reject,
|
72
|
+
};
|
73
|
+
}
|
74
|
+
class TaskManager {
|
75
|
+
_tasks = [];
|
76
|
+
add(id, runner, metadata) {
|
77
|
+
const existingTask = this.get(id);
|
78
|
+
if (existingTask) {
|
79
|
+
return existingTask;
|
80
|
+
}
|
81
|
+
const future = createFuture();
|
82
|
+
const task = new Task({
|
83
|
+
id,
|
84
|
+
status: TaskStatus.PENDING,
|
85
|
+
metadata,
|
86
|
+
future,
|
87
|
+
run: runner,
|
88
|
+
});
|
89
|
+
this._tasks.push(task);
|
90
|
+
socketManager_1.socketManager.emitGlobal(EVENT_TASK_ADDED, task.toData());
|
91
|
+
this.invokeTask(task).catch(() => { });
|
92
|
+
return task;
|
93
|
+
}
|
94
|
+
async waitFor(filter) {
|
95
|
+
const tasks = this._tasks.filter(filter);
|
96
|
+
while (tasks.length > 0) {
|
97
|
+
const task = tasks.shift();
|
98
|
+
if (!task) {
|
99
|
+
continue;
|
100
|
+
}
|
101
|
+
try {
|
102
|
+
await task.wait();
|
103
|
+
}
|
104
|
+
catch (e) {
|
105
|
+
//Ignore
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
get(taskId) {
|
110
|
+
return this._tasks.find((t) => t.id === taskId);
|
111
|
+
}
|
112
|
+
exists(taskId) {
|
113
|
+
return !!this.get(taskId);
|
114
|
+
}
|
115
|
+
remove(taskId) {
|
116
|
+
const task = this.get(taskId);
|
117
|
+
if (!task) {
|
118
|
+
return;
|
119
|
+
}
|
120
|
+
if (task.status === TaskStatus.RUNNING) {
|
121
|
+
throw new Error('Cannot remove a running task');
|
122
|
+
}
|
123
|
+
this._tasks = this._tasks.filter((t) => t.id !== taskId);
|
124
|
+
socketManager_1.socketManager.emitGlobal(EVENT_TASK_REMOVED, task.toData());
|
125
|
+
}
|
126
|
+
list() {
|
127
|
+
return this._tasks.map((t) => t.toData());
|
128
|
+
}
|
129
|
+
async invokeTask(task) {
|
130
|
+
if (task.metadata.group) {
|
131
|
+
const existingTaskInGroup = this._tasks.find((t) => t.id !== task.id && t.metadata.group === task.metadata.group && t.status === TaskStatus.RUNNING);
|
132
|
+
if (existingTaskInGroup) {
|
133
|
+
console.log('existingTaskInGroup', existingTaskInGroup.toData());
|
134
|
+
return;
|
135
|
+
}
|
136
|
+
}
|
137
|
+
try {
|
138
|
+
task.status = TaskStatus.RUNNING;
|
139
|
+
task.emitUpdate();
|
140
|
+
const result = await task.run(task);
|
141
|
+
task.status = TaskStatus.COMPLETED;
|
142
|
+
task.future.resolve(result);
|
143
|
+
task.emitUpdate();
|
144
|
+
}
|
145
|
+
catch (e) {
|
146
|
+
task.errorMessage = e.message;
|
147
|
+
task.status = TaskStatus.FAILED;
|
148
|
+
task.future.reject(e);
|
149
|
+
task.emitUpdate();
|
150
|
+
}
|
151
|
+
finally {
|
152
|
+
this.remove(task.id);
|
153
|
+
}
|
154
|
+
if (task.metadata.group) {
|
155
|
+
const nextTaskInGroup = this._tasks.find((t) => t.id !== task.id && t.metadata.group === task.metadata.group && t.status === TaskStatus.PENDING);
|
156
|
+
if (nextTaskInGroup) {
|
157
|
+
console.log('nextTaskInGroup', nextTaskInGroup.toData());
|
158
|
+
return this.invokeTask(nextTaskInGroup);
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
exports.taskManager = new TaskManager();
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
const express_promise_router_1 = __importDefault(require("express-promise-router"));
|
7
|
+
const cors_1 = require("../middleware/cors");
|
8
|
+
const taskManager_1 = require("../taskManager");
|
9
|
+
const router = (0, express_promise_router_1.default)();
|
10
|
+
router.use('/', cors_1.corsHandler);
|
11
|
+
/**
|
12
|
+
* Get all current tasks
|
13
|
+
*/
|
14
|
+
router.get('/', (req, res) => {
|
15
|
+
res.send(taskManager_1.taskManager.list());
|
16
|
+
});
|
17
|
+
router.get('/:taskId', (req, res) => {
|
18
|
+
const task = taskManager_1.taskManager.get(req.params.taskId);
|
19
|
+
if (!task) {
|
20
|
+
res.status(404).send({ error: 'Task not found' });
|
21
|
+
return;
|
22
|
+
}
|
23
|
+
res.send(task.toData());
|
24
|
+
});
|
25
|
+
router.delete('/:taskId', (req, res) => {
|
26
|
+
try {
|
27
|
+
taskManager_1.taskManager.remove(req.params.taskId);
|
28
|
+
res.send({ ok: true });
|
29
|
+
}
|
30
|
+
catch (e) {
|
31
|
+
res.status(400).send({ error: e.message });
|
32
|
+
return;
|
33
|
+
}
|
34
|
+
});
|
35
|
+
exports.default = router;
|
@@ -324,7 +324,6 @@ class BlockInstanceRunner {
|
|
324
324
|
}
|
325
325
|
async ensureContainer(opts) {
|
326
326
|
const container = await containerManager_1.containerManager.ensureContainer(opts);
|
327
|
-
await containerManager_1.containerManager.waitForReady(container);
|
328
327
|
return this._handleContainer(container);
|
329
328
|
}
|
330
329
|
async _handleContainer(container) {
|
package/dist/esm/index.js
CHANGED
@@ -15,6 +15,7 @@ import FilesystemRoutes from './src/filesystem/routes';
|
|
15
15
|
import AssetsRoutes from './src/assets/routes';
|
16
16
|
import ProviderRoutes from './src/providers/routes';
|
17
17
|
import AttachmentRoutes from './src/attachments/routes';
|
18
|
+
import TaskRoutes from './src/tasks/routes';
|
18
19
|
import { getBindHost } from './src/utils/utils';
|
19
20
|
import request from 'request';
|
20
21
|
let currentServer = null;
|
@@ -29,6 +30,7 @@ function createServer() {
|
|
29
30
|
app.use('/assets', AssetsRoutes);
|
30
31
|
app.use('/providers', ProviderRoutes);
|
31
32
|
app.use('/attachments', AttachmentRoutes);
|
33
|
+
app.use('/tasks', TaskRoutes);
|
32
34
|
app.get('/status', async (req, res) => {
|
33
35
|
res.send({
|
34
36
|
ok: true,
|
@@ -13,6 +13,7 @@ export interface EnrichedAsset {
|
|
13
13
|
declare class AssetManager {
|
14
14
|
private cache;
|
15
15
|
constructor();
|
16
|
+
clearCache(): void;
|
16
17
|
/**
|
17
18
|
*
|
18
19
|
* @param {string[]} [assetKinds]
|
@@ -21,11 +22,12 @@ declare class AssetManager {
|
|
21
22
|
getAssets(assetKinds?: string[]): EnrichedAsset[];
|
22
23
|
getPlans(): EnrichedAsset[];
|
23
24
|
getPlan(ref: string, noCache?: boolean): Promise<Definition>;
|
24
|
-
getAsset(ref: string, noCache?: boolean): Promise<EnrichedAsset | undefined>;
|
25
|
+
getAsset(ref: string, noCache?: boolean, autoFetch?: boolean): Promise<EnrichedAsset | undefined>;
|
25
26
|
createAsset(path: string, yaml: BlockDefinition): Promise<EnrichedAsset[]>;
|
26
27
|
updateAsset(ref: string, yaml: BlockDefinition): Promise<void>;
|
27
28
|
importFile(filePath: string): Promise<EnrichedAsset[]>;
|
28
29
|
unregisterAsset(ref: string): Promise<void>;
|
30
|
+
installAsset(ref: string): Promise<import("./taskManager").Task<void>[] | undefined>;
|
29
31
|
}
|
30
32
|
export declare const assetManager: AssetManager;
|
31
33
|
export {};
|
@@ -41,6 +41,9 @@ class AssetManager {
|
|
41
41
|
stdTTL: 60 * 60, // 1 hour
|
42
42
|
});
|
43
43
|
}
|
44
|
+
clearCache() {
|
45
|
+
this.cache.flushAll();
|
46
|
+
}
|
44
47
|
/**
|
45
48
|
*
|
46
49
|
* @param {string[]} [assetKinds]
|
@@ -70,22 +73,26 @@ class AssetManager {
|
|
70
73
|
}
|
71
74
|
return asset.data;
|
72
75
|
}
|
73
|
-
async getAsset(ref, noCache = false) {
|
76
|
+
async getAsset(ref, noCache = false, autoFetch = true) {
|
74
77
|
ref = normalizeKapetaUri(ref);
|
75
78
|
const cacheKey = `getAsset:${ref}`;
|
76
79
|
if (!noCache && this.cache.has(cacheKey)) {
|
77
80
|
return this.cache.get(cacheKey);
|
78
81
|
}
|
79
82
|
const uri = parseKapetaUri(ref);
|
80
|
-
|
83
|
+
if (autoFetch) {
|
84
|
+
await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, true);
|
85
|
+
}
|
81
86
|
let asset = definitionsManager
|
82
87
|
.getDefinitions()
|
83
88
|
.map(enrichAsset)
|
84
89
|
.find((a) => parseKapetaUri(a.ref).equals(uri));
|
85
|
-
if (!asset) {
|
90
|
+
if (autoFetch && !asset) {
|
86
91
|
throw new Error('Asset not found: ' + ref);
|
87
92
|
}
|
88
|
-
|
93
|
+
if (asset) {
|
94
|
+
this.cache.set(cacheKey, asset);
|
95
|
+
}
|
89
96
|
return asset;
|
90
97
|
}
|
91
98
|
async createAsset(path, yaml) {
|
@@ -146,5 +153,14 @@ class AssetManager {
|
|
146
153
|
this.cache.flushAll();
|
147
154
|
await Actions.uninstall(progressListener, [asset.ref]);
|
148
155
|
}
|
156
|
+
async installAsset(ref) {
|
157
|
+
const asset = await this.getAsset(ref, true, false);
|
158
|
+
if (asset) {
|
159
|
+
throw new Error('Asset already installed: ' + ref);
|
160
|
+
}
|
161
|
+
const uri = parseKapetaUri(ref);
|
162
|
+
console.log('Installing %s', ref);
|
163
|
+
return await repositoryManager.ensureAsset(uri.handle, uri.name, uri.version, false);
|
164
|
+
}
|
149
165
|
}
|
150
166
|
export const assetManager = new AssetManager();
|
@@ -34,8 +34,15 @@ router.get('/read', async (req, res) => {
|
|
34
34
|
res.status(400).send({ error: 'Query parameter "ref" is missing' });
|
35
35
|
return;
|
36
36
|
}
|
37
|
+
const ensure = req.query.ensure !== 'false';
|
37
38
|
try {
|
38
|
-
|
39
|
+
const asset = await assetManager.getAsset(req.query.ref, true, ensure);
|
40
|
+
if (asset) {
|
41
|
+
res.send(asset);
|
42
|
+
}
|
43
|
+
else {
|
44
|
+
res.status(404).send({ error: 'Asset not found' });
|
45
|
+
}
|
39
46
|
}
|
40
47
|
catch (err) {
|
41
48
|
res.status(400).send({ error: err.message });
|
@@ -109,4 +116,18 @@ router.put('/import', async (req, res) => {
|
|
109
116
|
res.status(400).send({ error: err.message });
|
110
117
|
}
|
111
118
|
});
|
119
|
+
router.put('/install', async (req, res) => {
|
120
|
+
if (!req.query.ref) {
|
121
|
+
res.status(400).send({ error: 'Query parameter "ref" is missing' });
|
122
|
+
return;
|
123
|
+
}
|
124
|
+
try {
|
125
|
+
const tasks = await assetManager.installAsset(req.query.ref);
|
126
|
+
const taskIds = tasks?.map((t) => t.id) ?? [];
|
127
|
+
res.status(200).send(taskIds);
|
128
|
+
}
|
129
|
+
catch (err) {
|
130
|
+
res.status(400).send({ error: err.message });
|
131
|
+
}
|
132
|
+
});
|
112
133
|
export default router;
|
@@ -59,7 +59,7 @@ declare class ContainerManager {
|
|
59
59
|
ping(): Promise<void>;
|
60
60
|
docker(): Docker;
|
61
61
|
getContainerByName(containerName: string): Promise<ContainerInfo | undefined>;
|
62
|
-
pull(image: string
|
62
|
+
pull(image: string): Promise<boolean>;
|
63
63
|
toDockerMounts(mounts: StringMap): DockerMounts[];
|
64
64
|
toDockerHealth(health: Health): {
|
65
65
|
Test: string[];
|
@@ -9,15 +9,13 @@ import ClusterConfiguration from '@kapeta/local-cluster-config';
|
|
9
9
|
import uuid from 'node-uuid';
|
10
10
|
import md5 from 'md5';
|
11
11
|
import { getBlockInstanceContainerName } from './utils/utils';
|
12
|
-
import { socketManager } from './socketManager';
|
13
12
|
import { KapetaAPI } from '@kapeta/nodejs-api-client';
|
13
|
+
import { taskManager } from './taskManager';
|
14
14
|
const EVENT_IMAGE_PULL = 'docker-image-pull';
|
15
15
|
export const CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
|
16
16
|
const NANO_SECOND = 1000000;
|
17
17
|
const HEALTH_CHECK_INTERVAL = 3000;
|
18
18
|
const HEALTH_CHECK_MAX = 20;
|
19
|
-
const IMAGE_PULL_CACHE_TTL = 30 * 60 * 1000;
|
20
|
-
const IMAGE_PULL_CACHE = {};
|
21
19
|
export const HEALTH_CHECK_TIMEOUT = HEALTH_CHECK_INTERVAL * HEALTH_CHECK_MAX * 2;
|
22
20
|
const promisifyStream = (stream, handler) => new Promise((resolve, reject) => {
|
23
21
|
stream.on('data', handler);
|
@@ -145,17 +143,11 @@ class ContainerManager {
|
|
145
143
|
}
|
146
144
|
return undefined;
|
147
145
|
}
|
148
|
-
async pull(image
|
146
|
+
async pull(image) {
|
149
147
|
let [imageName, tag] = image.split(/:/);
|
150
148
|
if (!tag) {
|
151
149
|
tag = 'latest';
|
152
150
|
}
|
153
|
-
if (IMAGE_PULL_CACHE[image]) {
|
154
|
-
const timeSince = Date.now() - IMAGE_PULL_CACHE[image];
|
155
|
-
if (timeSince < cacheForMS) {
|
156
|
-
return false;
|
157
|
-
}
|
158
|
-
}
|
159
151
|
const imageTagList = (await this.docker().image.list())
|
160
152
|
.map((image) => image.data)
|
161
153
|
.filter((imageData) => !!imageData.RepoTags)
|
@@ -164,121 +156,141 @@ class ContainerManager {
|
|
164
156
|
console.log('Image found: %s', image);
|
165
157
|
return false;
|
166
158
|
}
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
let lastEmitted = Date.now();
|
184
|
-
await promisifyStream(stream, (rawData) => {
|
185
|
-
const lines = rawData.toString().trim().split('\n');
|
186
|
-
lines.forEach((line) => {
|
187
|
-
const data = JSON.parse(line);
|
188
|
-
if (![
|
189
|
-
'Waiting',
|
190
|
-
'Downloading',
|
191
|
-
'Extracting',
|
192
|
-
'Download complete',
|
193
|
-
'Pull complete',
|
194
|
-
'Already exists',
|
195
|
-
].includes(data.status)) {
|
196
|
-
return;
|
197
|
-
}
|
198
|
-
if (!chunks[data.id]) {
|
199
|
-
chunks[data.id] = {
|
200
|
-
downloading: {
|
201
|
-
total: 0,
|
202
|
-
current: 0,
|
203
|
-
},
|
204
|
-
extracting: {
|
205
|
-
total: 0,
|
206
|
-
current: 0,
|
207
|
-
},
|
208
|
-
done: false,
|
209
|
-
};
|
210
|
-
}
|
211
|
-
const chunk = chunks[data.id];
|
212
|
-
switch (data.status) {
|
213
|
-
case 'Downloading':
|
214
|
-
chunk.downloading = data.progressDetail;
|
215
|
-
break;
|
216
|
-
case 'Extracting':
|
217
|
-
chunk.extracting = data.progressDetail;
|
218
|
-
break;
|
219
|
-
case 'Download complete':
|
220
|
-
chunk.downloading.current = chunks[data.id].downloading.total;
|
221
|
-
break;
|
222
|
-
case 'Pull complete':
|
223
|
-
chunk.extracting.current = chunks[data.id].extracting.total;
|
224
|
-
chunk.done = true;
|
225
|
-
break;
|
226
|
-
case 'Already exists':
|
227
|
-
// Force layer to be done
|
228
|
-
chunk.downloading.current = 1;
|
229
|
-
chunk.downloading.total = 1;
|
230
|
-
chunk.extracting.current = 1;
|
231
|
-
chunk.extracting.total = 1;
|
232
|
-
chunk.done = true;
|
233
|
-
break;
|
234
|
-
}
|
235
|
-
});
|
236
|
-
if (Date.now() - lastEmitted < 1000) {
|
237
|
-
return;
|
238
|
-
}
|
239
|
-
const chunkList = Object.values(chunks);
|
240
|
-
let totals = {
|
241
|
-
downloading: {
|
242
|
-
total: 0,
|
243
|
-
current: 0,
|
244
|
-
},
|
245
|
-
extracting: {
|
246
|
-
total: 0,
|
247
|
-
current: 0,
|
248
|
-
},
|
249
|
-
total: chunkList.length,
|
250
|
-
done: 0,
|
251
|
-
};
|
252
|
-
chunkList.forEach((chunk) => {
|
253
|
-
if (chunk.downloading.current > 0) {
|
254
|
-
totals.downloading.current += chunk.downloading.current;
|
255
|
-
}
|
256
|
-
if (chunk.downloading.total > 0) {
|
257
|
-
totals.downloading.total += chunk.downloading.total;
|
159
|
+
let friendlyImageName = image;
|
160
|
+
const imageParts = imageName.split('/');
|
161
|
+
if (imageParts.length > 2) {
|
162
|
+
//Strip the registry to make the name shorter
|
163
|
+
friendlyImageName = `${imageParts.slice(1).join('/')}:${tag}`;
|
164
|
+
}
|
165
|
+
const taskName = `Pulling image ${friendlyImageName}`;
|
166
|
+
const processor = async (task) => {
|
167
|
+
const timeStarted = Date.now();
|
168
|
+
const api = new KapetaAPI();
|
169
|
+
const accessToken = await api.getAccessToken();
|
170
|
+
const auth = image.startsWith('docker.kapeta.com/')
|
171
|
+
? {
|
172
|
+
username: 'kapeta',
|
173
|
+
password: accessToken,
|
174
|
+
serveraddress: 'docker.kapeta.com',
|
258
175
|
}
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
176
|
+
: {};
|
177
|
+
const stream = (await this.docker().image.create(auth, {
|
178
|
+
fromImage: imageName,
|
179
|
+
tag: tag,
|
180
|
+
}));
|
181
|
+
const chunks = {};
|
182
|
+
let lastEmitted = Date.now();
|
183
|
+
await promisifyStream(stream, (rawData) => {
|
184
|
+
const lines = rawData.toString().trim().split('\n');
|
185
|
+
lines.forEach((line) => {
|
186
|
+
const data = JSON.parse(line);
|
187
|
+
if (![
|
188
|
+
'Waiting',
|
189
|
+
'Downloading',
|
190
|
+
'Extracting',
|
191
|
+
'Download complete',
|
192
|
+
'Pull complete',
|
193
|
+
'Already exists',
|
194
|
+
].includes(data.status)) {
|
195
|
+
return;
|
196
|
+
}
|
197
|
+
if (!chunks[data.id]) {
|
198
|
+
chunks[data.id] = {
|
199
|
+
downloading: {
|
200
|
+
total: 0,
|
201
|
+
current: 0,
|
202
|
+
},
|
203
|
+
extracting: {
|
204
|
+
total: 0,
|
205
|
+
current: 0,
|
206
|
+
},
|
207
|
+
done: false,
|
208
|
+
};
|
209
|
+
}
|
210
|
+
const chunk = chunks[data.id];
|
211
|
+
switch (data.status) {
|
212
|
+
case 'Downloading':
|
213
|
+
chunk.downloading = data.progressDetail;
|
214
|
+
break;
|
215
|
+
case 'Extracting':
|
216
|
+
chunk.extracting = data.progressDetail;
|
217
|
+
break;
|
218
|
+
case 'Download complete':
|
219
|
+
chunk.downloading.current = chunks[data.id].downloading.total;
|
220
|
+
break;
|
221
|
+
case 'Pull complete':
|
222
|
+
chunk.extracting.current = chunks[data.id].extracting.total;
|
223
|
+
chunk.done = true;
|
224
|
+
break;
|
225
|
+
case 'Already exists':
|
226
|
+
// Force layer to be done
|
227
|
+
chunk.downloading.current = 1;
|
228
|
+
chunk.downloading.total = 1;
|
229
|
+
chunk.extracting.current = 1;
|
230
|
+
chunk.extracting.total = 1;
|
231
|
+
chunk.done = true;
|
232
|
+
break;
|
233
|
+
}
|
234
|
+
});
|
235
|
+
if (Date.now() - lastEmitted < 1000) {
|
236
|
+
return;
|
267
237
|
}
|
238
|
+
const chunkList = Object.values(chunks);
|
239
|
+
let totals = {
|
240
|
+
downloading: {
|
241
|
+
total: 0,
|
242
|
+
current: 0,
|
243
|
+
},
|
244
|
+
extracting: {
|
245
|
+
total: 0,
|
246
|
+
current: 0,
|
247
|
+
},
|
248
|
+
total: chunkList.length,
|
249
|
+
done: 0,
|
250
|
+
};
|
251
|
+
chunkList.forEach((chunk) => {
|
252
|
+
if (chunk.downloading.current > 0) {
|
253
|
+
totals.downloading.current += chunk.downloading.current;
|
254
|
+
}
|
255
|
+
if (chunk.downloading.total > 0) {
|
256
|
+
totals.downloading.total += chunk.downloading.total;
|
257
|
+
}
|
258
|
+
if (chunk.extracting.current > 0) {
|
259
|
+
totals.extracting.current += chunk.extracting.current;
|
260
|
+
}
|
261
|
+
if (chunk.extracting.total > 0) {
|
262
|
+
totals.extracting.total += chunk.extracting.total;
|
263
|
+
}
|
264
|
+
if (chunk.done) {
|
265
|
+
totals.done++;
|
266
|
+
}
|
267
|
+
});
|
268
|
+
const progress = totals.total > 0 ? (totals.done / totals.total) * 100 : 0;
|
269
|
+
task.metadata = {
|
270
|
+
...task.metadata,
|
271
|
+
image,
|
272
|
+
progress,
|
273
|
+
status: totals,
|
274
|
+
timeTaken: Date.now() - timeStarted,
|
275
|
+
};
|
276
|
+
task.emitUpdate();
|
277
|
+
lastEmitted = Date.now();
|
278
|
+
//console.log('Pulling image %s: %s % [done: %s, total: %s]', image, Math.round(percent), totals.done, totals.total);
|
268
279
|
});
|
269
|
-
|
270
|
-
|
271
|
-
socketManager.emitGlobal(EVENT_IMAGE_PULL, {
|
280
|
+
task.metadata = {
|
281
|
+
...task.metadata,
|
272
282
|
image,
|
273
|
-
|
274
|
-
status: totals,
|
283
|
+
progress: 100,
|
275
284
|
timeTaken: Date.now() - timeStarted,
|
276
|
-
}
|
277
|
-
|
278
|
-
|
285
|
+
};
|
286
|
+
task.emitUpdate();
|
287
|
+
};
|
288
|
+
const task = taskManager.add(`docker:image:pull:${image}`, processor, {
|
289
|
+
name: taskName,
|
290
|
+
image,
|
291
|
+
progress: -1,
|
279
292
|
});
|
280
|
-
|
281
|
-
socketManager.emitGlobal(EVENT_IMAGE_PULL, { image, percent: 100, timeTaken: Date.now() - timeStarted });
|
293
|
+
await task.wait();
|
282
294
|
return true;
|
283
295
|
}
|
284
296
|
toDockerMounts(mounts) {
|
@@ -313,9 +325,7 @@ class ContainerManager {
|
|
313
325
|
dockerOpts.Labels.HASH = hash;
|
314
326
|
}
|
315
327
|
async ensureContainer(opts) {
|
316
|
-
|
317
|
-
await this.waitForReady(container);
|
318
|
-
return container;
|
328
|
+
return await this.createOrUpdateContainer(opts);
|
319
329
|
}
|
320
330
|
async createOrUpdateContainer(opts) {
|
321
331
|
let imagePulled = await this.pull(opts.Image);
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import { InstanceInfo, LogEntry } from './types';
|
2
|
+
import { Task } from './taskManager';
|
2
3
|
export declare class InstanceManager {
|
3
4
|
private _interval;
|
4
5
|
private readonly _instances;
|
@@ -18,11 +19,11 @@ export declare class InstanceManager {
|
|
18
19
|
registerInstanceFromSDK(systemId: string, instanceId: string, info: Omit<InstanceInfo, 'systemId' | 'instanceId'>): Promise<InstanceInfo | undefined>;
|
19
20
|
private getHealthUrl;
|
20
21
|
markAsStopped(systemId: string, instanceId: string): Promise<void>;
|
21
|
-
startAllForPlan(systemId: string): Promise<InstanceInfo[]
|
22
|
+
startAllForPlan(systemId: string): Promise<Task<InstanceInfo[]>>;
|
22
23
|
stop(systemId: string, instanceId: string): Promise<void>;
|
23
24
|
private stopInner;
|
24
|
-
stopAllForPlan(systemId: string):
|
25
|
-
start(systemId: string, instanceId: string): Promise<InstanceInfo
|
25
|
+
stopAllForPlan(systemId: string): Task<void>;
|
26
|
+
start(systemId: string, instanceId: string): Promise<InstanceInfo | Task<InstanceInfo>>;
|
26
27
|
/**
|
27
28
|
* Stops an instance but does not remove it from the list of active instances
|
28
29
|
*
|