@decocms/sandbox-controller 0.15.4 → 0.16.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/dist/index.d.ts +23 -0
- package/dist/index.js +74 -12
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3750,6 +3750,8 @@ interface FreestyleSandboxProviderOptions {
|
|
|
3750
3750
|
meter?: Meter;
|
|
3751
3751
|
/** Replaces the API client, for tests. */
|
|
3752
3752
|
client?: Freestyle;
|
|
3753
|
+
/** How long a started VM's daemon gets to answer /health, for tests. */
|
|
3754
|
+
daemonBootTimeoutMs?: number;
|
|
3753
3755
|
}
|
|
3754
3756
|
declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
3755
3757
|
private readonly apiKey;
|
|
@@ -3761,6 +3763,7 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3761
3763
|
private readonly idleTimeoutSeconds;
|
|
3762
3764
|
private readonly idlePauseMs;
|
|
3763
3765
|
private readonly autoDeleteSeconds;
|
|
3766
|
+
private readonly daemonBootTimeoutMs;
|
|
3764
3767
|
private readonly domainSuffix;
|
|
3765
3768
|
private readonly timeDaemonRequest;
|
|
3766
3769
|
private readonly inflight;
|
|
@@ -3867,7 +3870,19 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3867
3870
|
private createVm;
|
|
3868
3871
|
private configPayload;
|
|
3869
3872
|
private waitForDaemon;
|
|
3873
|
+
/**
|
|
3874
|
+
* Deletes the VM only once its shutdown sync is confirmed. Otherwise the
|
|
3875
|
+
* user's unpushed work may still be on its disk, so it is detached instead:
|
|
3876
|
+
* the handle is free for a new sandbox, and the VM stays recoverable by id
|
|
3877
|
+
* until Freestyle's auto-delete.
|
|
3878
|
+
*/
|
|
3870
3879
|
delete(handle: string): Promise<void>;
|
|
3880
|
+
/**
|
|
3881
|
+
* Stops the daemon, which publishes unsaved work to git on SIGTERM, then the
|
|
3882
|
+
* sidecar, which flushes its mounts. The daemon exits 0 even when that
|
|
3883
|
+
* publish fails, so its log decides.
|
|
3884
|
+
*/
|
|
3885
|
+
private syncedOnShutdown;
|
|
3871
3886
|
lastTermination(_handle: string): Promise<PodTermination | null>;
|
|
3872
3887
|
/** A paused VM is alive: the next request resumes it. */
|
|
3873
3888
|
alive(handle: string): Promise<boolean>;
|
|
@@ -3954,6 +3969,13 @@ declare class SandboxProviderRouter implements SandboxProvider {
|
|
|
3954
3969
|
* reclaims a stray paused VM anyway, only later.
|
|
3955
3970
|
*/
|
|
3956
3971
|
private readonly strays;
|
|
3972
|
+
/**
|
|
3973
|
+
* The latest ensure per handle. Placement reads what exists, so two at once
|
|
3974
|
+
* each found nothing and put one sandbox on each provider.
|
|
3975
|
+
*
|
|
3976
|
+
* ponytail: in-process; replicas would need the state store's lock.
|
|
3977
|
+
*/
|
|
3978
|
+
private readonly ensuring;
|
|
3957
3979
|
private readonly freestyleShare;
|
|
3958
3980
|
constructor(providers: SandboxProviders, opts?: SandboxProviderRouterOptions);
|
|
3959
3981
|
private get;
|
|
@@ -3964,6 +3986,7 @@ declare class SandboxProviderRouter implements SandboxProvider {
|
|
|
3964
3986
|
/** Where `ensure` puts this sandbox, and why. */
|
|
3965
3987
|
place(handle: string, opts: EnsureOptions): Promise<Placement>;
|
|
3966
3988
|
ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
|
|
3989
|
+
private ensureOne;
|
|
3967
3990
|
/** Delete the sandbox a fallback left behind on `kind`, in the background. */
|
|
3968
3991
|
private retire;
|
|
3969
3992
|
delete(handle: string): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -6804,16 +6804,17 @@ var Freestyle = class {
|
|
|
6804
6804
|
// ../sandbox/package.json
|
|
6805
6805
|
var package_default = {
|
|
6806
6806
|
name: "@decocms/sandbox",
|
|
6807
|
-
version: "1.
|
|
6807
|
+
version: "1.96.0",
|
|
6808
6808
|
private: true,
|
|
6809
6809
|
type: "module",
|
|
6810
6810
|
description: "Sandbox runtime for isolated Studio tool execution",
|
|
6811
6811
|
scripts: {
|
|
6812
6812
|
check: "tsc --noEmit",
|
|
6813
6813
|
test: "bun test",
|
|
6814
|
-
build: "bun run build:typegen && bun run build:harness-runner",
|
|
6814
|
+
build: "bun run build:typegen && bun run build:harness-runner && bun run build:studio-cli",
|
|
6815
6815
|
"build:typegen": "mkdir -p dist && bun run --cwd=../typegen build && bun --cwd=../typegen pm pack --filename $PWD/dist/typegen.tgz --quiet",
|
|
6816
|
-
"build:harness-runner": "mkdir -p dist && bun --cwd=../harness-runner pm pack --filename $PWD/dist/harness-runner.tgz --quiet"
|
|
6816
|
+
"build:harness-runner": "mkdir -p dist && bun --cwd=../harness-runner pm pack --filename $PWD/dist/harness-runner.tgz --quiet",
|
|
6817
|
+
"build:studio-cli": "bun scripts/build-studio-cli.ts dist/decocms"
|
|
6817
6818
|
},
|
|
6818
6819
|
exports: {
|
|
6819
6820
|
"./shared": "./shared.ts",
|
|
@@ -6926,6 +6927,7 @@ var FreestyleSandboxProvider = class {
|
|
|
6926
6927
|
idleTimeoutSeconds;
|
|
6927
6928
|
idlePauseMs;
|
|
6928
6929
|
autoDeleteSeconds;
|
|
6930
|
+
daemonBootTimeoutMs;
|
|
6929
6931
|
domainSuffix;
|
|
6930
6932
|
timeDaemonRequest;
|
|
6931
6933
|
inflight = new Inflight();
|
|
@@ -6966,6 +6968,7 @@ var FreestyleSandboxProvider = class {
|
|
|
6966
6968
|
this.idleTimeoutSeconds = opts.idleTimeoutSeconds ?? 5 * 60;
|
|
6967
6969
|
this.idlePauseMs = opts.idlePauseMs ?? 10 * 6e4;
|
|
6968
6970
|
this.autoDeleteSeconds = opts.autoDeleteSeconds ?? 24 * 60 * 60;
|
|
6971
|
+
this.daemonBootTimeoutMs = opts.daemonBootTimeoutMs ?? DAEMON_BOOT_TIMEOUT_MS;
|
|
6969
6972
|
this.domainSuffix = opts.domainSuffix ?? "style.dev";
|
|
6970
6973
|
this.timeDaemonRequest = daemonProxyTimer(opts.meter);
|
|
6971
6974
|
if (this.idlePauseMs > 0) {
|
|
@@ -7300,7 +7303,12 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7300
7303
|
async resume(handle, found, opts) {
|
|
7301
7304
|
const { state, sourceSnapshotSlugAtCreate, metadata } = await found.vm.data();
|
|
7302
7305
|
if (state !== "running") await found.vm.start();
|
|
7303
|
-
await this.waitForDaemon(found.daemon.url)
|
|
7306
|
+
await this.waitForDaemon(found.daemon.url).catch(async (err) => {
|
|
7307
|
+
console.warn(`[${LOG_LABEL2}] ${handle}: ${errMsg2(err)}; rebooting`);
|
|
7308
|
+
await found.vm.exec({ command: "reboot -f", timeoutMs: 1e4 }).catch(() => {
|
|
7309
|
+
});
|
|
7310
|
+
await this.waitForDaemon(found.daemon.url);
|
|
7311
|
+
});
|
|
7304
7312
|
const sentinelSlug = metadata[BASE_KEY] ?? sourceSnapshotSlugAtCreate;
|
|
7305
7313
|
if (sentinelSlug) {
|
|
7306
7314
|
await this.reclaim(found, sentinelSlug, opts);
|
|
@@ -7539,26 +7547,63 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7539
7547
|
});
|
|
7540
7548
|
}
|
|
7541
7549
|
async waitForDaemon(url) {
|
|
7542
|
-
const deadline = Date.now() +
|
|
7550
|
+
const deadline = Date.now() + this.daemonBootTimeoutMs;
|
|
7543
7551
|
while (Date.now() < deadline) {
|
|
7544
7552
|
if (await daemonAnswers(url)) return;
|
|
7545
7553
|
await delay(HEALTH_POLL_MS);
|
|
7546
7554
|
}
|
|
7547
7555
|
throw new Error(
|
|
7548
|
-
`sandbox daemon at ${url} did not answer /health within ${
|
|
7556
|
+
`sandbox daemon at ${url} did not answer /health within ${this.daemonBootTimeoutMs / 1e3}s`
|
|
7549
7557
|
);
|
|
7550
7558
|
}
|
|
7559
|
+
/**
|
|
7560
|
+
* Deletes the VM only once its shutdown sync is confirmed. Otherwise the
|
|
7561
|
+
* user's unpushed work may still be on its disk, so it is detached instead:
|
|
7562
|
+
* the handle is free for a new sandbox, and the VM stays recoverable by id
|
|
7563
|
+
* until Freestyle's auto-delete.
|
|
7564
|
+
*/
|
|
7551
7565
|
async delete(handle) {
|
|
7552
7566
|
this.cancelRelease(handle);
|
|
7553
7567
|
const found = await this.find(handle, { refresh: true });
|
|
7554
7568
|
this.lookups.delete(handle);
|
|
7555
7569
|
if (!found) return;
|
|
7556
|
-
await found.vm
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
}
|
|
7570
|
+
if (await this.syncedOnShutdown(found.vm)) {
|
|
7571
|
+
await found.vm.delete();
|
|
7572
|
+
return;
|
|
7573
|
+
}
|
|
7574
|
+
const { rules } = await this.freestyle.tls.rules.list({
|
|
7575
|
+
vmId: found.vm.id
|
|
7560
7576
|
});
|
|
7561
|
-
await
|
|
7577
|
+
for (const rule of rules) await this.freestyle.tls.rules.delete(rule.id);
|
|
7578
|
+
await found.vm.update({ slug: "" });
|
|
7579
|
+
this.appIngress.delete(handle);
|
|
7580
|
+
console.warn(
|
|
7581
|
+
`[${LOG_LABEL2}] ${handle}: shutdown sync not confirmed; detached VM ${found.vm.id} instead of deleting it`
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
7584
|
+
/**
|
|
7585
|
+
* Stops the daemon, which publishes unsaved work to git on SIGTERM, then the
|
|
7586
|
+
* sidecar, which flushes its mounts. The daemon exits 0 even when that
|
|
7587
|
+
* publish fails, so its log decides.
|
|
7588
|
+
*/
|
|
7589
|
+
async syncedOnShutdown(vm) {
|
|
7590
|
+
try {
|
|
7591
|
+
const { state } = await vm.data();
|
|
7592
|
+
if (state !== "running") await vm.start();
|
|
7593
|
+
const res = await vm.exec({
|
|
7594
|
+
command: [
|
|
7595
|
+
"since=$(date +%s)",
|
|
7596
|
+
`docker stop -t 30 ${CONTAINER} >/dev/null`,
|
|
7597
|
+
`code=$(docker inspect -f '{{.State.ExitCode}}' ${CONTAINER})`,
|
|
7598
|
+
`docker stop -t 10 ${SIDECAR} >/dev/null`,
|
|
7599
|
+
`[ "$code" = 0 ] && ! docker logs --since "$since" ${CONTAINER} 2>&1 | grep -q "shutdown publish failed"`
|
|
7600
|
+
].join("; "),
|
|
7601
|
+
timeoutMs: 6e4
|
|
7602
|
+
});
|
|
7603
|
+
return res.statusCode === 0;
|
|
7604
|
+
} catch {
|
|
7605
|
+
return false;
|
|
7606
|
+
}
|
|
7562
7607
|
}
|
|
7563
7608
|
async lastTermination(_handle) {
|
|
7564
7609
|
return null;
|
|
@@ -7861,6 +7906,13 @@ var SandboxProviderRouter = class {
|
|
|
7861
7906
|
* reclaims a stray paused VM anyway, only later.
|
|
7862
7907
|
*/
|
|
7863
7908
|
strays = /* @__PURE__ */ new Map();
|
|
7909
|
+
/**
|
|
7910
|
+
* The latest ensure per handle. Placement reads what exists, so two at once
|
|
7911
|
+
* each found nothing and put one sandbox on each provider.
|
|
7912
|
+
*
|
|
7913
|
+
* ponytail: in-process; replicas would need the state store's lock.
|
|
7914
|
+
*/
|
|
7915
|
+
ensuring = /* @__PURE__ */ new Map();
|
|
7864
7916
|
freestyleShare;
|
|
7865
7917
|
get(kind) {
|
|
7866
7918
|
const provider = this.providers[kind];
|
|
@@ -7919,8 +7971,18 @@ var SandboxProviderRouter = class {
|
|
|
7919
7971
|
return roomy ? { kind: "kubernetes", reason: "split" } : { kind: "freestyle", reason: "capacity" };
|
|
7920
7972
|
}
|
|
7921
7973
|
async ensure(id2, opts = {}) {
|
|
7922
|
-
const { provider: _requested, ...rest } = opts;
|
|
7923
7974
|
const handle = computeHandle(id2);
|
|
7975
|
+
const run = (this.ensuring.get(handle) ?? Promise.resolve()).catch(() => {
|
|
7976
|
+
}).then(() => this.ensureOne(handle, id2, opts));
|
|
7977
|
+
this.ensuring.set(handle, run);
|
|
7978
|
+
try {
|
|
7979
|
+
return await run;
|
|
7980
|
+
} finally {
|
|
7981
|
+
if (this.ensuring.get(handle) === run) this.ensuring.delete(handle);
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
async ensureOne(handle, id2, opts) {
|
|
7985
|
+
const { provider: _requested, ...rest } = opts;
|
|
7924
7986
|
const placed = await this.place(handle, opts);
|
|
7925
7987
|
this.remember(handle, placed.kind);
|
|
7926
7988
|
if (this.strays.get(handle) === placed.kind) this.strays.delete(handle);
|