@geminixiang/mikan 1.0.0-beta.83 → 1.0.0-beta.85
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 +28 -0
- package/README.md +2 -2
- package/dist/adapters/slack/bot.d.ts +1 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +8 -2
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/response-lifecycle.js +2 -2
- package/dist/adapters/slack/response-lifecycle.js.map +1 -1
- package/dist/cli/boot.d.ts.map +1 -1
- package/dist/cli/boot.js +4 -6
- package/dist/cli/boot.js.map +1 -1
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +75 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/office.d.ts.map +1 -1
- package/dist/cli/office.js +8 -129
- package/dist/cli/office.js.map +1 -1
- package/dist/cli/onboard.d.ts.map +1 -1
- package/dist/cli/onboard.js +3 -0
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/types.d.ts +2 -3
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js.map +1 -1
- package/dist/events/index.d.ts +2 -13
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -58
- package/dist/events/index.js.map +1 -1
- package/dist/harness/budget-stop.d.ts +3 -0
- package/dist/harness/budget-stop.d.ts.map +1 -0
- package/dist/harness/budget-stop.js +36 -0
- package/dist/harness/budget-stop.js.map +1 -0
- package/dist/harness/execution-resolver.d.ts.map +1 -1
- package/dist/harness/execution-resolver.js +5 -9
- package/dist/harness/execution-resolver.js.map +1 -1
- package/dist/harness/open-connector.d.ts +0 -9
- package/dist/harness/open-connector.d.ts.map +1 -1
- package/dist/harness/open-connector.js +1 -62
- package/dist/harness/open-connector.js.map +1 -1
- package/dist/harness/presenter.d.ts.map +1 -1
- package/dist/harness/presenter.js +31 -2
- package/dist/harness/presenter.js.map +1 -1
- package/dist/harness/prompt.js +1 -1
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/run-events.d.ts.map +1 -1
- package/dist/harness/run-events.js +6 -1
- package/dist/harness/run-events.js.map +1 -1
- package/dist/harness/skills.d.ts.map +1 -1
- package/dist/harness/skills.js +19 -14
- package/dist/harness/skills.js.map +1 -1
- package/dist/harness/tools/jev-browser.d.ts.map +1 -1
- package/dist/harness/tools/jev-browser.js +13 -15
- package/dist/harness/tools/jev-browser.js.map +1 -1
- package/dist/harness/types.d.ts +16 -0
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/main.js +8 -50
- package/dist/main.js.map +1 -1
- package/dist/migrations/actor-vaults.d.ts +3 -0
- package/dist/migrations/actor-vaults.d.ts.map +1 -0
- package/dist/migrations/actor-vaults.js +43 -0
- package/dist/migrations/actor-vaults.js.map +1 -0
- package/dist/migrations/conversation-settings.d.ts +3 -0
- package/dist/migrations/conversation-settings.d.ts.map +1 -0
- package/dist/migrations/conversation-settings.js +31 -0
- package/dist/migrations/conversation-settings.js.map +1 -0
- package/dist/migrations/index.d.ts +14 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +97 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/models-json.d.ts +3 -0
- package/dist/migrations/models-json.d.ts.map +1 -0
- package/dist/migrations/models-json.js +18 -0
- package/dist/migrations/models-json.js.map +1 -0
- package/dist/migrations/office-layout.d.ts +4 -0
- package/dist/migrations/office-layout.d.ts.map +1 -0
- package/dist/migrations/office-layout.js +94 -0
- package/dist/migrations/office-layout.js.map +1 -0
- package/dist/migrations/sandbox-containers.d.ts +3 -0
- package/dist/migrations/sandbox-containers.d.ts.map +1 -0
- package/dist/migrations/sandbox-containers.js +36 -0
- package/dist/migrations/sandbox-containers.js.map +1 -0
- package/dist/{sessions/migrate-common.d.ts → migrations/session-files.d.ts} +1 -2
- package/dist/migrations/session-files.d.ts.map +1 -0
- package/dist/{sessions/migrate-common.js → migrations/session-files.js} +2 -2
- package/dist/migrations/session-files.js.map +1 -0
- package/dist/{sessions/migrate-v3.d.ts → migrations/sessions-v3.d.ts} +4 -3
- package/dist/migrations/sessions-v3.d.ts.map +1 -0
- package/dist/{sessions/migrate-v3.js → migrations/sessions-v3.js} +14 -4
- package/dist/migrations/sessions-v3.js.map +1 -0
- package/dist/migrations/types.d.ts +30 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +2 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/migrations/workspace-events.d.ts +3 -0
- package/dist/migrations/workspace-events.d.ts.map +1 -0
- package/dist/migrations/workspace-events.js +43 -0
- package/dist/migrations/workspace-events.js.map +1 -0
- package/dist/office/index.d.ts +2 -23
- package/dist/office/index.d.ts.map +1 -1
- package/dist/office/index.js +12 -574
- package/dist/office/index.js.map +1 -1
- package/dist/office/types.d.ts +0 -10
- package/dist/office/types.d.ts.map +1 -1
- package/dist/office/types.js.map +1 -1
- package/dist/sandbox/identity.d.ts +2 -3
- package/dist/sandbox/identity.d.ts.map +1 -1
- package/dist/sandbox/identity.js +6 -13
- package/dist/sandbox/identity.js.map +1 -1
- package/dist/sandbox/layout.d.ts +0 -2
- package/dist/sandbox/layout.d.ts.map +1 -1
- package/dist/sandbox/layout.js +2 -2
- package/dist/sandbox/layout.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts +3 -37
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js +13 -452
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sessions/session-store.d.ts.map +1 -1
- package/dist/sessions/session-store.js +1 -8
- package/dist/sessions/session-store.js.map +1 -1
- package/dist/sessions/types.d.ts +0 -10
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/settings/index.d.ts +0 -48
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +6 -37
- package/dist/settings/index.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/index.d.ts +0 -7
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +1 -36
- package/dist/vault/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/sandbox.d.ts +0 -3
- package/dist/cli/sandbox.d.ts.map +0 -1
- package/dist/cli/sandbox.js +0 -80
- package/dist/cli/sandbox.js.map +0 -1
- package/dist/cli/sessions.d.ts +0 -2
- package/dist/cli/sessions.d.ts.map +0 -1
- package/dist/cli/sessions.js +0 -78
- package/dist/cli/sessions.js.map +0 -1
- package/dist/sessions/migrate-common.d.ts.map +0 -1
- package/dist/sessions/migrate-common.js.map +0 -1
- package/dist/sessions/migrate-pi-084.d.ts +0 -7
- package/dist/sessions/migrate-pi-084.d.ts.map +0 -1
- package/dist/sessions/migrate-pi-084.js +0 -293
- package/dist/sessions/migrate-pi-084.js.map +0 -1
- package/dist/sessions/migrate-v3.d.ts.map +0 -1
- package/dist/sessions/migrate-v3.js.map +0 -1
- package/dist/settings/migrate.d.ts +0 -16
- package/dist/settings/migrate.d.ts.map +0 -1
- package/dist/settings/migrate.js +0 -70
- package/dist/settings/migrate.js.map +0 -1
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { legacyConversationResourceKey, sanitizeIdentitySegment } from "./identity.js";
|
|
3
|
-
import { GUEST_HOME, GUEST_PUBLIC_OFFICES_DIR } from "./layout.js";
|
|
4
2
|
import { createHash } from "node:crypto";
|
|
5
3
|
import { readFileSync, statSync } from "node:fs";
|
|
6
4
|
import { promisify } from "node:util";
|
|
@@ -21,29 +19,16 @@ function isDockerNotFoundError(err) {
|
|
|
21
19
|
/error: no such [^\n]+/.test(haystack));
|
|
22
20
|
}
|
|
23
21
|
import { errorMessage } from "../unknown-values.js";
|
|
24
|
-
function bindSpecToMount(bindSpec) {
|
|
25
|
-
const readOnly = bindSpec.endsWith(":ro");
|
|
26
|
-
const spec = readOnly ? bindSpec.slice(0, -3) : bindSpec;
|
|
27
|
-
const separator = spec.indexOf(":");
|
|
28
|
-
return {
|
|
29
|
-
source: spec.slice(0, separator),
|
|
30
|
-
target: spec.slice(separator + 1),
|
|
31
|
-
readOnly: readOnly ? true : undefined,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
22
|
export class DockerContainerManager {
|
|
35
23
|
static { this.MANAGED_LABEL = "mikan.managed=true"; }
|
|
36
24
|
static { this.IMAGE_MODE_LABEL = "mikan.sandbox=image"; }
|
|
37
25
|
static { this.VAULT_ID_LABEL_KEY = "mikan.vault-id"; }
|
|
38
26
|
static { this.CONVERSATION_ID_LABEL_KEY = "mikan.conversation-id"; }
|
|
39
27
|
static { this.MOUNT_SIGNATURE_LABEL_KEY = "mikan.mount-signature"; }
|
|
40
|
-
static { this.MIGRATE_IMAGE_PREFIX = "mikan-migrate"; }
|
|
41
|
-
static { this.MIGRATE_BINDS_LABEL_KEY = "mikan.migrate-binds"; }
|
|
42
28
|
constructor(image, options = {}) {
|
|
43
29
|
this.image = image;
|
|
44
30
|
this.state = new Map();
|
|
45
31
|
this.inflight = new Map();
|
|
46
|
-
this.layoutMigrations = new Map();
|
|
47
32
|
this.keyQueues = new Map();
|
|
48
33
|
this.boostedKeys = new Set();
|
|
49
34
|
this.overrideLimits = new Map();
|
|
@@ -51,18 +36,12 @@ export class DockerContainerManager {
|
|
|
51
36
|
this.boostLimits = options.boostLimits;
|
|
52
37
|
this.execFileImpl = options.execFileImpl ?? execFileAsync;
|
|
53
38
|
}
|
|
54
|
-
static sanitizeSegment(value) {
|
|
55
|
-
return sanitizeIdentitySegment(value);
|
|
56
|
-
}
|
|
57
39
|
static containerName(containerKey) {
|
|
58
40
|
return `mikan-sandbox-${containerKey}`;
|
|
59
41
|
}
|
|
60
42
|
static networkName(containerKey) {
|
|
61
43
|
return `mikan-sandbox-net-${containerKey}`;
|
|
62
44
|
}
|
|
63
|
-
static homeVolumeName(containerKey) {
|
|
64
|
-
return `mikan-home-${containerKey}`;
|
|
65
|
-
}
|
|
66
45
|
serialize(containerKey, task) {
|
|
67
46
|
const previous = this.keyQueues.get(containerKey) ?? Promise.resolve();
|
|
68
47
|
const next = previous.then(task, task);
|
|
@@ -87,28 +66,16 @@ export class DockerContainerManager {
|
|
|
87
66
|
async provisionInner(containerKey, options) {
|
|
88
67
|
const containerName = options.containerName ?? DockerContainerManager.containerName(containerKey);
|
|
89
68
|
const mounts = options.mounts ?? [];
|
|
90
|
-
await this.migrateContainerLayout(containerName);
|
|
91
69
|
const status = await this.inspectStatus(containerName);
|
|
92
70
|
try {
|
|
93
|
-
const binds = status === "missing" ? [] : await this.inspectBindMounts(containerName);
|
|
94
|
-
const homeVolume = this.hasHomeVolumeBind(containerKey, binds);
|
|
95
71
|
const drift = status === "missing"
|
|
96
72
|
? undefined
|
|
97
|
-
: await this.runtimeDrift(containerKey, containerName, mounts,
|
|
98
|
-
|
|
99
|
-
status,
|
|
100
|
-
homeVolume,
|
|
101
|
-
});
|
|
102
|
-
if (drift && homeVolume) {
|
|
73
|
+
: await this.runtimeDrift(containerKey, containerName, mounts, status);
|
|
74
|
+
if (drift) {
|
|
103
75
|
log.logInfo(`Container ${containerName} is out of date (${drift}); replacing container`);
|
|
104
|
-
await this.
|
|
76
|
+
await this.replaceContainer(containerKey, containerName, mounts, options);
|
|
105
77
|
log.logInfo(`Container ${containerName} replaced from image ${this.image}`);
|
|
106
78
|
}
|
|
107
|
-
else if (drift) {
|
|
108
|
-
log.logInfo(`Container ${containerName} configuration changed (${drift}); recreating container`);
|
|
109
|
-
await this.recreateContainerPreservingContents(containerKey, containerName, mounts);
|
|
110
|
-
log.logInfo(`Container ${containerName} recreated`);
|
|
111
|
-
}
|
|
112
79
|
else if (status === "stopped") {
|
|
113
80
|
await this.execFileImpl("docker", ["start", containerName]);
|
|
114
81
|
log.logInfo(`Container ${containerName} started`);
|
|
@@ -173,10 +140,10 @@ export class DockerContainerManager {
|
|
|
173
140
|
log.logWarning(`Failed to stop container ${containerName}`, errorMessage(err));
|
|
174
141
|
}
|
|
175
142
|
}
|
|
176
|
-
remove(containerKey
|
|
177
|
-
return this.serialize(containerKey, () => this.removeInner(containerKey
|
|
143
|
+
remove(containerKey) {
|
|
144
|
+
return this.serialize(containerKey, () => this.removeInner(containerKey));
|
|
178
145
|
}
|
|
179
|
-
async removeInner(containerKey
|
|
146
|
+
async removeInner(containerKey) {
|
|
180
147
|
const containerName = this.getContainerName(containerKey);
|
|
181
148
|
const networkName = DockerContainerManager.networkName(containerKey);
|
|
182
149
|
const removed = await this.forceRemoveContainer(containerName, `Container ${containerName} removed`, `Failed to remove container ${containerName}`);
|
|
@@ -190,9 +157,6 @@ export class DockerContainerManager {
|
|
|
190
157
|
catch (err) {
|
|
191
158
|
log.logWarning(`Failed to remove network ${networkName}`, errorMessage(err));
|
|
192
159
|
}
|
|
193
|
-
await this.removeMigrateSnapshot(containerName);
|
|
194
|
-
if (options.purgeHome)
|
|
195
|
-
await this.removeHomeVolume(containerKey);
|
|
196
160
|
this.state.delete(containerKey);
|
|
197
161
|
this.boostedKeys.delete(containerKey);
|
|
198
162
|
this.overrideLimits.delete(containerKey);
|
|
@@ -207,291 +171,25 @@ export class DockerContainerManager {
|
|
|
207
171
|
}
|
|
208
172
|
await Promise.all(toStop.map((containerKey) => this.stop(containerKey)));
|
|
209
173
|
}
|
|
210
|
-
async removeContainersForConversations(conversationIds) {
|
|
211
|
-
if (conversationIds.size === 0)
|
|
212
|
-
return;
|
|
213
|
-
const names = await this.listContainerNamesByLabel();
|
|
214
|
-
await Promise.all(names.map(async (containerName) => {
|
|
215
|
-
const details = await this.inspectContainerDetails(containerName);
|
|
216
|
-
if (!details?.conversationId || !conversationIds.has(details.conversationId))
|
|
217
|
-
return;
|
|
218
|
-
const removed = await this.forceRemoveContainer(containerName, `Removed container ${containerName} after office migration`, `Failed to remove container ${containerName} after office migration`);
|
|
219
|
-
const containerKey = this.containerKeyFromContainerName(containerName);
|
|
220
|
-
if (removed && containerKey)
|
|
221
|
-
await this.removeHomeVolume(containerKey);
|
|
222
|
-
}));
|
|
223
|
-
}
|
|
224
|
-
armContainerLayoutMigration(translator) {
|
|
225
|
-
this.bindTranslator = translator;
|
|
226
|
-
}
|
|
227
|
-
async sweepContainerLayoutMigration(delayMs = 2000) {
|
|
228
|
-
if (!this.bindTranslator)
|
|
229
|
-
return;
|
|
230
|
-
let names;
|
|
231
|
-
try {
|
|
232
|
-
const containers = await this.listContainerNamesByLabel();
|
|
233
|
-
const snapshots = await this.listMigrateSnapshotContainerNames();
|
|
234
|
-
names = Array.from(new Set([...containers, ...snapshots]));
|
|
235
|
-
}
|
|
236
|
-
catch (err) {
|
|
237
|
-
log.logWarning("Container layout sweep could not list containers", String(err));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
for (const containerName of names) {
|
|
241
|
-
try {
|
|
242
|
-
await this.migrateContainerLayout(containerName);
|
|
243
|
-
}
|
|
244
|
-
catch (err) {
|
|
245
|
-
log.logWarning(`Container layout migration failed for ${containerName}; leaving it in place`, errorMessage(err));
|
|
246
|
-
}
|
|
247
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
248
|
-
}
|
|
249
|
-
await this.removeDanglingMigrateImages();
|
|
250
|
-
}
|
|
251
|
-
migrateContainerLayout(containerName) {
|
|
252
|
-
const translator = this.bindTranslator;
|
|
253
|
-
if (!translator)
|
|
254
|
-
return Promise.resolve();
|
|
255
|
-
const existing = this.layoutMigrations.get(containerName);
|
|
256
|
-
if (existing)
|
|
257
|
-
return existing;
|
|
258
|
-
const pending = this.migrateContainerLayoutInner(containerName, translator).finally(() => {
|
|
259
|
-
this.layoutMigrations.delete(containerName);
|
|
260
|
-
});
|
|
261
|
-
this.layoutMigrations.set(containerName, pending);
|
|
262
|
-
return pending;
|
|
263
|
-
}
|
|
264
|
-
async migrateContainerLayoutInner(containerName, translator) {
|
|
265
|
-
const snapshotImage = `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`;
|
|
266
|
-
let originalBinds;
|
|
267
|
-
const status = await this.inspectStatus(containerName);
|
|
268
|
-
if (status === "missing") {
|
|
269
|
-
const labeled = await this.readMigrateImageBinds(snapshotImage);
|
|
270
|
-
if (labeled === undefined)
|
|
271
|
-
return;
|
|
272
|
-
originalBinds = labeled;
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
originalBinds = await this.inspectBindMounts(containerName);
|
|
276
|
-
const translated = originalBinds.map(translator);
|
|
277
|
-
if (this.sameBinds(translated.toSorted(), originalBinds.slice().toSorted())) {
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
log.logInfo(`Migrating container ${containerName} to the office layout`);
|
|
281
|
-
await this.execFileImpl("docker", [
|
|
282
|
-
"commit",
|
|
283
|
-
"-c",
|
|
284
|
-
`LABEL ${DockerContainerManager.MIGRATE_BINDS_LABEL_KEY}=${JSON.stringify(JSON.stringify(originalBinds))}`,
|
|
285
|
-
containerName,
|
|
286
|
-
snapshotImage,
|
|
287
|
-
]);
|
|
288
|
-
await this.execFileImpl("docker", ["rm", "-f", containerName]);
|
|
289
|
-
}
|
|
290
|
-
await this.createContainerFromSnapshot(containerName, originalBinds.map(translator));
|
|
291
|
-
await this.removeMigrateSnapshot(containerName);
|
|
292
|
-
log.logInfo(`Container ${containerName} migrated to the office layout`);
|
|
293
|
-
}
|
|
294
|
-
async createContainerFromSnapshot(containerName, bindSpecs, knownContainerKey) {
|
|
295
|
-
const snapshotImage = `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`;
|
|
296
|
-
const containerKey = knownContainerKey ?? this.containerKeyFromContainerName(containerName) ?? containerName;
|
|
297
|
-
const networkName = await this.ensureNetwork(containerKey);
|
|
298
|
-
const conversationId = await this.readImageLabel(snapshotImage, DockerContainerManager.CONVERSATION_ID_LABEL_KEY);
|
|
299
|
-
const labels = [
|
|
300
|
-
"--label",
|
|
301
|
-
DockerContainerManager.MANAGED_LABEL,
|
|
302
|
-
"--label",
|
|
303
|
-
DockerContainerManager.IMAGE_MODE_LABEL,
|
|
304
|
-
"--label",
|
|
305
|
-
`${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
|
|
306
|
-
];
|
|
307
|
-
if (conversationId) {
|
|
308
|
-
labels.push("--label", `${DockerContainerManager.CONVERSATION_ID_LABEL_KEY}=${conversationId}`);
|
|
309
|
-
}
|
|
310
|
-
labels.push("--label", `${DockerContainerManager.MOUNT_SIGNATURE_LABEL_KEY}=${this.mountSignature(bindSpecs.map(bindSpecToMount))}`);
|
|
311
|
-
await this.execFileImpl("docker", [
|
|
312
|
-
"create",
|
|
313
|
-
"--name",
|
|
314
|
-
containerName,
|
|
315
|
-
"--network",
|
|
316
|
-
networkName,
|
|
317
|
-
"--cap-drop",
|
|
318
|
-
"ALL",
|
|
319
|
-
"--security-opt",
|
|
320
|
-
"no-new-privileges",
|
|
321
|
-
"--pids-limit",
|
|
322
|
-
"1024",
|
|
323
|
-
...labels,
|
|
324
|
-
...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
|
|
325
|
-
...bindSpecs.flatMap((bind) => ["-v", bind]),
|
|
326
|
-
snapshotImage,
|
|
327
|
-
"sleep",
|
|
328
|
-
"infinity",
|
|
329
|
-
]);
|
|
330
|
-
}
|
|
331
|
-
async removeStaleMountpoints(containerName, bindSpecs) {
|
|
332
|
-
const keep = new Set(bindSpecs
|
|
333
|
-
.map((bind) => bindSpecToMount(bind).target)
|
|
334
|
-
.filter((target) => target.startsWith(`${GUEST_PUBLIC_OFFICES_DIR}/`))
|
|
335
|
-
.map((target) => target.slice(GUEST_PUBLIC_OFFICES_DIR.length + 1)));
|
|
336
|
-
const script = [
|
|
337
|
-
`cd ${GUEST_PUBLIC_OFFICES_DIR} 2>/dev/null || exit 0`,
|
|
338
|
-
"for d in *; do",
|
|
339
|
-
' [ -e "$d" ] || continue',
|
|
340
|
-
' case " $KEEP " in *" ${d##*/} "*) continue;; esac',
|
|
341
|
-
' rmdir "$d" 2>/dev/null || true',
|
|
342
|
-
"done",
|
|
343
|
-
].join("\n");
|
|
344
|
-
try {
|
|
345
|
-
await this.execFileImpl("docker", [
|
|
346
|
-
"exec",
|
|
347
|
-
"-e",
|
|
348
|
-
`KEEP=${[...keep].join(" ")}`,
|
|
349
|
-
containerName,
|
|
350
|
-
"sh",
|
|
351
|
-
"-c",
|
|
352
|
-
script,
|
|
353
|
-
]);
|
|
354
|
-
}
|
|
355
|
-
catch (err) {
|
|
356
|
-
log.logWarning(`Could not clean stale mountpoints in ${containerName}`, String(err));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
async recreateContainerPreservingContents(containerKey, containerName, mounts) {
|
|
360
|
-
const snapshotImage = `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`;
|
|
361
|
-
const bindSpecs = mounts.map((mount) => this.toBindSpec(mount));
|
|
362
|
-
await this.execFileImpl("docker", [
|
|
363
|
-
"commit",
|
|
364
|
-
"-c",
|
|
365
|
-
`LABEL ${DockerContainerManager.MIGRATE_BINDS_LABEL_KEY}=${JSON.stringify(JSON.stringify(bindSpecs))}`,
|
|
366
|
-
containerName,
|
|
367
|
-
snapshotImage,
|
|
368
|
-
]);
|
|
369
|
-
await this.execFileImpl("docker", ["rm", "-f", containerName]);
|
|
370
|
-
await this.createContainerFromSnapshot(containerName, bindSpecs, containerKey);
|
|
371
|
-
await this.execFileImpl("docker", ["start", containerName]);
|
|
372
|
-
await this.removeStaleMountpoints(containerName, bindSpecs);
|
|
373
|
-
await this.removeMigrateSnapshot(containerName);
|
|
374
|
-
}
|
|
375
|
-
async readMigrateImageBinds(snapshotImage) {
|
|
376
|
-
const raw = await this.readImageLabel(snapshotImage, DockerContainerManager.MIGRATE_BINDS_LABEL_KEY);
|
|
377
|
-
if (raw === undefined)
|
|
378
|
-
return undefined;
|
|
379
|
-
const parsed = JSON.parse(raw);
|
|
380
|
-
if (!Array.isArray(parsed) || parsed.some((bind) => typeof bind !== "string")) {
|
|
381
|
-
throw new Error(`Snapshot image ${snapshotImage} carries malformed migrate binds`);
|
|
382
|
-
}
|
|
383
|
-
return parsed;
|
|
384
|
-
}
|
|
385
|
-
async readImageLabel(image, key) {
|
|
386
|
-
try {
|
|
387
|
-
const { stdout } = await this.execFileImpl("docker", [
|
|
388
|
-
"inspect",
|
|
389
|
-
"-f",
|
|
390
|
-
`{{index .Config.Labels "${key}"}}`,
|
|
391
|
-
image,
|
|
392
|
-
]);
|
|
393
|
-
return this.normalizeDockerValue(stdout.trim());
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
return undefined;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
async removeMigrateSnapshot(containerName) {
|
|
400
|
-
try {
|
|
401
|
-
await this.execFileImpl("docker", [
|
|
402
|
-
"rmi",
|
|
403
|
-
`${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`,
|
|
404
|
-
]);
|
|
405
|
-
}
|
|
406
|
-
catch { }
|
|
407
|
-
}
|
|
408
|
-
async listMigrateSnapshotContainerNames() {
|
|
409
|
-
try {
|
|
410
|
-
const { stdout } = await this.execFileImpl("docker", [
|
|
411
|
-
"images",
|
|
412
|
-
DockerContainerManager.MIGRATE_IMAGE_PREFIX,
|
|
413
|
-
"--format",
|
|
414
|
-
"{{.Tag}}",
|
|
415
|
-
]);
|
|
416
|
-
return this.parseNameLines(stdout);
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
return [];
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
async removeDanglingMigrateImages() {
|
|
423
|
-
let stdout;
|
|
424
|
-
try {
|
|
425
|
-
({ stdout } = await this.execFileImpl("docker", [
|
|
426
|
-
"images",
|
|
427
|
-
`${DockerContainerManager.MIGRATE_IMAGE_PREFIX}`,
|
|
428
|
-
"--format",
|
|
429
|
-
"{{.Repository}}:{{.Tag}}",
|
|
430
|
-
]));
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
for (const image of this.parseNameLines(stdout)) {
|
|
436
|
-
const containerName = image.slice(DockerContainerManager.MIGRATE_IMAGE_PREFIX.length + 1);
|
|
437
|
-
const status = await this.inspectStatus(containerName);
|
|
438
|
-
if (status === "missing")
|
|
439
|
-
continue;
|
|
440
|
-
if (await this.containerRunsFromImage(containerName, image))
|
|
441
|
-
continue;
|
|
442
|
-
try {
|
|
443
|
-
await this.execFileImpl("docker", ["rmi", image]);
|
|
444
|
-
log.logInfo(`Removed layout-migration snapshot image ${image}`);
|
|
445
|
-
}
|
|
446
|
-
catch (err) {
|
|
447
|
-
log.logWarning(`Could not remove snapshot image ${image}`, String(err));
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
async containerRunsFromImage(containerName, image) {
|
|
452
|
-
const { stdout } = await this.execFileImpl("docker", [
|
|
453
|
-
"inspect",
|
|
454
|
-
"-f",
|
|
455
|
-
"{{.Config.Image}}",
|
|
456
|
-
containerName,
|
|
457
|
-
]);
|
|
458
|
-
return stdout.trim() === image;
|
|
459
|
-
}
|
|
460
174
|
async reconcile() {
|
|
461
|
-
const
|
|
462
|
-
const labeledNames = await this.listContainerNamesByLabel();
|
|
463
|
-
for (const name of labeledNames)
|
|
464
|
-
discovered.add(name);
|
|
465
|
-
const legacyNames = await this.listContainerNamesByPrefix();
|
|
466
|
-
for (const name of legacyNames)
|
|
467
|
-
discovered.add(name);
|
|
175
|
+
const names = await this.listContainerNamesByLabel();
|
|
468
176
|
this.state.clear();
|
|
469
|
-
const inspected = await Promise.all(
|
|
177
|
+
const inspected = await Promise.all(names.map(async (containerName) => ({
|
|
470
178
|
containerName,
|
|
471
179
|
details: await this.inspectContainerDetails(containerName),
|
|
472
180
|
})));
|
|
473
|
-
const legacyRemovals = [];
|
|
474
181
|
for (const { containerName, details } of inspected) {
|
|
475
182
|
if (!details)
|
|
476
183
|
continue;
|
|
477
|
-
if (!details.conversationId) {
|
|
478
|
-
legacyRemovals.push(this.removeLegacyContainer(containerName));
|
|
479
|
-
continue;
|
|
480
|
-
}
|
|
481
184
|
const containerKey = this.containerKeyFromContainerName(containerName);
|
|
482
185
|
if (!containerKey) {
|
|
483
186
|
log.logWarning(`Skipping unmanaged-style container without container key`, containerName);
|
|
484
187
|
continue;
|
|
485
188
|
}
|
|
486
|
-
if (containerKey === legacyConversationResourceKey(details.conversationId)) {
|
|
487
|
-
legacyRemovals.push(this.forceRemoveContainer(containerName, `Removed container ${containerName} keyed by pre-office resource identity`, `Failed to remove legacy-keyed container ${containerName}`).then(() => undefined));
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
189
|
const status = details.running ? "running" : "stopped";
|
|
491
190
|
const lastUsed = details.startedAtMs ?? Date.now();
|
|
492
191
|
this.state.set(containerKey, { status, lastUsed, containerName });
|
|
493
192
|
}
|
|
494
|
-
await Promise.all(legacyRemovals);
|
|
495
193
|
const running = Array.from(this.state.values()).filter((s) => s.status === "running").length;
|
|
496
194
|
const stopped = this.state.size - running;
|
|
497
195
|
log.logInfo(`Reconciled ${this.state.size} managed containers (running=${running}, stopped=${stopped})`);
|
|
@@ -511,7 +209,6 @@ export class DockerContainerManager {
|
|
|
511
209
|
}
|
|
512
210
|
async runContainer(containerKey, containerName, mounts, options) {
|
|
513
211
|
const networkName = await this.ensureNetwork(containerKey);
|
|
514
|
-
const homeVolume = await this.ensureHomeVolume(containerKey);
|
|
515
212
|
log.logInfo(`Creating container ${containerName} from image ${this.image}`);
|
|
516
213
|
const labels = [
|
|
517
214
|
"--label",
|
|
@@ -542,8 +239,6 @@ export class DockerContainerManager {
|
|
|
542
239
|
"1024",
|
|
543
240
|
...labels,
|
|
544
241
|
...this.resourceLimitArgs(this.effectiveLimits(containerKey)),
|
|
545
|
-
"-v",
|
|
546
|
-
`${homeVolume}:${GUEST_HOME}`,
|
|
547
242
|
...this.mountArgs(mounts),
|
|
548
243
|
this.image,
|
|
549
244
|
"sleep",
|
|
@@ -593,23 +288,19 @@ export class DockerContainerManager {
|
|
|
593
288
|
});
|
|
594
289
|
}
|
|
595
290
|
}
|
|
596
|
-
async runtimeDrift(containerKey, containerName, mounts,
|
|
597
|
-
|
|
598
|
-
if (this.hasBindMountDrift(binds, mounts))
|
|
291
|
+
async runtimeDrift(containerKey, containerName, mounts, status) {
|
|
292
|
+
if (this.hasBindMountDrift(await this.inspectBindMounts(containerName), mounts))
|
|
599
293
|
return "binds";
|
|
600
294
|
if (await this.hasMountSignatureDrift(containerName, mounts))
|
|
601
295
|
return "mount-content";
|
|
602
296
|
if (await this.hasNetworkModeDrift(containerKey, containerName))
|
|
603
297
|
return "network";
|
|
604
|
-
if (
|
|
298
|
+
if (status === "stopped" && (await this.hasImageDrift(containerName)))
|
|
605
299
|
return "image";
|
|
606
|
-
}
|
|
607
300
|
return undefined;
|
|
608
301
|
}
|
|
609
302
|
hasBindMountDrift(binds, mounts) {
|
|
610
|
-
|
|
611
|
-
const actual = binds.filter((bind) => !this.isHomeVolumeBind(bind));
|
|
612
|
-
return !this.sameBinds(expected, actual);
|
|
303
|
+
return !this.sameBinds(this.expectedBinds(mounts), [...binds]);
|
|
613
304
|
}
|
|
614
305
|
expectedBinds(mounts) {
|
|
615
306
|
return mounts
|
|
@@ -675,21 +366,6 @@ export class DockerContainerManager {
|
|
|
675
366
|
}
|
|
676
367
|
return [...parsed].toSorted();
|
|
677
368
|
}
|
|
678
|
-
isHomeVolumeBind(bind) {
|
|
679
|
-
const separator = bind.indexOf(":");
|
|
680
|
-
if (separator === -1)
|
|
681
|
-
return false;
|
|
682
|
-
const source = bind.slice(0, separator);
|
|
683
|
-
const target = bind.slice(separator + 1).replace(/:r[ow]$/, "");
|
|
684
|
-
return source.startsWith(DockerContainerManager.homeVolumeName("")) && target === GUEST_HOME;
|
|
685
|
-
}
|
|
686
|
-
hasHomeVolumeBind(containerKey, binds) {
|
|
687
|
-
const expected = `${DockerContainerManager.homeVolumeName(containerKey)}:${GUEST_HOME}`;
|
|
688
|
-
return binds.includes(expected);
|
|
689
|
-
}
|
|
690
|
-
async hasHomeVolume(containerKey, containerName) {
|
|
691
|
-
return this.hasHomeVolumeBind(containerKey, await this.inspectBindMounts(containerName));
|
|
692
|
-
}
|
|
693
369
|
async hasImageDrift(containerName) {
|
|
694
370
|
const desired = await this.localImageId();
|
|
695
371
|
if (!desired)
|
|
@@ -717,107 +393,12 @@ export class DockerContainerManager {
|
|
|
717
393
|
return undefined;
|
|
718
394
|
}
|
|
719
395
|
}
|
|
720
|
-
async
|
|
721
|
-
const volumeName = DockerContainerManager.homeVolumeName(containerKey);
|
|
722
|
-
await this.execFileImpl("docker", [
|
|
723
|
-
"volume",
|
|
724
|
-
"create",
|
|
725
|
-
"--label",
|
|
726
|
-
DockerContainerManager.MANAGED_LABEL,
|
|
727
|
-
"--label",
|
|
728
|
-
`${DockerContainerManager.VAULT_ID_LABEL_KEY}=${containerKey}`,
|
|
729
|
-
volumeName,
|
|
730
|
-
]);
|
|
731
|
-
return volumeName;
|
|
732
|
-
}
|
|
733
|
-
async removeHomeVolume(containerKey) {
|
|
734
|
-
const volumeName = DockerContainerManager.homeVolumeName(containerKey);
|
|
735
|
-
try {
|
|
736
|
-
await this.execFileImpl("docker", ["volume", "rm", volumeName]);
|
|
737
|
-
log.logInfo(`Home volume ${volumeName} removed`);
|
|
738
|
-
}
|
|
739
|
-
catch (err) {
|
|
740
|
-
if (isDockerNotFoundError(err))
|
|
741
|
-
return;
|
|
742
|
-
log.logWarning(`Failed to remove home volume ${volumeName}`, errorMessage(err));
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async replaceHomeVolumeContainer(containerKey, containerName, mounts, options) {
|
|
396
|
+
async replaceContainer(containerKey, containerName, mounts, options) {
|
|
746
397
|
const removed = await this.forceRemoveContainer(containerName, `Container ${containerName} removed for replacement`, `Failed to remove container ${containerName} for replacement`);
|
|
747
398
|
if (!removed)
|
|
748
399
|
throw new Error(`Failed to remove container ${containerName} for replacement`);
|
|
749
400
|
await this.runContainer(containerKey, containerName, mounts, options);
|
|
750
401
|
}
|
|
751
|
-
async inventory() {
|
|
752
|
-
const names = await this.listContainerNamesByLabel();
|
|
753
|
-
const entries = [];
|
|
754
|
-
for (const containerName of names) {
|
|
755
|
-
const containerKey = this.containerKeyFromContainerName(containerName);
|
|
756
|
-
const status = await this.inspectStatus(containerName);
|
|
757
|
-
if (status === "missing")
|
|
758
|
-
continue;
|
|
759
|
-
const homeVolume = containerKey
|
|
760
|
-
? await this.hasHomeVolume(containerKey, containerName)
|
|
761
|
-
: false;
|
|
762
|
-
entries.push({
|
|
763
|
-
containerName,
|
|
764
|
-
containerKey,
|
|
765
|
-
running: status === "running",
|
|
766
|
-
homeVolume,
|
|
767
|
-
imageStale: await this.hasImageDrift(containerName),
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
return entries;
|
|
771
|
-
}
|
|
772
|
-
async systemChanges(containerName) {
|
|
773
|
-
const { stdout } = await this.execFileImpl("docker", ["diff", containerName]);
|
|
774
|
-
return this.parseNameLines(stdout).filter((line) => {
|
|
775
|
-
const path = line.slice(2);
|
|
776
|
-
return !this.isUnderGuestPath(path, GUEST_HOME) && !this.isUnderGuestPath(path, "/workspace");
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
isUnderGuestPath(path, root) {
|
|
780
|
-
return path === root || path.startsWith(`${root}/`);
|
|
781
|
-
}
|
|
782
|
-
migrateToHomeVolume(containerKey) {
|
|
783
|
-
return this.serialize(containerKey, () => this.migrateToHomeVolumeInner(containerKey));
|
|
784
|
-
}
|
|
785
|
-
async migrateToHomeVolumeInner(containerKey) {
|
|
786
|
-
const containerName = DockerContainerManager.containerName(containerKey);
|
|
787
|
-
const status = await this.inspectStatus(containerName);
|
|
788
|
-
if (status === "missing")
|
|
789
|
-
return "missing";
|
|
790
|
-
if (await this.hasHomeVolume(containerKey, containerName))
|
|
791
|
-
return "already-migrated";
|
|
792
|
-
const details = await this.inspectContainerDetails(containerName);
|
|
793
|
-
const mounts = (await this.inspectBindMounts(containerName))
|
|
794
|
-
.filter((bind) => !this.isHomeVolumeBind(bind))
|
|
795
|
-
.map(bindSpecToMount);
|
|
796
|
-
const snapshotImage = `${DockerContainerManager.MIGRATE_IMAGE_PREFIX}:${containerName}`;
|
|
797
|
-
log.logInfo(`Migrating container ${containerName} to a home volume`);
|
|
798
|
-
await this.execFileImpl("docker", ["commit", containerName, snapshotImage]);
|
|
799
|
-
const homeVolume = await this.ensureHomeVolume(containerKey);
|
|
800
|
-
await this.execFileImpl("docker", [
|
|
801
|
-
"run",
|
|
802
|
-
"--rm",
|
|
803
|
-
"--network",
|
|
804
|
-
"none",
|
|
805
|
-
"-v",
|
|
806
|
-
`${homeVolume}:${GUEST_HOME}`,
|
|
807
|
-
snapshotImage,
|
|
808
|
-
"true",
|
|
809
|
-
]);
|
|
810
|
-
const removed = await this.forceRemoveContainer(containerName, `Container ${containerName} removed for home-volume migration`, `Failed to remove container ${containerName} for home-volume migration`);
|
|
811
|
-
if (!removed)
|
|
812
|
-
throw new Error(`Failed to remove container ${containerName}`);
|
|
813
|
-
await this.runContainer(containerKey, containerName, mounts, details?.conversationId ? { conversationId: details.conversationId } : {});
|
|
814
|
-
if (status === "stopped")
|
|
815
|
-
await this.execFileImpl("docker", ["stop", containerName]);
|
|
816
|
-
await this.removeMigrateSnapshot(containerName);
|
|
817
|
-
this.setState(containerKey, status === "stopped" ? "stopped" : "running", containerName);
|
|
818
|
-
log.logInfo(`Container ${containerName} migrated to home volume ${homeVolume}`);
|
|
819
|
-
return "migrated";
|
|
820
|
-
}
|
|
821
402
|
async hasNetworkModeDrift(containerKey, containerName) {
|
|
822
403
|
const expected = DockerContainerManager.networkName(containerKey);
|
|
823
404
|
const { stdout } = await this.execFileImpl("docker", [
|
|
@@ -888,23 +469,6 @@ export class DockerContainerManager {
|
|
|
888
469
|
return [];
|
|
889
470
|
}
|
|
890
471
|
}
|
|
891
|
-
async listContainerNamesByPrefix() {
|
|
892
|
-
try {
|
|
893
|
-
const { stdout } = await this.execFileImpl("docker", [
|
|
894
|
-
"ps",
|
|
895
|
-
"-a",
|
|
896
|
-
"--filter",
|
|
897
|
-
`name=${DockerContainerManager.containerName("")}`,
|
|
898
|
-
"--format",
|
|
899
|
-
"{{.Names}}",
|
|
900
|
-
]);
|
|
901
|
-
return this.parseNameLines(stdout);
|
|
902
|
-
}
|
|
903
|
-
catch (err) {
|
|
904
|
-
log.logWarning("Failed to list legacy managed containers", errorMessage(err));
|
|
905
|
-
return [];
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
472
|
parseNameLines(stdout) {
|
|
909
473
|
return stdout
|
|
910
474
|
.split("\n")
|
|
@@ -964,8 +528,5 @@ export class DockerContainerManager {
|
|
|
964
528
|
return false;
|
|
965
529
|
}
|
|
966
530
|
}
|
|
967
|
-
async removeLegacyContainer(containerName) {
|
|
968
|
-
await this.forceRemoveContainer(containerName, `Removed legacy mikan container ${containerName} (pre-channel-isolation scheme)`, `Failed to remove legacy mikan container ${containerName}`);
|
|
969
|
-
}
|
|
970
531
|
}
|
|
971
532
|
//# sourceMappingURL=provisioner.js.map
|