@moinulmoin/eden 0.9.0 → 0.10.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/README.md +3 -3
- package/dist/eve-control-plane.d.ts +28 -0
- package/dist/eve-control-plane.js +342 -33
- package/dist/eve-runtime-config.d.ts +1 -1
- package/dist/eve-runtime-config.js +7 -0
- package/dist/eve-runtime-image.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,15 +12,15 @@ If you already have an Eve project, start with `eden deploy`.
|
|
|
12
12
|
Use any one of these package managers:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
npm install --global @moinulmoin/eden@0.
|
|
15
|
+
npm install --global @moinulmoin/eden@0.10.0
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
pnpm add --global @moinulmoin/eden@0.
|
|
19
|
+
pnpm add --global @moinulmoin/eden@0.10.0
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
|
-
bun add --global @moinulmoin/eden@0.
|
|
23
|
+
bun add --global @moinulmoin/eden@0.10.0
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Then confirm the CLI is available:
|
|
@@ -128,6 +128,11 @@ export interface EveDeploymentIdentity {
|
|
|
128
128
|
readonly sourceDigest: string;
|
|
129
129
|
readonly generationId: string;
|
|
130
130
|
readonly deploymentId: string;
|
|
131
|
+
readonly releaseId?: string;
|
|
132
|
+
readonly eveVersion?: string;
|
|
133
|
+
readonly affinityContract?: number;
|
|
134
|
+
readonly retainedDeployments?: Readonly<Record<string, EveAffinityDeployment>>;
|
|
135
|
+
readonly pendingRetirements?: Readonly<Record<string, EveAffinityDeployment>>;
|
|
131
136
|
readonly environment: EveCliEnvironment;
|
|
132
137
|
readonly name: string;
|
|
133
138
|
readonly accountId: string;
|
|
@@ -144,6 +149,28 @@ export type EveDeploymentIdentityProof = Partial<EveDeploymentIdentity> & {
|
|
|
144
149
|
/** The deployment the serving Container verifiably started under; set only by a deployed Eden host that proves its running image before stamping. */
|
|
145
150
|
readonly startedDeploymentId?: string;
|
|
146
151
|
};
|
|
152
|
+
export interface EveAffinityDeployment {
|
|
153
|
+
readonly image: string;
|
|
154
|
+
readonly generationId: string;
|
|
155
|
+
readonly runtimeRevisionHandle?: string;
|
|
156
|
+
readonly eveVersion?: string;
|
|
157
|
+
readonly affinityContract?: number;
|
|
158
|
+
readonly releaseId?: string;
|
|
159
|
+
readonly worldRpcAuth?: boolean;
|
|
160
|
+
}
|
|
161
|
+
export interface EveDeploymentDrainState {
|
|
162
|
+
readonly liveRuns: number;
|
|
163
|
+
readonly liveSessions: number;
|
|
164
|
+
readonly pendingMessages: number;
|
|
165
|
+
readonly unfinishedTransfers: number;
|
|
166
|
+
readonly drained: boolean;
|
|
167
|
+
}
|
|
168
|
+
export type EveDeploymentRpcRunner = (request: {
|
|
169
|
+
readonly origin: string;
|
|
170
|
+
readonly operation: "deployments.drainState" | "deployments.retire";
|
|
171
|
+
readonly deploymentIds: readonly string[];
|
|
172
|
+
readonly controlToken: string;
|
|
173
|
+
}) => Promise<unknown>;
|
|
147
174
|
export interface EveDeploymentPublicationRequest {
|
|
148
175
|
readonly identity: EveDeploymentIdentity;
|
|
149
176
|
readonly hostConfig: EveHostConfig;
|
|
@@ -233,6 +260,7 @@ export interface EvePreflightOptions {
|
|
|
233
260
|
readonly protectedStore?: EveRuntimeProtectedStore;
|
|
234
261
|
readonly publish?: EveDeploymentPublicationRunner;
|
|
235
262
|
readonly publishImage?: EveImagePublicationRunner;
|
|
263
|
+
readonly deploymentRpc?: EveDeploymentRpcRunner;
|
|
236
264
|
readonly compensate?: EveDeploymentCompensationRunner;
|
|
237
265
|
readonly health?: EveDeploymentHealthRunner;
|
|
238
266
|
readonly afterPromotion?: () => void | Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as waitForActivation } from "node:timers/promises";
|
|
1
2
|
import { createRequire, } from "node:module";
|
|
2
3
|
import { createHash, randomUUID, } from "node:crypto";
|
|
3
4
|
import { join, dirname, relative, } from "node:path";
|
|
@@ -11,7 +12,7 @@ import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
|
|
|
11
12
|
import { buildEveProjectSnapshot, checkEveSandboxProvider, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
|
|
12
13
|
import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
|
|
13
14
|
import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
|
|
14
|
-
import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
|
|
15
|
+
import { EDEN_AFFINITY_CONTRACT, callDeploymentControlRpc, createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
const wranglerEntrypoint = require.resolve("wrangler");
|
|
@@ -32,6 +33,54 @@ export const DEFAULT_EVE_NODE_IMAGE = Object.freeze({
|
|
|
32
33
|
});
|
|
33
34
|
/** The single tag Eden pushes to the managed Cloudflare registry per generation. */
|
|
34
35
|
const EVE_REGISTRY_IMAGE_TAG = "candidate";
|
|
36
|
+
function validateDeploymentDrainStates(value, ids) {
|
|
37
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
38
|
+
throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", "The authenticated World did not return deployment drain states.");
|
|
39
|
+
}
|
|
40
|
+
const states = value;
|
|
41
|
+
for (const id of ids) {
|
|
42
|
+
const state = states[id];
|
|
43
|
+
if (typeof state !== "object" || state === null ||
|
|
44
|
+
typeof state.drained !== "boolean" ||
|
|
45
|
+
[state.liveRuns, state.liveSessions, state.pendingMessages, state.unfinishedTransfers]
|
|
46
|
+
.some((count) => !Number.isSafeInteger(count) || count < 0) ||
|
|
47
|
+
state.drained !== (state.liveRuns + state.liveSessions + state.pendingMessages + state.unfinishedTransfers === 0)) {
|
|
48
|
+
throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", `The authenticated World returned an invalid drain state for ${id}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return states;
|
|
52
|
+
}
|
|
53
|
+
const defaultDeploymentRpc = async (request) => {
|
|
54
|
+
// A successful Cloudflare secret write creates an active deployment, but
|
|
55
|
+
// existing stateless Worker isolates may briefly serve the preceding secret.
|
|
56
|
+
// Only the first read-only drain call following our own rotation may wait
|
|
57
|
+
// for that exact in-memory credential. All other failures remain terminal.
|
|
58
|
+
const activationRead = request.operation === "deployments.drainState";
|
|
59
|
+
const deadline = Date.now() + 60_000;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const remaining = deadline - Date.now();
|
|
62
|
+
if (remaining <= 0) {
|
|
63
|
+
throw deploymentFailure("EVE_AFFINITY_CONTROL_ACTIVATION_TIMEOUT", "The freshly rotated deployment-control credential did not activate within 60 seconds; publication was refused.");
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return await callDeploymentControlRpc({
|
|
67
|
+
origin: request.origin,
|
|
68
|
+
token: request.controlToken,
|
|
69
|
+
operation: request.operation,
|
|
70
|
+
args: [{ deploymentIds: request.deploymentIds }],
|
|
71
|
+
signal: AbortSignal.timeout(remaining),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!activationRead || typeof error !== "object" || error === null ||
|
|
76
|
+
!("status" in error) || error.status !== 401)
|
|
77
|
+
throw error;
|
|
78
|
+
const delay = Math.min(1_000, deadline - Date.now());
|
|
79
|
+
if (delay > 0)
|
|
80
|
+
await waitForActivation(delay);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
35
84
|
function safeText(value) {
|
|
36
85
|
return redactEveRuntimeOutput(value)
|
|
37
86
|
.replace(/(?:^|[\s"'=])(?:Bearer|Token)\s+\S+/giu, "$1[redacted]")
|
|
@@ -471,13 +520,13 @@ function safeDockerEnvironment() {
|
|
|
471
520
|
DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
472
521
|
};
|
|
473
522
|
}
|
|
474
|
-
async function readWranglerJson(args) {
|
|
523
|
+
async function readWranglerJson(args, timeoutMs = 30_000) {
|
|
475
524
|
try {
|
|
476
525
|
const result = await execFileAsync(process.execPath, [wranglerEntrypoint, ...args], {
|
|
477
526
|
env: safeWranglerEnvironment(),
|
|
478
527
|
cwd: wranglerCwd,
|
|
479
528
|
maxBuffer: 2 * 1024 * 1024,
|
|
480
|
-
timeout:
|
|
529
|
+
timeout: timeoutMs,
|
|
481
530
|
});
|
|
482
531
|
const parsed = JSON.parse(result.stdout.trim());
|
|
483
532
|
return { value: parsed, failed: false };
|
|
@@ -813,11 +862,33 @@ async function resolveEveTargetOwnership(request, result) {
|
|
|
813
862
|
evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
|
|
814
863
|
const remoteProven = evidence !== undefined && workerProven && containerProven;
|
|
815
864
|
if (!remoteProven) {
|
|
865
|
+
const failures = [];
|
|
866
|
+
if (evidence === undefined)
|
|
867
|
+
failures.push("provider evidence unavailable");
|
|
868
|
+
if (result.target.observed?.worker === true) {
|
|
869
|
+
if (evidence?.settingsReadable !== true)
|
|
870
|
+
failures.push("Worker settings unreadable");
|
|
871
|
+
else if (evidence.workerDeploymentId === undefined)
|
|
872
|
+
failures.push("Worker deployment stamp absent");
|
|
873
|
+
else if (!recordedDeploymentIds.has(evidence.workerDeploymentId))
|
|
874
|
+
failures.push("Worker deployment stamp differs from recorded identities");
|
|
875
|
+
}
|
|
876
|
+
if (result.target.observed?.container === true) {
|
|
877
|
+
if (evidence?.containerSchedulingPolicy !== "durable_object")
|
|
878
|
+
failures.push("Container scheduling policy unproven");
|
|
879
|
+
if (evidence?.containerNamespaceId === undefined)
|
|
880
|
+
failures.push("Container namespace unreadable");
|
|
881
|
+
if (evidence?.workerContainerNamespaceId === undefined)
|
|
882
|
+
failures.push("Worker Container binding namespace unreadable");
|
|
883
|
+
if (evidence?.containerNamespaceId !== undefined && evidence.workerContainerNamespaceId !== undefined &&
|
|
884
|
+
evidence.containerNamespaceId !== evidence.workerContainerNamespaceId)
|
|
885
|
+
failures.push("Container and Worker binding namespaces differ");
|
|
886
|
+
}
|
|
816
887
|
return {
|
|
817
888
|
...result,
|
|
818
889
|
target: {
|
|
819
890
|
...result.target,
|
|
820
|
-
message:
|
|
891
|
+
message: `The exact Worker or Container target exists, and although a local Eden record matches, provider state cannot prove it is the one Eden deployed — the target may have been recreated outside Eden. Unproven: ${failures.join("; ")}.`,
|
|
821
892
|
remediation: "Inspect the Worker at its workers.dev origin and the recorded deployment history before redeploying; if it is not Eden's, deploy under a different --name or delete it yourself.",
|
|
822
893
|
},
|
|
823
894
|
};
|
|
@@ -995,11 +1066,17 @@ function transportIdentityMatches(expected, observed) {
|
|
|
995
1066
|
"generationId",
|
|
996
1067
|
"stableWorkersDevOrigin",
|
|
997
1068
|
"workerName",
|
|
998
|
-
"stableContainerInstanceName",
|
|
999
1069
|
];
|
|
1000
1070
|
if (!required.every((key) => observed[key] === expected[key])) {
|
|
1001
1071
|
return false;
|
|
1002
1072
|
}
|
|
1073
|
+
const expectedInstance = expected.retainedDeployments === undefined ||
|
|
1074
|
+
expected.deploymentId === "eden-cloudflare"
|
|
1075
|
+
? expected.stableContainerInstanceName
|
|
1076
|
+
: `${expected.stableContainerInstanceName}:${expected.deploymentId}`;
|
|
1077
|
+
if (observed.stableContainerInstanceName !== expectedInstance ||
|
|
1078
|
+
observed.releaseId !== expected.releaseId)
|
|
1079
|
+
return false;
|
|
1003
1080
|
return expected.runtimeRevisionHandle === undefined
|
|
1004
1081
|
? observed.runtimeRevisionHandle === undefined
|
|
1005
1082
|
: observed.runtimeRevisionHandle === expected.runtimeRevisionHandle;
|
|
@@ -1012,6 +1089,7 @@ function identityMatches(expected, observed) {
|
|
|
1012
1089
|
"sourceDigest",
|
|
1013
1090
|
"generationId",
|
|
1014
1091
|
"deploymentId",
|
|
1092
|
+
"releaseId",
|
|
1015
1093
|
"environment",
|
|
1016
1094
|
"name",
|
|
1017
1095
|
"accountId",
|
|
@@ -1039,6 +1117,11 @@ function safeIdentity(identity) {
|
|
|
1039
1117
|
sourceDigest: identity.sourceDigest,
|
|
1040
1118
|
generationId: identity.generationId,
|
|
1041
1119
|
deploymentId: identity.deploymentId,
|
|
1120
|
+
releaseId: identity.releaseId,
|
|
1121
|
+
eveVersion: identity.eveVersion,
|
|
1122
|
+
affinityContract: identity.affinityContract,
|
|
1123
|
+
retainedDeployments: identity.retainedDeployments,
|
|
1124
|
+
pendingRetirements: identity.pendingRetirements,
|
|
1042
1125
|
environment: identity.environment,
|
|
1043
1126
|
name: identity.name,
|
|
1044
1127
|
accountId: identity.accountId,
|
|
@@ -1281,6 +1364,7 @@ function responseIdentity(response, body) {
|
|
|
1281
1364
|
read("sourceDigest", "x-eden-eve-source-digest");
|
|
1282
1365
|
read("generationId", "x-eden-eve-generation-id");
|
|
1283
1366
|
read("deploymentId", "x-eden-eve-deployment-id");
|
|
1367
|
+
read("releaseId", "x-eden-eve-release-id");
|
|
1284
1368
|
read("environment", "x-eden-eve-environment");
|
|
1285
1369
|
read("name", "x-eden-eve-name");
|
|
1286
1370
|
read("accountId", "x-eden-eve-account-id");
|
|
@@ -1341,7 +1425,7 @@ function defaultHealthRunner() {
|
|
|
1341
1425
|
// must wait until the *served container* verifiably runs this
|
|
1342
1426
|
// generation's image (the host stamps the header only after
|
|
1343
1427
|
// inspecting the running instance).
|
|
1344
|
-
if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
|
|
1428
|
+
if (observedIdentity.startedDeploymentId !== (request.identity.releaseId ?? request.identity.deploymentId)) {
|
|
1345
1429
|
lastReason =
|
|
1346
1430
|
"The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
|
|
1347
1431
|
await wait();
|
|
@@ -1460,8 +1544,8 @@ async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoo
|
|
|
1460
1544
|
}
|
|
1461
1545
|
return currentPath;
|
|
1462
1546
|
}
|
|
1463
|
-
async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}) {
|
|
1464
|
-
await writeFile(join(candidate.generationRoot,
|
|
1547
|
+
async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}, filename = "deployment.json") {
|
|
1548
|
+
await writeFile(join(candidate.generationRoot, filename), `${JSON.stringify({
|
|
1465
1549
|
version: 1,
|
|
1466
1550
|
status,
|
|
1467
1551
|
identity: safeIdentity(identity),
|
|
@@ -1474,6 +1558,10 @@ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status,
|
|
|
1474
1558
|
flag: "wx",
|
|
1475
1559
|
});
|
|
1476
1560
|
}
|
|
1561
|
+
async function deploymentRecordPath(generationRoot) {
|
|
1562
|
+
const path = join(generationRoot, "deployment.json");
|
|
1563
|
+
return await lstat(path).then(() => path, () => join(generationRoot, "deployment-intent.json"));
|
|
1564
|
+
}
|
|
1477
1565
|
async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
|
|
1478
1566
|
await writeFile(join(candidate.generationRoot, "deployment-attempt.json"), `${JSON.stringify({
|
|
1479
1567
|
version: 1,
|
|
@@ -1538,6 +1626,14 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1538
1626
|
}
|
|
1539
1627
|
const priorIdentity = prior?.identity;
|
|
1540
1628
|
const priorDurableObjects = prior?.durableObjects;
|
|
1629
|
+
const worldManifest = collected.workflowWorld === "@moinulmoin/eden-world-cloudflare"
|
|
1630
|
+
? await readInstalledManifest(candidate.snapshotRoot, collected.workflowWorld)
|
|
1631
|
+
: undefined;
|
|
1632
|
+
const affinityEnabled = worldManifest?.eden?.capabilities?.deploymentAffinity === true;
|
|
1633
|
+
const worldRpcAuth = worldManifest?.eden?.capabilities?.worldRpcAuth === true;
|
|
1634
|
+
if (!affinityEnabled && priorIdentity?.retainedDeployments !== undefined) {
|
|
1635
|
+
throw deploymentFailure("EVE_AFFINITY_REQUIRED", "This target retains affinity deployments; install a World package declaring deploymentAffinity before updating it.");
|
|
1636
|
+
}
|
|
1541
1637
|
const superseded = isUpdate
|
|
1542
1638
|
? {
|
|
1543
1639
|
generationId: priorIdentity?.generationId,
|
|
@@ -1553,8 +1649,12 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1553
1649
|
if (durableWorld) {
|
|
1554
1650
|
resultWarnings.push({
|
|
1555
1651
|
id: "EVE_UPDATE_IN_FLIGHT",
|
|
1556
|
-
message:
|
|
1557
|
-
|
|
1652
|
+
message: affinityEnabled
|
|
1653
|
+
? "This update retains pinned deployments until their sessions, queue messages, and transfers drain."
|
|
1654
|
+
: "This update replaces the running Container; an Eve version change can strand existing sessions because the installed World does not declare deployment affinity.",
|
|
1655
|
+
remediation: affinityEnabled
|
|
1656
|
+
? "End old sessions to allow their deployment images to retire."
|
|
1657
|
+
: "Let sessions settle before changing Eve versions, or install an affinity-capable World package.",
|
|
1558
1658
|
});
|
|
1559
1659
|
}
|
|
1560
1660
|
if (prior?.worldCloudflare !==
|
|
@@ -1595,6 +1695,80 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1595
1695
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
|
|
1596
1696
|
}
|
|
1597
1697
|
const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
|
|
1698
|
+
const releaseId = `eve-deploy-${randomUUID()}`;
|
|
1699
|
+
let previousEveVersion = priorIdentity?.eveVersion;
|
|
1700
|
+
if (previousEveVersion === undefined && prior !== undefined) {
|
|
1701
|
+
previousEveVersion = (await readInstalledManifest(join(prior.generationRoot, "container/snapshot"), "eve"))?.version;
|
|
1702
|
+
}
|
|
1703
|
+
const retainedDeployments = {
|
|
1704
|
+
...priorIdentity?.retainedDeployments,
|
|
1705
|
+
};
|
|
1706
|
+
if (affinityEnabled && priorIdentity?.retainedDeployments === undefined &&
|
|
1707
|
+
priorIdentity?.containerImage !== undefined && priorIdentity.generationId !== undefined) {
|
|
1708
|
+
retainedDeployments["eden-cloudflare"] = {
|
|
1709
|
+
image: priorIdentity.containerImage,
|
|
1710
|
+
generationId: priorIdentity.generationId,
|
|
1711
|
+
...(previousEveVersion === undefined ? {} : { eveVersion: previousEveVersion }),
|
|
1712
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1713
|
+
...(priorIdentity.runtimeRevisionHandle === undefined ? {} : { runtimeRevisionHandle: priorIdentity.runtimeRevisionHandle }),
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
const matchingDeployment = Object.entries(retainedDeployments).find(([, entry]) => entry.eveVersion === candidate.eveVersion && entry.affinityContract === EDEN_AFFINITY_CONTRACT)?.[0];
|
|
1717
|
+
const deploymentId = !affinityEnabled ? releaseId : matchingDeployment ??
|
|
1718
|
+
`eden-affinity-${createHash("sha256").update(`${candidate.eveVersion}:${EDEN_AFFINITY_CONTRACT}`).digest("hex").slice(0, 24)}`;
|
|
1719
|
+
const retiringDeployments = { ...priorIdentity?.pendingRetirements };
|
|
1720
|
+
delete retiringDeployments[deploymentId];
|
|
1721
|
+
if (priorIdentity?.deploymentId !== undefined)
|
|
1722
|
+
delete retiringDeployments[priorIdentity.deploymentId];
|
|
1723
|
+
for (const id of prior?.protectedDeploymentIds ?? [])
|
|
1724
|
+
delete retiringDeployments[id];
|
|
1725
|
+
const hasRetainedNoncurrentDeployments = priorIdentity?.retainedDeployments !== undefined &&
|
|
1726
|
+
Object.keys(retainedDeployments).some((id) => id !== priorIdentity.deploymentId);
|
|
1727
|
+
const controlToken = `${randomUUID()}${randomUUID()}`;
|
|
1728
|
+
if (affinityEnabled && (hasRetainedNoncurrentDeployments || Object.keys(retiringDeployments).length > 0)) {
|
|
1729
|
+
await (options.protectedStore ?? defaultProtectedStore(request.name)).put({
|
|
1730
|
+
targetId: targetKey, revision: `eve-affinity-control-${randomUUID()}`,
|
|
1731
|
+
inputIdentity: {
|
|
1732
|
+
token: `eve-affinity-control-input-${randomUUID()}`,
|
|
1733
|
+
byteLength: Buffer.byteLength(controlToken),
|
|
1734
|
+
digest: createHash("sha256").update(controlToken).digest("hex"),
|
|
1735
|
+
},
|
|
1736
|
+
variableNames: ["EDEN_EVE_CONTROL_TOKEN"], values: { EDEN_EVE_CONTROL_TOKEN: controlToken },
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
if (affinityEnabled && hasRetainedNoncurrentDeployments) {
|
|
1740
|
+
const ids = Object.keys(retainedDeployments);
|
|
1741
|
+
const raw = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
1742
|
+
origin: stableWorkersDevOrigin, operation: "deployments.drainState", deploymentIds: ids,
|
|
1743
|
+
controlToken,
|
|
1744
|
+
});
|
|
1745
|
+
const states = validateDeploymentDrainStates(raw, ids);
|
|
1746
|
+
const eligible = ids.filter((id) => id !== deploymentId && id !== priorIdentity.deploymentId &&
|
|
1747
|
+
!prior?.protectedDeploymentIds?.includes(id) && states[id]?.drained === true);
|
|
1748
|
+
const remainingCount = ids.length - eligible.length + (deploymentId in retainedDeployments ? 0 : 1);
|
|
1749
|
+
if (remainingCount <= 10 && eligible.length > 0) {
|
|
1750
|
+
// Drain snapshots are not fences. Retire only noncurrent targets, and
|
|
1751
|
+
// obtain the World's durable admission fence before dropping routes.
|
|
1752
|
+
const retired = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
1753
|
+
origin: stableWorkersDevOrigin, operation: "deployments.retire",
|
|
1754
|
+
deploymentIds: eligible, controlToken,
|
|
1755
|
+
});
|
|
1756
|
+
if (typeof retired !== "object" || retired === null || !("retired" in retired) ||
|
|
1757
|
+
!Array.isArray(retired.retired) ||
|
|
1758
|
+
eligible.some((id) => !retired.retired.includes(id))) {
|
|
1759
|
+
throw deploymentFailure("EVE_AFFINITY_RETIRE_UNPROVEN", "The World did not prove the noncurrent deployments durably fenced for retirement.");
|
|
1760
|
+
}
|
|
1761
|
+
for (const id of eligible) {
|
|
1762
|
+
retiringDeployments[id] = retainedDeployments[id];
|
|
1763
|
+
delete retainedDeployments[id];
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
if (Object.keys(retainedDeployments).length + (deploymentId in retainedDeployments ? 0 : 1) > 10) {
|
|
1767
|
+
const blockers = Object.entries(states).filter(([id]) => id in retainedDeployments)
|
|
1768
|
+
.map(([id, state]) => `${id}: ${state.liveSessions} live sessions, ${state.liveRuns} live runs, ${state.pendingMessages} pending messages, ${state.unfinishedTransfers} unfinished transfers`);
|
|
1769
|
+
throw deploymentFailure("EVE_AFFINITY_LIMIT", `Deployment refused before image/code publication: at most 10 affinity deployments may be retained. Blocking deployments: ${blockers.join("; ")}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1598
1772
|
let imageReference = options.containerImageReference;
|
|
1599
1773
|
if (imageReference !== undefined) {
|
|
1600
1774
|
if (imageDigestFromReference(imageReference) !== imageDigest) {
|
|
@@ -1607,7 +1781,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1607
1781
|
if (localImageReference === undefined || localImageReference.length === 0) {
|
|
1608
1782
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
|
|
1609
1783
|
}
|
|
1610
|
-
const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
|
|
1784
|
+
const imageRepository = `eden-eve-${targetKey}-${affinityEnabled ? deploymentId : candidate.generationId}`;
|
|
1611
1785
|
const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
1612
1786
|
const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
|
|
1613
1787
|
const imagePublication = await (options.publishImage ??
|
|
@@ -1669,7 +1843,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1669
1843
|
throw deploymentFailure("DEPLOY_INDETERMINATE", reason);
|
|
1670
1844
|
}
|
|
1671
1845
|
}
|
|
1672
|
-
const deploymentId = `eve-deploy-${randomUUID()}`;
|
|
1673
1846
|
const containerApplicationName = boundedResourceName(request.name, "container");
|
|
1674
1847
|
const stableContainerInstanceName = boundedResourceName(request.name, "instance");
|
|
1675
1848
|
const runtimeVariableNames = candidate.runtimeVariableNames;
|
|
@@ -1678,6 +1851,24 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1678
1851
|
sourceDigest: candidate.sourceDigest,
|
|
1679
1852
|
generationId: candidate.generationId,
|
|
1680
1853
|
deploymentId,
|
|
1854
|
+
releaseId,
|
|
1855
|
+
eveVersion: candidate.eveVersion,
|
|
1856
|
+
...(affinityEnabled ? {
|
|
1857
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1858
|
+
retainedDeployments: {
|
|
1859
|
+
...retainedDeployments,
|
|
1860
|
+
[deploymentId]: {
|
|
1861
|
+
image: imageReference,
|
|
1862
|
+
generationId: candidate.generationId,
|
|
1863
|
+
eveVersion: candidate.eveVersion,
|
|
1864
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1865
|
+
releaseId,
|
|
1866
|
+
worldRpcAuth,
|
|
1867
|
+
...(runtimeInjection?.protectedHandle === undefined ? {} : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
|
|
1868
|
+
},
|
|
1869
|
+
},
|
|
1870
|
+
pendingRetirements: retiringDeployments,
|
|
1871
|
+
} : {}),
|
|
1681
1872
|
environment: request.environment,
|
|
1682
1873
|
name: request.name,
|
|
1683
1874
|
accountId,
|
|
@@ -1698,6 +1889,11 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1698
1889
|
containerApplicationName: identity.containerApplicationName,
|
|
1699
1890
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1700
1891
|
deploymentId: identity.deploymentId,
|
|
1892
|
+
worldRpcAuth,
|
|
1893
|
+
...(identity.releaseId === undefined ? {} : { releaseId: identity.releaseId }),
|
|
1894
|
+
...(identity.eveVersion === undefined ? {} : { eveVersion: identity.eveVersion }),
|
|
1895
|
+
...(identity.affinityContract === undefined ? {} : { affinityContract: identity.affinityContract }),
|
|
1896
|
+
...(identity.retainedDeployments === undefined ? {} : { retainedDeployments: identity.retainedDeployments }),
|
|
1701
1897
|
generationId: identity.generationId,
|
|
1702
1898
|
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1703
1899
|
containerImage: identity.containerImage,
|
|
@@ -1753,6 +1949,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1753
1949
|
throw error;
|
|
1754
1950
|
}
|
|
1755
1951
|
};
|
|
1952
|
+
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-intent" }, "deployment-intent.json");
|
|
1756
1953
|
try {
|
|
1757
1954
|
eveProgress(options, "publishing Worker and Container application");
|
|
1758
1955
|
publication = await publishOne(publicationRequest);
|
|
@@ -1955,7 +2152,40 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1955
2152
|
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
|
|
1956
2153
|
await options.afterPromotion?.();
|
|
1957
2154
|
let supersededImageCleanup;
|
|
1958
|
-
if (
|
|
2155
|
+
if (affinityEnabled && Object.keys(retiringDeployments).length > 0) {
|
|
2156
|
+
const ids = Object.keys(retiringDeployments);
|
|
2157
|
+
const completedRetirements = [];
|
|
2158
|
+
try {
|
|
2159
|
+
const result = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
2160
|
+
origin: stableWorkersDevOrigin, operation: "deployments.retire", deploymentIds: ids,
|
|
2161
|
+
controlToken,
|
|
2162
|
+
});
|
|
2163
|
+
if (typeof result !== "object" || result === null ||
|
|
2164
|
+
!("retired" in result) || !Array.isArray(result.retired) ||
|
|
2165
|
+
ids.some((id) => !result.retired.includes(id)))
|
|
2166
|
+
throw new Error("Retirement was not proven.");
|
|
2167
|
+
for (const [id, retained] of Object.entries(retiringDeployments)) {
|
|
2168
|
+
const repository = ownedRegistryRepository({
|
|
2169
|
+
...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
|
|
2170
|
+
}, request.environment, request.name, accountId);
|
|
2171
|
+
if (repository !== undefined && await (options.deleteRegistryRepository ??
|
|
2172
|
+
defaultRegistryRepositoryDelete)({ repository }) === "indeterminate") {
|
|
2173
|
+
throw new Error(`Repository deletion was not proven: ${repository}`);
|
|
2174
|
+
}
|
|
2175
|
+
completedRetirements.push(id);
|
|
2176
|
+
}
|
|
2177
|
+
supersededImageCleanup = check("VAL-LIFE-007", "passed", "Drained deployment instances and their owned registry repositories were retired.");
|
|
2178
|
+
}
|
|
2179
|
+
catch {
|
|
2180
|
+
supersededImageCleanup = check("VAL-LIFE-007", "failed", "The deployment is healthy, but drained resource retirement was not proven; exact pending retirement records were retained for retry.", "Retry deploy to finish only recorded drained resource cleanup.");
|
|
2181
|
+
}
|
|
2182
|
+
if (completedRetirements.length > 0) {
|
|
2183
|
+
// Keep the immutable deployment record as evidence, but exclude proven
|
|
2184
|
+
// cleanup from the next deploy's active retry set.
|
|
2185
|
+
await writeFile(join(candidate.generationRoot, "completed-retirements.json"), `${JSON.stringify(completedRetirements)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (isUpdate && !affinityEnabled) {
|
|
1959
2189
|
// The superseded generation's registry repository is generation-scoped
|
|
1960
2190
|
// and no longer referenced once CURRENT promotes this generation; only
|
|
1961
2191
|
// then is it safe to delete. Every tag in the repository goes with it —
|
|
@@ -2512,7 +2742,7 @@ async function readEveDestroyRecord(projectRoot, environment, name) {
|
|
|
2512
2742
|
if (generationRoot === undefined) {
|
|
2513
2743
|
return readEveDestroyRecordFromGenerations(projectRoot, environment, name);
|
|
2514
2744
|
}
|
|
2515
|
-
const parsed = await parseEveDestroyRecord(
|
|
2745
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2516
2746
|
return parsed === undefined
|
|
2517
2747
|
? undefined
|
|
2518
2748
|
: { ...parsed, generationRoot };
|
|
@@ -2526,7 +2756,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
|
|
|
2526
2756
|
const details = await lstat(generationRoot).catch(() => undefined);
|
|
2527
2757
|
if (details === undefined || !details.isDirectory())
|
|
2528
2758
|
continue;
|
|
2529
|
-
const recordDetails = await lstat(
|
|
2759
|
+
const recordDetails = await lstat(await deploymentRecordPath(generationRoot)).catch(() => undefined);
|
|
2530
2760
|
if (recordDetails !== undefined && recordDetails.isFile()) {
|
|
2531
2761
|
withRecords.push({
|
|
2532
2762
|
generationRoot,
|
|
@@ -2536,7 +2766,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
|
|
|
2536
2766
|
}
|
|
2537
2767
|
withRecords.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
2538
2768
|
for (const { generationRoot } of withRecords) {
|
|
2539
|
-
const parsed = await parseEveDestroyRecord(
|
|
2769
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2540
2770
|
if (parsed !== undefined) {
|
|
2541
2771
|
return { ...parsed, generationRoot };
|
|
2542
2772
|
}
|
|
@@ -2581,7 +2811,35 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
2581
2811
|
if (identity.environment !== environment || identity.name !== name) {
|
|
2582
2812
|
return undefined;
|
|
2583
2813
|
}
|
|
2814
|
+
const metadata = {};
|
|
2815
|
+
for (const key of ["deploymentId", "eveVersion", "releaseId", "runtimeRevisionHandle"]) {
|
|
2816
|
+
if (typeof identity[key] === "string")
|
|
2817
|
+
Object.assign(metadata, { [key]: identity[key] });
|
|
2818
|
+
}
|
|
2819
|
+
if (typeof identity.affinityContract === "number")
|
|
2820
|
+
Object.assign(metadata, { affinityContract: identity.affinityContract });
|
|
2821
|
+
for (const key of ["retainedDeployments", "pendingRetirements"]) {
|
|
2822
|
+
const value = identity[key];
|
|
2823
|
+
if (value === undefined)
|
|
2824
|
+
continue;
|
|
2825
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2826
|
+
return undefined;
|
|
2827
|
+
for (const [id, deployment] of Object.entries(value)) {
|
|
2828
|
+
if (!/^(?:eden-cloudflare|eden-affinity-[a-f0-9]{24})$/u.test(id) ||
|
|
2829
|
+
typeof deployment !== "object" || deployment === null ||
|
|
2830
|
+
typeof deployment.image !== "string" || imageDigestFromReference(deployment.image) === undefined ||
|
|
2831
|
+
typeof deployment.generationId !== "string" ||
|
|
2832
|
+
(deployment.eveVersion !== undefined && typeof deployment.eveVersion !== "string") ||
|
|
2833
|
+
(deployment.affinityContract !== undefined && typeof deployment.affinityContract !== "number") ||
|
|
2834
|
+
(deployment.releaseId !== undefined && typeof deployment.releaseId !== "string") ||
|
|
2835
|
+
(deployment.worldRpcAuth !== undefined && typeof deployment.worldRpcAuth !== "boolean") ||
|
|
2836
|
+
(deployment.runtimeRevisionHandle !== undefined && typeof deployment.runtimeRevisionHandle !== "string"))
|
|
2837
|
+
return undefined;
|
|
2838
|
+
}
|
|
2839
|
+
Object.assign(metadata, { [key]: value });
|
|
2840
|
+
}
|
|
2584
2841
|
const verified = {
|
|
2842
|
+
...metadata,
|
|
2585
2843
|
workerName: identity.workerName,
|
|
2586
2844
|
containerApplicationName: identity.containerApplicationName,
|
|
2587
2845
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
@@ -2620,7 +2878,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2620
2878
|
const deploymentIds = new Set();
|
|
2621
2879
|
for (const entry of entries) {
|
|
2622
2880
|
const generationRoot = join(generationsRoot, entry);
|
|
2623
|
-
const recordPath =
|
|
2881
|
+
const recordPath = await deploymentRecordPath(generationRoot);
|
|
2624
2882
|
const details = await lstat(recordPath).catch(() => undefined);
|
|
2625
2883
|
if (details === undefined || !details.isFile())
|
|
2626
2884
|
continue;
|
|
@@ -2653,7 +2911,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2653
2911
|
live = pinned;
|
|
2654
2912
|
}
|
|
2655
2913
|
else {
|
|
2656
|
-
const parsed = await parseEveDeploymentRecord(
|
|
2914
|
+
const parsed = await parseEveDeploymentRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2657
2915
|
if (parsed !== undefined &&
|
|
2658
2916
|
!((accountId !== undefined &&
|
|
2659
2917
|
parsed.identity.accountId !== accountId) ||
|
|
@@ -2669,10 +2927,31 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2669
2927
|
// Newest record carrying a usable DO history wins, regardless of its
|
|
2670
2928
|
// deployment outcome: a tag it published must never be replayed.
|
|
2671
2929
|
const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
|
|
2930
|
+
// CURRENT is only a health/promotion pointer. A newer uncertain publication
|
|
2931
|
+
// may already serve sessions; preserve every such image until a later deploy
|
|
2932
|
+
// can prove it drained, never infer absence from a failed health check.
|
|
2933
|
+
const unresolved = scanned.filter((entry) => entry.modifiedAt > live.modifiedAt);
|
|
2934
|
+
const retainedDeployments = { ...live.record.identity.retainedDeployments };
|
|
2935
|
+
const pendingRetirements = { ...live.record.identity.pendingRetirements };
|
|
2936
|
+
const protectedDeploymentIds = new Set();
|
|
2937
|
+
if (live.record.identity.retainedDeployments === undefined) {
|
|
2938
|
+
protectedDeploymentIds.add("eden-cloudflare");
|
|
2939
|
+
}
|
|
2940
|
+
for (const entry of unresolved.slice().reverse()) {
|
|
2941
|
+
Object.assign(retainedDeployments, entry.record.identity.retainedDeployments);
|
|
2942
|
+
Object.assign(pendingRetirements, entry.record.identity.pendingRetirements);
|
|
2943
|
+
if (entry.record.deploymentId !== undefined)
|
|
2944
|
+
protectedDeploymentIds.add(entry.record.deploymentId);
|
|
2945
|
+
}
|
|
2672
2946
|
return {
|
|
2673
2947
|
recordPath: live.record.recordPath,
|
|
2674
2948
|
generationRoot: live.generationRoot,
|
|
2675
|
-
identity:
|
|
2949
|
+
identity: {
|
|
2950
|
+
...live.record.identity,
|
|
2951
|
+
...(Object.keys(retainedDeployments).length === 0 ? {} : { retainedDeployments }),
|
|
2952
|
+
...(Object.keys(pendingRetirements).length === 0 ? {} : { pendingRetirements }),
|
|
2953
|
+
},
|
|
2954
|
+
protectedDeploymentIds: [...protectedDeploymentIds],
|
|
2676
2955
|
status: live.record.status,
|
|
2677
2956
|
deploymentIds: [...deploymentIds],
|
|
2678
2957
|
worldCloudflare: live.record.worldCloudflare,
|
|
@@ -2683,6 +2962,19 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2683
2962
|
const base = await parseEveDestroyRecord(recordPath, environment, name);
|
|
2684
2963
|
if (base === undefined)
|
|
2685
2964
|
return undefined;
|
|
2965
|
+
let identity = base.identity;
|
|
2966
|
+
try {
|
|
2967
|
+
const completed = JSON.parse(await readFile(join(dirname(recordPath), "completed-retirements.json"), "utf8"));
|
|
2968
|
+
if (Array.isArray(completed) && completed.every((id) => typeof id === "string")) {
|
|
2969
|
+
const pendingRetirements = { ...identity.pendingRetirements };
|
|
2970
|
+
for (const id of completed)
|
|
2971
|
+
delete pendingRetirements[id];
|
|
2972
|
+
identity = { ...identity, pendingRetirements };
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
catch {
|
|
2976
|
+
// Missing or unreadable cleanup evidence must not suppress a retry.
|
|
2977
|
+
}
|
|
2686
2978
|
let parsed;
|
|
2687
2979
|
try {
|
|
2688
2980
|
parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
@@ -2726,7 +3018,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2726
3018
|
Array.isArray(rawMigrations);
|
|
2727
3019
|
const invalidHistory = () => ({
|
|
2728
3020
|
recordPath,
|
|
2729
|
-
identity
|
|
3021
|
+
identity,
|
|
2730
3022
|
status,
|
|
2731
3023
|
deploymentId,
|
|
2732
3024
|
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
@@ -2753,7 +3045,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2753
3045
|
}
|
|
2754
3046
|
return {
|
|
2755
3047
|
recordPath,
|
|
2756
|
-
identity
|
|
3048
|
+
identity,
|
|
2757
3049
|
status,
|
|
2758
3050
|
deploymentId,
|
|
2759
3051
|
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
@@ -2775,7 +3067,7 @@ async function defaultDestroyTargetRead(request) {
|
|
|
2775
3067
|
"images",
|
|
2776
3068
|
"list",
|
|
2777
3069
|
"--json",
|
|
2778
|
-
]);
|
|
3070
|
+
], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
2779
3071
|
const tagsByRepository = registryTagsByRepository(images.value);
|
|
2780
3072
|
if (!images.failed && tagsByRepository !== undefined) {
|
|
2781
3073
|
const present = [];
|
|
@@ -2854,12 +3146,13 @@ async function defaultContainerDelete(request) {
|
|
|
2854
3146
|
* repository is deleted individually.
|
|
2855
3147
|
*/
|
|
2856
3148
|
async function defaultRegistryRepositoryDelete(request) {
|
|
3149
|
+
// Catalog reads include Cloudflare snapshot tags and can exceed 30 seconds.
|
|
2857
3150
|
const images = await readWranglerJson([
|
|
2858
3151
|
"containers",
|
|
2859
3152
|
"images",
|
|
2860
3153
|
"list",
|
|
2861
3154
|
"--json",
|
|
2862
|
-
]);
|
|
3155
|
+
], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
2863
3156
|
const collection = jsonCollection(images.value);
|
|
2864
3157
|
if (images.failed || collection === undefined)
|
|
2865
3158
|
return "indeterminate";
|
|
@@ -2885,9 +3178,11 @@ async function defaultRegistryRepositoryDelete(request) {
|
|
|
2885
3178
|
deletedAny = true;
|
|
2886
3179
|
continue;
|
|
2887
3180
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3181
|
+
}
|
|
3182
|
+
const after = await readWranglerJson(["containers", "images", "list", "--json"], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
3183
|
+
const remaining = after.failed ? undefined : registryTagsByRepository(after.value);
|
|
3184
|
+
if (remaining === undefined || (remaining.get(request.repository)?.length ?? 0) > 0) {
|
|
3185
|
+
return "indeterminate";
|
|
2891
3186
|
}
|
|
2892
3187
|
return deletedAny ? "deleted" : "absent";
|
|
2893
3188
|
}
|
|
@@ -2905,13 +3200,13 @@ function ownedRegistryRepository(identity, environment, name, accountId) {
|
|
|
2905
3200
|
identity.accountId !== accountId) {
|
|
2906
3201
|
return undefined;
|
|
2907
3202
|
}
|
|
2908
|
-
const
|
|
2909
|
-
const
|
|
2910
|
-
if (
|
|
2911
|
-
|
|
3203
|
+
const prefix = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-`;
|
|
3204
|
+
const suffixes = [identity.generationId];
|
|
3205
|
+
if (identity.retainedDeployments !== undefined && identity.deploymentId !== undefined)
|
|
3206
|
+
suffixes.push(identity.deploymentId);
|
|
3207
|
+
if (imageDigestFromReference(identity.containerImage) === undefined)
|
|
2912
3208
|
return undefined;
|
|
2913
|
-
}
|
|
2914
|
-
return repository;
|
|
3209
|
+
return suffixes.map((suffix) => `${prefix}${suffix}`).find((repository) => identity.containerImage.startsWith(`registry.cloudflare.com/${identity.accountId}/${repository}@`));
|
|
2915
3210
|
}
|
|
2916
3211
|
/**
|
|
2917
3212
|
* The registry repository an indeterminate deploy attempt provably pushed.
|
|
@@ -2964,17 +3259,31 @@ async function collectEveDestroyRegistryRepositories(projectRoot, environment, n
|
|
|
2964
3259
|
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2965
3260
|
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2966
3261
|
const repositories = new Set();
|
|
3262
|
+
const addRetained = (identity) => {
|
|
3263
|
+
for (const [id, retained] of Object.entries({
|
|
3264
|
+
...identity.retainedDeployments, ...identity.pendingRetirements,
|
|
3265
|
+
})) {
|
|
3266
|
+
const repository = ownedRegistryRepository({
|
|
3267
|
+
...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
|
|
3268
|
+
}, environment, name, record.accountId);
|
|
3269
|
+
if (repository !== undefined)
|
|
3270
|
+
repositories.add(repository);
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
addRetained(record);
|
|
2967
3274
|
const targetKey = record.projectId === undefined
|
|
2968
3275
|
? undefined
|
|
2969
3276
|
: stableDeploymentKey(record.projectId, record.accountId, environment, name);
|
|
2970
3277
|
for (const entry of entries) {
|
|
2971
3278
|
const generationRoot = join(generationsRoot, entry);
|
|
2972
|
-
const parsed = await parseEveDestroyRecord(
|
|
3279
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2973
3280
|
const deployedRepository = parsed === undefined
|
|
2974
3281
|
? undefined
|
|
2975
3282
|
: ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
|
|
2976
3283
|
if (deployedRepository !== undefined)
|
|
2977
3284
|
repositories.add(deployedRepository);
|
|
3285
|
+
if (parsed !== undefined)
|
|
3286
|
+
addRetained(parsed.identity);
|
|
2978
3287
|
if (targetKey === undefined)
|
|
2979
3288
|
continue;
|
|
2980
3289
|
const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID"];
|
|
1
|
+
export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_WORLD_TOKEN", "EDEN_EVE_WORLD_RPC_AUTH", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID", "EDEN_EVE_RELEASE_ID", "EDEN_EVE_DEPLOYMENTS", "EDEN_AFFINITY_CONTRACT", "EVE_SCHEDULES_DISABLED", "EDEN_EVE_CONTROL_TOKEN"];
|
|
2
2
|
export type EveReservedHostVariable = (typeof EVE_RESERVED_HOST_VARIABLES)[number];
|
|
3
3
|
export declare const EVE_START_COMMAND: readonly ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"];
|
|
4
4
|
export declare const EVE_HOST_ENVIRONMENT: Readonly<{
|
|
@@ -10,8 +10,15 @@ export const EVE_RESERVED_HOST_VARIABLES = Object.freeze([
|
|
|
10
10
|
"NODE_EXTRA_CA_CERTS",
|
|
11
11
|
"WORKFLOW_LOCAL_BASE_URL",
|
|
12
12
|
"EDEN_WORLD_URL",
|
|
13
|
+
"EDEN_WORLD_TOKEN",
|
|
14
|
+
"EDEN_EVE_WORLD_RPC_AUTH",
|
|
13
15
|
"EDEN_EVE_DEPLOYMENT_ID",
|
|
14
16
|
"EDEN_EVE_GENERATION_ID",
|
|
17
|
+
"EDEN_EVE_RELEASE_ID",
|
|
18
|
+
"EDEN_EVE_DEPLOYMENTS",
|
|
19
|
+
"EDEN_AFFINITY_CONTRACT",
|
|
20
|
+
"EVE_SCHEDULES_DISABLED",
|
|
21
|
+
"EDEN_EVE_CONTROL_TOKEN",
|
|
15
22
|
]);
|
|
16
23
|
export const EVE_START_COMMAND = Object.freeze([
|
|
17
24
|
"./node_modules/.bin/eve",
|
|
@@ -458,6 +458,7 @@ COPY node_modules /candidate/node_modules
|
|
|
458
458
|
${sandboxContextCopy}${sourceEntries.map((entry) => `COPY ${JSON.stringify([entry, `/candidate/${entry}`])}`).join("\n")}
|
|
459
459
|
|
|
460
460
|
FROM --platform=linux/amd64 ${imageReference(nodeImage)} AS runtime
|
|
461
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*
|
|
461
462
|
WORKDIR /workspace
|
|
462
463
|
ENV HOST=0.0.0.0 \\
|
|
463
464
|
NITRO_HOST=0.0.0.0 \\
|
|
@@ -582,6 +583,8 @@ function runtimeContainerEnvironment(request) {
|
|
|
582
583
|
...(request.workflowWorld === "@moinulmoin/eden-world-cloudflare"
|
|
583
584
|
? {
|
|
584
585
|
EDEN_WORLD_URL: `${request.publicOrigin ?? "http://localhost:8080"}/__eden/world/rpc`,
|
|
586
|
+
EDEN_EVE_DEPLOYMENT_ID: `eden-smoke-${request.candidate.generationId}`,
|
|
587
|
+
EDEN_EVE_RELEASE_ID: `eden-smoke-${request.candidate.generationId}`,
|
|
585
588
|
CBOR_NATIVE_ACCELERATION_DISABLED: "true",
|
|
586
589
|
}
|
|
587
590
|
: {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moinulmoin/eden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Deploy existing Eve projects to your own Cloudflare account.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"NOTICE"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@moinulmoin/eden-runtime-cloudflare": "0.
|
|
34
|
+
"@moinulmoin/eden-runtime-cloudflare": "0.10.0",
|
|
35
35
|
"wrangler": "4.147.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|