@decocms/sandbox-controller 0.13.1 → 0.15.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 +16 -0
- package/dist/index.js +110 -18
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ interface Workload {
|
|
|
81
81
|
devPort?: number;
|
|
82
82
|
/** Subdirectory inside the repo where the package manager manifest lives (e.g. `apps/web`). */
|
|
83
83
|
packageManagerPath?: string;
|
|
84
|
+
/** Other monorepo apps to serve beside it, by path. Absent keeps what the sandbox has. */
|
|
85
|
+
apps?: string[];
|
|
84
86
|
}
|
|
85
87
|
/**
|
|
86
88
|
* What a sandbox is for. AgentSandboxProvider may size, pool, and place the two
|
|
@@ -1054,6 +1056,11 @@ declare class AgentSandboxProvider {
|
|
|
1054
1056
|
* mounts are additive, a relay failure must not fail provisioning/recovery.
|
|
1055
1057
|
*/
|
|
1056
1058
|
private relayOrgFsConfig;
|
|
1059
|
+
/**
|
|
1060
|
+
* Bring a resumed/adopted sandbox's extra apps and their preview hosts in
|
|
1061
|
+
* line with the caller's. Best-effort, like the credential refresh.
|
|
1062
|
+
*/
|
|
1063
|
+
private syncExtraApps;
|
|
1057
1064
|
/**
|
|
1058
1065
|
* Forward a freshly-minted clone credential to a resumed/adopted sandbox's
|
|
1059
1066
|
* daemon so authenticated git survives the ~1h expiry of the token baked in
|
|
@@ -3766,6 +3773,8 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3766
3773
|
private sweeping;
|
|
3767
3774
|
/** Org + repo → the newest repo snapshot known to exist, and the head it was built for. */
|
|
3768
3775
|
private readonly repoSnapshots;
|
|
3776
|
+
/** Handle → the app domains its VM was last given, so a resume skips the API. */
|
|
3777
|
+
private readonly appIngress;
|
|
3769
3778
|
/** Org + repo with a build running, so a push mid-build waits for it. */
|
|
3770
3779
|
private readonly repoSnapshotBuilds;
|
|
3771
3780
|
/** When a repo's last build failed; a build takes up to 30 min, so not on every ensure. */
|
|
@@ -3824,6 +3833,13 @@ declare class FreestyleSandboxProvider implements SandboxProvider {
|
|
|
3824
3833
|
private run;
|
|
3825
3834
|
ensure(id: SandboxId, opts?: EnsureOptions): Promise<Sandbox>;
|
|
3826
3835
|
private resume;
|
|
3836
|
+
/** Public hosts of a sandbox's extra apps; the daemon routes them by Host. */
|
|
3837
|
+
private appDomains;
|
|
3838
|
+
/**
|
|
3839
|
+
* Gives each extra app of a running VM its ingress, and takes it from apps
|
|
3840
|
+
* no longer configured.
|
|
3841
|
+
*/
|
|
3842
|
+
private syncAppIngress;
|
|
3827
3843
|
/**
|
|
3828
3844
|
* The bearer this VM was claimed with lives in daemon memory. A cold boot
|
|
3829
3845
|
* (Freestyle restarts a guest it finds wedged) brings the daemon back on the
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,14 @@ import "./chunk-QGM4M3NI.js";
|
|
|
3
3
|
// index.ts
|
|
4
4
|
import { KubeConfig } from "@kubernetes/client-node";
|
|
5
5
|
|
|
6
|
+
// ../shared/src/app-preview-host.ts
|
|
7
|
+
function appSlug(path) {
|
|
8
|
+
return path.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
|
+
}
|
|
10
|
+
function appHostLabel(handle, path) {
|
|
11
|
+
return `${handle}--${appSlug(path)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
// ../sandbox/server/provider/agent-sandbox/runner.ts
|
|
7
15
|
import { createHash as createHash2, randomBytes, randomUUID } from "crypto";
|
|
8
16
|
import * as net from "net";
|
|
@@ -957,7 +965,8 @@ function buildConfigPayload(args) {
|
|
|
957
965
|
const application = packageManager ? {
|
|
958
966
|
packageManager,
|
|
959
967
|
runtime: args.runtime,
|
|
960
|
-
...args.port !== void 0 ? { port: args.port } : {}
|
|
968
|
+
...args.port !== void 0 ? { port: args.port } : {},
|
|
969
|
+
...args.apps ? { apps: args.apps.map((path) => ({ path })) } : {}
|
|
961
970
|
} : void 0;
|
|
962
971
|
const orgId = tenant?.orgId;
|
|
963
972
|
if (!git && !application && !operator && args.cloneOnly === void 0 && !orgId) {
|
|
@@ -979,6 +988,9 @@ function buildConfigPayload(args) {
|
|
|
979
988
|
...application ? { application } : {}
|
|
980
989
|
};
|
|
981
990
|
}
|
|
991
|
+
function extraAppsPatch(apps) {
|
|
992
|
+
return { application: { apps: apps.map((path) => ({ path })) } };
|
|
993
|
+
}
|
|
982
994
|
function deriveRepoLabel(cloneUrl) {
|
|
983
995
|
try {
|
|
984
996
|
const u = new URL(cloneUrl);
|
|
@@ -2477,6 +2489,7 @@ var AgentSandboxProvider = class {
|
|
|
2477
2489
|
const rec = await this.rehydrate(id2, handle, persisted);
|
|
2478
2490
|
if (rec) {
|
|
2479
2491
|
await this.refreshDaemonGitCredential(rec, opts);
|
|
2492
|
+
await this.syncExtraApps(rec, opts);
|
|
2480
2493
|
await this.relayOrgFsConfig(
|
|
2481
2494
|
rec.daemonUrl,
|
|
2482
2495
|
rec.token,
|
|
@@ -2553,6 +2566,7 @@ var AgentSandboxProvider = class {
|
|
|
2553
2566
|
});
|
|
2554
2567
|
if (adopted) {
|
|
2555
2568
|
await this.refreshDaemonGitCredential(adopted, opts);
|
|
2569
|
+
await this.syncExtraApps(adopted, opts);
|
|
2556
2570
|
await this.relayOrgFsConfig(
|
|
2557
2571
|
adopted.daemonUrl,
|
|
2558
2572
|
adopted.token,
|
|
@@ -2794,7 +2808,8 @@ var AgentSandboxProvider = class {
|
|
|
2794
2808
|
await this.ensureHttpRouteForHandle(
|
|
2795
2809
|
handle,
|
|
2796
2810
|
adoptedSandboxName,
|
|
2797
|
-
opts.tenant ?? null
|
|
2811
|
+
opts.tenant ?? null,
|
|
2812
|
+
opts.workload?.apps
|
|
2798
2813
|
);
|
|
2799
2814
|
} catch (err) {
|
|
2800
2815
|
await deleteSandboxClaim(this.kubeConfig, this.namespace, handle).catch(
|
|
@@ -2875,6 +2890,7 @@ var AgentSandboxProvider = class {
|
|
|
2875
2890
|
repo: opts?.repo ?? null,
|
|
2876
2891
|
extraRepos: opts?.extraRepos ?? [],
|
|
2877
2892
|
port: opts?.workload?.devPort ?? DEFAULT_DEV_PORT,
|
|
2893
|
+
apps: opts?.workload?.apps,
|
|
2878
2894
|
tenant: opts?.tenant ?? void 0,
|
|
2879
2895
|
// Sent on every ensure that carries opts, so a warm-pool pod inheriting a
|
|
2880
2896
|
// previous claim's clone-only config gets it cleared on a normal one.
|
|
@@ -2896,6 +2912,27 @@ var AgentSandboxProvider = class {
|
|
|
2896
2912
|
(err) => console.warn("[org-fs] sidecar config relay failed", err)
|
|
2897
2913
|
);
|
|
2898
2914
|
}
|
|
2915
|
+
/**
|
|
2916
|
+
* Bring a resumed/adopted sandbox's extra apps and their preview hosts in
|
|
2917
|
+
* line with the caller's. Best-effort, like the credential refresh.
|
|
2918
|
+
*/
|
|
2919
|
+
async syncExtraApps(rec, opts) {
|
|
2920
|
+
const apps = opts.workload?.apps;
|
|
2921
|
+
if (!apps) return;
|
|
2922
|
+
try {
|
|
2923
|
+
await this.ensureHttpRouteForHandle(
|
|
2924
|
+
rec.handle,
|
|
2925
|
+
rec.adoptedSandboxName,
|
|
2926
|
+
rec.tenant,
|
|
2927
|
+
apps
|
|
2928
|
+
);
|
|
2929
|
+
await postConfig(rec.daemonUrl, rec.token, extraAppsPatch(apps));
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
console.warn(
|
|
2932
|
+
`[${LOG_LABEL}] extra apps sync failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2899
2936
|
/**
|
|
2900
2937
|
* Forward a freshly-minted clone credential to a resumed/adopted sandbox's
|
|
2901
2938
|
* daemon so authenticated git survives the ~1h expiry of the token baked in
|
|
@@ -3353,14 +3390,18 @@ var AgentSandboxProvider = class {
|
|
|
3353
3390
|
* `ensureServicePortForAdoptedSandbox` does — we want traffic to land on
|
|
3354
3391
|
* the actually-bound pool pod, not the operator's cold-path orphan.
|
|
3355
3392
|
*/
|
|
3356
|
-
async ensureHttpRouteForHandle(handle, adoptedSandboxName, tenant) {
|
|
3393
|
+
async ensureHttpRouteForHandle(handle, adoptedSandboxName, tenant, apps = []) {
|
|
3357
3394
|
if (!this.previewGateway || !this.previewUrlPattern) return;
|
|
3358
|
-
const
|
|
3395
|
+
const pattern = this.previewUrlPattern;
|
|
3396
|
+
const hostname = previewHostnameForHandle(pattern, handle);
|
|
3359
3397
|
if (!hostname) {
|
|
3360
3398
|
throw new SandboxError(
|
|
3361
|
-
`Unable to derive preview hostname for ${handle} from pattern: ${
|
|
3399
|
+
`Unable to derive preview hostname for ${handle} from pattern: ${pattern}`
|
|
3362
3400
|
);
|
|
3363
3401
|
}
|
|
3402
|
+
const appHostnames = apps.flatMap(
|
|
3403
|
+
(path) => previewHostnameForHandle(pattern, appHostLabel(handle, path)) ?? []
|
|
3404
|
+
);
|
|
3364
3405
|
const route = {
|
|
3365
3406
|
apiVersion: `${HTTPROUTE_CONSTANTS.API_GROUP}/${HTTPROUTE_CONSTANTS.API_VERSION}`,
|
|
3366
3407
|
kind: "HTTPRoute",
|
|
@@ -3384,7 +3425,7 @@ var AgentSandboxProvider = class {
|
|
|
3384
3425
|
namespace: this.previewGateway.namespace
|
|
3385
3426
|
}
|
|
3386
3427
|
],
|
|
3387
|
-
hostnames: [hostname],
|
|
3428
|
+
hostnames: [hostname, ...appHostnames],
|
|
3388
3429
|
rules: [
|
|
3389
3430
|
{
|
|
3390
3431
|
backendRefs: [
|
|
@@ -6763,7 +6804,7 @@ var Freestyle = class {
|
|
|
6763
6804
|
// ../sandbox/package.json
|
|
6764
6805
|
var package_default = {
|
|
6765
6806
|
name: "@decocms/sandbox",
|
|
6766
|
-
version: "1.
|
|
6807
|
+
version: "1.95.0",
|
|
6767
6808
|
private: true,
|
|
6768
6809
|
type: "module",
|
|
6769
6810
|
description: "Sandbox runtime for isolated Studio tool execution",
|
|
@@ -6897,6 +6938,8 @@ var FreestyleSandboxProvider = class {
|
|
|
6897
6938
|
sweeping = false;
|
|
6898
6939
|
/** Org + repo → the newest repo snapshot known to exist, and the head it was built for. */
|
|
6899
6940
|
repoSnapshots = /* @__PURE__ */ new Map();
|
|
6941
|
+
/** Handle → the app domains its VM was last given, so a resume skips the API. */
|
|
6942
|
+
appIngress = /* @__PURE__ */ new Map();
|
|
6900
6943
|
/** Org + repo with a build running, so a push mid-build waits for it. */
|
|
6901
6944
|
repoSnapshotBuilds = /* @__PURE__ */ new Set();
|
|
6902
6945
|
/** When a repo's last build failed; a build takes up to 30 min, so not on every ensure. */
|
|
@@ -7241,7 +7284,7 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7241
7284
|
const existing = await this.find(handle, { refresh: true });
|
|
7242
7285
|
let warmPoolAdopted = false;
|
|
7243
7286
|
if (existing) {
|
|
7244
|
-
await this.resume(existing, opts);
|
|
7287
|
+
await this.resume(handle, existing, opts);
|
|
7245
7288
|
} else {
|
|
7246
7289
|
warmPoolAdopted = await this.create(handle, opts);
|
|
7247
7290
|
}
|
|
@@ -7254,7 +7297,7 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7254
7297
|
};
|
|
7255
7298
|
});
|
|
7256
7299
|
}
|
|
7257
|
-
async resume(found, opts) {
|
|
7300
|
+
async resume(handle, found, opts) {
|
|
7258
7301
|
const { state, sourceSnapshotSlugAtCreate, metadata } = await found.vm.data();
|
|
7259
7302
|
if (state !== "running") await found.vm.start();
|
|
7260
7303
|
await this.waitForDaemon(found.daemon.url);
|
|
@@ -7277,6 +7320,52 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7277
7320
|
)
|
|
7278
7321
|
);
|
|
7279
7322
|
}
|
|
7323
|
+
const apps = opts.workload?.apps;
|
|
7324
|
+
if (apps) {
|
|
7325
|
+
await this.syncAppIngress(handle, found.vm, apps).catch(
|
|
7326
|
+
(err) => console.warn(`[${LOG_LABEL2}] app ingress sync failed: ${errMsg2(err)}`)
|
|
7327
|
+
);
|
|
7328
|
+
await postConfig(
|
|
7329
|
+
found.daemon.url,
|
|
7330
|
+
found.daemon.token,
|
|
7331
|
+
extraAppsPatch(apps)
|
|
7332
|
+
).catch(
|
|
7333
|
+
(err) => console.warn(`[${LOG_LABEL2}] apps update failed: ${errMsg2(err)}`)
|
|
7334
|
+
);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
/** Public hosts of a sandbox's extra apps; the daemon routes them by Host. */
|
|
7338
|
+
appDomains(handle, apps) {
|
|
7339
|
+
return apps.map(
|
|
7340
|
+
(path) => `${appHostLabel(handle, path)}.${this.domainSuffix}`
|
|
7341
|
+
);
|
|
7342
|
+
}
|
|
7343
|
+
/**
|
|
7344
|
+
* Gives each extra app of a running VM its ingress, and takes it from apps
|
|
7345
|
+
* no longer configured.
|
|
7346
|
+
*/
|
|
7347
|
+
async syncAppIngress(handle, vm, apps) {
|
|
7348
|
+
const wanted = new Set(this.appDomains(handle, apps));
|
|
7349
|
+
const key = [...wanted].sort().join(",");
|
|
7350
|
+
if (this.appIngress.get(handle) === key) return;
|
|
7351
|
+
const appPrefix = `${handle}--`;
|
|
7352
|
+
const { rules } = await this.freestyle.tls.rules.list({ vmId: vm.id });
|
|
7353
|
+
const have = /* @__PURE__ */ new Set();
|
|
7354
|
+
for (const rule of rules) {
|
|
7355
|
+
if (!rule.domain.startsWith(appPrefix)) continue;
|
|
7356
|
+
if (wanted.has(rule.domain)) have.add(rule.domain);
|
|
7357
|
+
else await this.freestyle.tls.rules.delete(rule.id);
|
|
7358
|
+
}
|
|
7359
|
+
for (const domain of wanted) {
|
|
7360
|
+
if (have.has(domain)) continue;
|
|
7361
|
+
await this.freestyle.tls.rules.create({
|
|
7362
|
+
action: "allow",
|
|
7363
|
+
domain,
|
|
7364
|
+
source: { public: true },
|
|
7365
|
+
destination: { vmId: vm.id, port: DAEMON_PORT }
|
|
7366
|
+
});
|
|
7367
|
+
}
|
|
7368
|
+
this.appIngress.set(handle, key);
|
|
7280
7369
|
}
|
|
7281
7370
|
/**
|
|
7282
7371
|
* The bearer this VM was claimed with lives in daemon memory. A cold boot
|
|
@@ -7346,13 +7435,14 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7346
7435
|
},
|
|
7347
7436
|
tls: {
|
|
7348
7437
|
rules: [
|
|
7349
|
-
{
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
}
|
|
7355
|
-
|
|
7438
|
+
`${handle}.${this.domainSuffix}`,
|
|
7439
|
+
...this.appDomains(handle, opts.workload?.apps ?? [])
|
|
7440
|
+
].map((domain) => ({
|
|
7441
|
+
action: "allow",
|
|
7442
|
+
domain,
|
|
7443
|
+
source: { public: true },
|
|
7444
|
+
destination: { port: DAEMON_PORT }
|
|
7445
|
+
}))
|
|
7356
7446
|
}
|
|
7357
7447
|
});
|
|
7358
7448
|
step("vm-fork");
|
|
@@ -7444,7 +7534,8 @@ ${head}`).digest("hex").slice(0, 16)}`;
|
|
|
7444
7534
|
extraRepos: opts.extraRepos ?? [],
|
|
7445
7535
|
port: opts.workload?.devPort ?? DEFAULT_DEV_PORT2,
|
|
7446
7536
|
tenant: opts.tenant,
|
|
7447
|
-
cloneOnly: opts.cloneOnly === true
|
|
7537
|
+
cloneOnly: opts.cloneOnly === true,
|
|
7538
|
+
apps: opts.workload?.apps
|
|
7448
7539
|
});
|
|
7449
7540
|
}
|
|
7450
7541
|
async waitForDaemon(url) {
|
|
@@ -8005,7 +8096,8 @@ var ensureOptionsSchema = z4.object({
|
|
|
8005
8096
|
runtime: z4.enum(["node", "bun", "deno"]),
|
|
8006
8097
|
packageManager: z4.enum(["npm", "pnpm", "yarn", "bun", "deno"]),
|
|
8007
8098
|
devPort: z4.number().int().optional(),
|
|
8008
|
-
packageManagerPath: z4.string().optional()
|
|
8099
|
+
packageManagerPath: z4.string().optional(),
|
|
8100
|
+
apps: z4.array(z4.string().min(1).max(256)).max(8).optional()
|
|
8009
8101
|
}).optional(),
|
|
8010
8102
|
cloneOnly: z4.boolean().optional(),
|
|
8011
8103
|
repoSnapshot: z4.boolean().optional(),
|