@moinulmoin/eden 0.5.0 → 0.7.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/NOTICE +0 -9
- package/README.md +14 -10
- package/dist/eve-control-plane.d.ts +21 -18
- package/dist/eve-control-plane.js +271 -219
- package/dist/eve-packaging.d.ts +79 -12
- package/dist/eve-packaging.js +342 -60
- package/dist/eve-runtime-config.d.ts +7 -2
- package/dist/eve-runtime-config.js +7 -2
- package/dist/eve-runtime-image.d.ts +12 -9
- package/dist/eve-runtime-image.js +140 -62
- package/dist/eve.d.ts +1 -2
- package/dist/eve.js +1 -18
- package/dist/index.d.ts +5 -8
- package/dist/index.js +2 -10
- package/package.json +3 -3
|
@@ -7,10 +7,11 @@ import { createServer, } from "node:net";
|
|
|
7
7
|
import { execFile, spawn, } from "node:child_process";
|
|
8
8
|
import { fileURLToPath, } from "node:url";
|
|
9
9
|
import { promisify, } from "node:util";
|
|
10
|
-
import { EveCliError, } from "./eve.js";
|
|
11
|
-
import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, } from "./eve-packaging.js";
|
|
10
|
+
import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
|
|
11
|
+
import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
|
|
12
12
|
import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
|
|
13
13
|
import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
|
|
14
|
+
import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
|
|
14
15
|
const execFileAsync = promisify(execFile);
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
const wranglerEntrypoint = require.resolve("wrangler");
|
|
@@ -63,6 +64,8 @@ function mapPackagingCheck(value) {
|
|
|
63
64
|
return "VAL-SEC-003";
|
|
64
65
|
case "DOCKER_PLATFORM_BLOCKED":
|
|
65
66
|
return "VAL-BUILD-005";
|
|
67
|
+
case "EVE_SANDBOX_RESUME_PATCH":
|
|
68
|
+
return value.id;
|
|
66
69
|
default:
|
|
67
70
|
return `EVE-${value.id}`;
|
|
68
71
|
}
|
|
@@ -440,21 +443,11 @@ function exactContainerEntries(value, name) {
|
|
|
440
443
|
typeof entry.name !== "string") {
|
|
441
444
|
return undefined;
|
|
442
445
|
}
|
|
443
|
-
const id = entry.id;
|
|
444
446
|
const entryName = entry.name;
|
|
445
447
|
if (entryName === name) {
|
|
446
|
-
const configuration = entry.configuration;
|
|
447
|
-
const image = typeof entry.image === "string"
|
|
448
|
-
? entry.image
|
|
449
|
-
: typeof configuration === "object" &&
|
|
450
|
-
configuration !== null &&
|
|
451
|
-
typeof configuration.image === "string"
|
|
452
|
-
? configuration.image
|
|
453
|
-
: undefined;
|
|
454
448
|
entries.push({
|
|
455
|
-
id,
|
|
449
|
+
id: entry.id,
|
|
456
450
|
name: entryName,
|
|
457
|
-
...(image === undefined ? {} : { image }),
|
|
458
451
|
});
|
|
459
452
|
}
|
|
460
453
|
}
|
|
@@ -530,7 +523,7 @@ async function readWorkersDevSubdomain(accountId) {
|
|
|
530
523
|
? result.subdomain
|
|
531
524
|
: undefined;
|
|
532
525
|
return typeof subdomain === "string" &&
|
|
533
|
-
|
|
526
|
+
EVE_NAME_PATTERN.test(subdomain)
|
|
534
527
|
? subdomain
|
|
535
528
|
: undefined;
|
|
536
529
|
}
|
|
@@ -546,34 +539,34 @@ async function readWorkersDevSubdomain(accountId) {
|
|
|
546
539
|
export function exactTargetContainerEntries(value, name) {
|
|
547
540
|
return exactContainerEntries(value, boundedResourceName(name, "container"));
|
|
548
541
|
}
|
|
549
|
-
async function
|
|
550
|
-
const account = await readWranglerJson(["whoami", "--json"]);
|
|
542
|
+
async function readExactTargetInventory(workerName, containerApp) {
|
|
551
543
|
const containers = await readWranglerJson([
|
|
552
|
-
"containers",
|
|
553
|
-
"list",
|
|
554
|
-
"--json",
|
|
555
|
-
"--per-page",
|
|
556
|
-
"100",
|
|
544
|
+
"containers", "list", "--json", "--per-page", "100",
|
|
557
545
|
]);
|
|
558
546
|
const deployments = await readWranglerJson([
|
|
559
|
-
"deployments",
|
|
560
|
-
"list",
|
|
561
|
-
"--name",
|
|
562
|
-
request.name,
|
|
563
|
-
"--json",
|
|
547
|
+
"deployments", "list", "--name", workerName, "--json",
|
|
564
548
|
]);
|
|
549
|
+
return {
|
|
550
|
+
containers,
|
|
551
|
+
deployments,
|
|
552
|
+
containerEntries: exactContainerEntries(containers.value, containerApp),
|
|
553
|
+
deploymentEntries: exactDeploymentEntries(deployments.value),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async function defaultCloudflareRead(request) {
|
|
557
|
+
const account = await readWranglerJson(["whoami", "--json"]);
|
|
558
|
+
const { containers, deployments, containerEntries, deploymentEntries: listedDeployments } = await readExactTargetInventory(request.name, boundedResourceName(request.name, "container"));
|
|
565
559
|
const accountId = authenticatedAccountId(account.value);
|
|
566
560
|
const accountAccess = account.failed || accountId === undefined
|
|
567
561
|
? "unavailable"
|
|
568
562
|
: "available";
|
|
569
|
-
const containerEntries = exactTargetContainerEntries(containers.value, request.name);
|
|
570
563
|
const containerAccess = containers.failed || containerEntries === undefined
|
|
571
564
|
? "unavailable"
|
|
572
565
|
: "available";
|
|
573
566
|
const deploymentEntries = deployments.failed &&
|
|
574
567
|
/(?:does not exist|not found)/iu.test(deployments.stderr ?? "")
|
|
575
568
|
? []
|
|
576
|
-
:
|
|
569
|
+
: listedDeployments;
|
|
577
570
|
const workersDevSubdomain = await readWorkersDevSubdomain(accountId);
|
|
578
571
|
if (accountAccess === "unavailable" ||
|
|
579
572
|
containerAccess === "unavailable" ||
|
|
@@ -610,7 +603,7 @@ async function defaultCloudflareRead(request) {
|
|
|
610
603
|
}
|
|
611
604
|
const workerPresent = deploymentEntries.length > 0;
|
|
612
605
|
const containerPresent = (containerEntries?.length ?? 0) > 0;
|
|
613
|
-
const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0]
|
|
606
|
+
const providerEvidence = await readProviderEvidence(request.name, accountId, workerPresent, containerPresent ? containerEntries?.[0] : undefined);
|
|
614
607
|
return {
|
|
615
608
|
accountAccess,
|
|
616
609
|
containerAccess,
|
|
@@ -634,16 +627,40 @@ async function defaultCloudflareRead(request) {
|
|
|
634
627
|
}
|
|
635
628
|
/**
|
|
636
629
|
* Reads provider-backed facts about the live target: the Container
|
|
637
|
-
* application's
|
|
638
|
-
* `
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
630
|
+
* application's identity from `wrangler containers list` plus `containers
|
|
631
|
+
* info` (scheduling policy and Durable Object namespace), and the
|
|
632
|
+
* `EDEN_EVE_DEPLOYMENT_ID` plain-text var plus the `EVE_CONTAINER` Durable
|
|
633
|
+
* Object namespace Eden stamps into every Worker it publishes (from the
|
|
634
|
+
* script settings API). All are provider state — a foreign Worker can forge
|
|
635
|
+
* `x-eden-*` response headers but cannot set vars or namespaces Eden
|
|
636
|
+
* recorded at publish time. The ownership proof is the Container
|
|
637
|
+
* application's Durable Object namespace matching the Worker's binding
|
|
638
|
+
* namespace.
|
|
642
639
|
*/
|
|
643
|
-
async function readProviderEvidence(workerName, accountId, workerPresent,
|
|
640
|
+
async function readProviderEvidence(workerName, accountId, workerPresent, containerEntry) {
|
|
644
641
|
const evidence = {
|
|
645
|
-
...(
|
|
642
|
+
...(containerEntry === undefined
|
|
643
|
+
? {}
|
|
644
|
+
: { containerApplicationId: containerEntry.id }),
|
|
646
645
|
};
|
|
646
|
+
if (containerEntry !== undefined) {
|
|
647
|
+
const info = await readWranglerJson([
|
|
648
|
+
"containers",
|
|
649
|
+
"info",
|
|
650
|
+
containerEntry.id,
|
|
651
|
+
"--json",
|
|
652
|
+
]);
|
|
653
|
+
if (!info.failed && typeof info.value === "object" && info.value !== null) {
|
|
654
|
+
const infoRecord = info.value;
|
|
655
|
+
if (typeof infoRecord.scheduling_policy === "string") {
|
|
656
|
+
evidence.containerSchedulingPolicy = infoRecord.scheduling_policy;
|
|
657
|
+
}
|
|
658
|
+
const namespaceId = infoRecord.durable_objects?.namespace_id;
|
|
659
|
+
if (typeof namespaceId === "string") {
|
|
660
|
+
evidence.containerNamespaceId = namespaceId;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
647
664
|
if (workerPresent && accountId !== undefined) {
|
|
648
665
|
const token = process.env.CLOUDFLARE_API_TOKEN ??
|
|
649
666
|
(await readWranglerOAuthToken());
|
|
@@ -656,10 +673,16 @@ async function readProviderEvidence(workerName, accountId, workerPresent, contai
|
|
|
656
673
|
if (response.ok) {
|
|
657
674
|
evidence.settingsReadable = true;
|
|
658
675
|
const parsed = (await response.json());
|
|
659
|
-
const
|
|
676
|
+
const bindings = parsed.result?.bindings ?? [];
|
|
677
|
+
const deploymentBinding = bindings.find((value) => value.type === "plain_text" &&
|
|
660
678
|
value.name === "EDEN_EVE_DEPLOYMENT_ID");
|
|
661
|
-
if (typeof
|
|
662
|
-
evidence.workerDeploymentId =
|
|
679
|
+
if (typeof deploymentBinding?.text === "string") {
|
|
680
|
+
evidence.workerDeploymentId = deploymentBinding.text;
|
|
681
|
+
}
|
|
682
|
+
const containerBinding = bindings.find((value) => value.type === "durable_object_namespace" &&
|
|
683
|
+
value.name === "EVE_CONTAINER");
|
|
684
|
+
if (typeof containerBinding?.namespace_id === "string") {
|
|
685
|
+
evidence.workerContainerNamespaceId = containerBinding.namespace_id;
|
|
663
686
|
}
|
|
664
687
|
}
|
|
665
688
|
else if (response.status !== 404) {
|
|
@@ -698,19 +721,23 @@ async function resolveEveTargetOwnership(request, result) {
|
|
|
698
721
|
}
|
|
699
722
|
// Remote proof must come from provider state. A foreign Worker can echo
|
|
700
723
|
// `x-eden-*` response headers, but it cannot plant the plain-text var Eden
|
|
701
|
-
// publishes into script settings
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
// unreadable settings fail closed.
|
|
724
|
+
// publishes into script settings or claim the Durable Object namespace the
|
|
725
|
+
// Worker's EVE_CONTAINER binding resolves to. The Worker is proven only by
|
|
726
|
+
// its own settings: unreadable settings fail closed.
|
|
705
727
|
const evidence = result.target.providerEvidence;
|
|
706
728
|
const recordedDeploymentIds = new Set(record.deploymentIds);
|
|
707
729
|
const workerProven = result.target.observed?.worker !== true ||
|
|
708
730
|
(evidence?.settingsReadable === true &&
|
|
709
731
|
evidence.workerDeploymentId !== undefined &&
|
|
710
732
|
recordedDeploymentIds.has(evidence.workerDeploymentId));
|
|
733
|
+
// The Container proof is the application running the durable_object
|
|
734
|
+
// scheduling policy with a namespace that matches the Worker binding's
|
|
735
|
+
// namespace; anything else fails closed.
|
|
711
736
|
const containerProven = result.target.observed?.container !== true ||
|
|
712
|
-
(evidence?.
|
|
713
|
-
|
|
737
|
+
(evidence?.containerSchedulingPolicy === "durable_object" &&
|
|
738
|
+
evidence.containerNamespaceId !== undefined &&
|
|
739
|
+
evidence.workerContainerNamespaceId !== undefined &&
|
|
740
|
+
evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
|
|
714
741
|
const remoteProven = evidence !== undefined && workerProven && containerProven;
|
|
715
742
|
if (!remoteProven) {
|
|
716
743
|
return {
|
|
@@ -763,7 +790,6 @@ async function resolvedCloudflareOrigin(result, workerName) {
|
|
|
763
790
|
return undefined;
|
|
764
791
|
}
|
|
765
792
|
try {
|
|
766
|
-
const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
767
793
|
const origin = resolveStableWorkersDevOrigin({
|
|
768
794
|
workerName,
|
|
769
795
|
workersDevSubdomain: result.workersDevSubdomain,
|
|
@@ -1130,8 +1156,6 @@ function defaultPublicationRunner() {
|
|
|
1130
1156
|
"--env",
|
|
1131
1157
|
request.identity.environment,
|
|
1132
1158
|
"--strict",
|
|
1133
|
-
"--containers-rollout",
|
|
1134
|
-
"immediate",
|
|
1135
1159
|
], dirname(request.wranglerConfigPath));
|
|
1136
1160
|
if (result.exitCode !== 0) {
|
|
1137
1161
|
return {
|
|
@@ -1140,22 +1164,7 @@ function defaultPublicationRunner() {
|
|
|
1140
1164
|
ownershipEvidenceRetained: true,
|
|
1141
1165
|
};
|
|
1142
1166
|
}
|
|
1143
|
-
const containers = await
|
|
1144
|
-
"containers",
|
|
1145
|
-
"list",
|
|
1146
|
-
"--json",
|
|
1147
|
-
"--per-page",
|
|
1148
|
-
"100",
|
|
1149
|
-
]);
|
|
1150
|
-
const containerEntries = exactContainerEntries(containers.value, request.hostConfig.container.applicationName);
|
|
1151
|
-
const deployments = await readWranglerJson([
|
|
1152
|
-
"deployments",
|
|
1153
|
-
"list",
|
|
1154
|
-
"--name",
|
|
1155
|
-
request.identity.workerName,
|
|
1156
|
-
"--json",
|
|
1157
|
-
]);
|
|
1158
|
-
const deploymentEntries = exactDeploymentEntries(deployments.value);
|
|
1167
|
+
const { containers, deployments, containerEntries, deploymentEntries } = await readExactTargetInventory(request.identity.workerName, request.hostConfig.container.applicationName);
|
|
1159
1168
|
if (containers.failed ||
|
|
1160
1169
|
containerEntries === undefined ||
|
|
1161
1170
|
containerEntries.length !== 1 ||
|
|
@@ -1490,7 +1499,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1490
1499
|
}
|
|
1491
1500
|
let stableWorkersDevOrigin;
|
|
1492
1501
|
try {
|
|
1493
|
-
const { resolveStableWorkersDevOrigin } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1494
1502
|
stableWorkersDevOrigin = resolveStableWorkersDevOrigin({
|
|
1495
1503
|
workerName: request.name,
|
|
1496
1504
|
workersDevSubdomain,
|
|
@@ -1506,7 +1514,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1506
1514
|
throw deploymentFailure("EVE_ORIGIN_UNAVAILABLE", "The authenticated stable workers.dev origin did not match the exact Worker target.");
|
|
1507
1515
|
}
|
|
1508
1516
|
const imageDigest = runtimeEvidence.imageDigest;
|
|
1509
|
-
if (imageDigest === undefined ||
|
|
1517
|
+
if (imageDigest === undefined || !IMAGE_ID_PATTERN.test(imageDigest)) {
|
|
1510
1518
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
|
|
1511
1519
|
}
|
|
1512
1520
|
const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
|
|
@@ -1607,38 +1615,37 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1607
1615
|
? {}
|
|
1608
1616
|
: { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
|
|
1609
1617
|
};
|
|
1618
|
+
const baseConfigRequest = {
|
|
1619
|
+
accountId,
|
|
1620
|
+
workerName: identity.workerName,
|
|
1621
|
+
containerApplicationName: identity.containerApplicationName,
|
|
1622
|
+
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1623
|
+
deploymentId: identity.deploymentId,
|
|
1624
|
+
generationId: identity.generationId,
|
|
1625
|
+
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1626
|
+
containerImage: identity.containerImage,
|
|
1627
|
+
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1628
|
+
...(identity.runtimeRevisionHandle === undefined
|
|
1629
|
+
? {}
|
|
1630
|
+
: { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
|
|
1631
|
+
schedules: collected.schedules ?? [],
|
|
1632
|
+
...(collected.workflowWorld === undefined
|
|
1633
|
+
? {}
|
|
1634
|
+
: { workflowWorld: collected.workflowWorld }),
|
|
1635
|
+
// In-place updates carry the prior Durable Object bindings and
|
|
1636
|
+
// migration history forward verbatim: classes are append-only and are
|
|
1637
|
+
// never silently re-declared or removed.
|
|
1638
|
+
...(isUpdate && priorDurableObjects !== undefined
|
|
1639
|
+
? { durableObjects: priorDurableObjects }
|
|
1640
|
+
: {}),
|
|
1641
|
+
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1642
|
+
? {}
|
|
1643
|
+
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
1644
|
+
};
|
|
1610
1645
|
let hostConfig;
|
|
1611
1646
|
try {
|
|
1612
|
-
const { createEveHostConfig, } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1613
1647
|
hostConfig = createEveHostConfig({
|
|
1614
|
-
|
|
1615
|
-
workerName: identity.workerName,
|
|
1616
|
-
containerApplicationName: identity.containerApplicationName,
|
|
1617
|
-
containerClassName: "EveHostContainer",
|
|
1618
|
-
containerBindingName: "EVE_CONTAINER",
|
|
1619
|
-
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1620
|
-
deploymentId: identity.deploymentId,
|
|
1621
|
-
generationId: identity.generationId,
|
|
1622
|
-
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1623
|
-
containerImage: identity.containerImage,
|
|
1624
|
-
runtimeVariableNames: identity.runtimeVariableNames,
|
|
1625
|
-
...(identity.runtimeRevisionHandle === undefined
|
|
1626
|
-
? {}
|
|
1627
|
-
: { runtimeRevisionHandle: identity.runtimeRevisionHandle }),
|
|
1628
|
-
schedules: collected.schedules ?? [],
|
|
1629
|
-
...(collected.workflowWorld === undefined
|
|
1630
|
-
? {}
|
|
1631
|
-
: { workflowWorld: collected.workflowWorld }),
|
|
1632
|
-
// In-place updates carry the prior Durable Object bindings and
|
|
1633
|
-
// migration history forward verbatim: classes are append-only, never
|
|
1634
|
-
// re-declared, and never deleted (a deleted_classes migration destroys
|
|
1635
|
-
// the data).
|
|
1636
|
-
...(isUpdate && priorDurableObjects !== undefined
|
|
1637
|
-
? { durableObjects: priorDurableObjects }
|
|
1638
|
-
: {}),
|
|
1639
|
-
...(process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER === undefined
|
|
1640
|
-
? {}
|
|
1641
|
-
: { containerSleepAfter: process.env.EDEN_EVE_CONTAINER_SLEEP_AFTER }),
|
|
1648
|
+
...baseConfigRequest,
|
|
1642
1649
|
});
|
|
1643
1650
|
}
|
|
1644
1651
|
catch (error) {
|
|
@@ -1646,7 +1653,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1646
1653
|
? error.message
|
|
1647
1654
|
: "The exact Eve Worker/Container host configuration was invalid.");
|
|
1648
1655
|
}
|
|
1649
|
-
const { generateEveHostWorkerSource } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
1650
1656
|
const workerSource = generateEveHostWorkerSource({ config: hostConfig });
|
|
1651
1657
|
const paths = await writeEveDeploymentArtifacts(candidate, hostConfig, workerSource);
|
|
1652
1658
|
const publicationRequest = {
|
|
@@ -1656,13 +1662,26 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1656
1662
|
...paths,
|
|
1657
1663
|
...(runtimeInjection === undefined ? {} : { runtimeInjection }),
|
|
1658
1664
|
};
|
|
1665
|
+
/** Publishes one wrangler config; returns its terminal result. */
|
|
1666
|
+
const publishOne = async (request) => (options.publish ?? defaultPublicationRunner())(request);
|
|
1659
1667
|
let publication;
|
|
1668
|
+
const writeFailureRecord = async (status, evidence) => {
|
|
1669
|
+
try {
|
|
1670
|
+
await writeEveDeploymentRecord(candidate, identity, hostConfig, status, {
|
|
1671
|
+
evidence,
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
if (error.code !== "EEXIST")
|
|
1676
|
+
throw error;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1660
1679
|
try {
|
|
1661
1680
|
eveProgress(options, "publishing Worker and Container application");
|
|
1662
|
-
publication = await (
|
|
1681
|
+
publication = await publishOne(publicationRequest);
|
|
1663
1682
|
}
|
|
1664
1683
|
catch {
|
|
1665
|
-
await
|
|
1684
|
+
await writeFailureRecord("indeterminate", "publication-threw-before-terminal-result");
|
|
1666
1685
|
const deployment = deploymentMetadata(identity, targetKey, "indeterminate", operation, superseded);
|
|
1667
1686
|
return {
|
|
1668
1687
|
...collected.result,
|
|
@@ -1671,7 +1690,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1671
1690
|
};
|
|
1672
1691
|
}
|
|
1673
1692
|
if (publication.status === "indeterminate") {
|
|
1674
|
-
await
|
|
1693
|
+
await writeFailureRecord("indeterminate", safeText(publication.reason));
|
|
1675
1694
|
return {
|
|
1676
1695
|
...collected.result,
|
|
1677
1696
|
ok: false,
|
|
@@ -1862,25 +1881,26 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1862
1881
|
if (isUpdate) {
|
|
1863
1882
|
// The superseded generation's registry repository is generation-scoped
|
|
1864
1883
|
// and no longer referenced once CURRENT promotes this generation; only
|
|
1865
|
-
// then is it safe to delete.
|
|
1866
|
-
|
|
1884
|
+
// then is it safe to delete. Every tag in the repository goes with it —
|
|
1885
|
+
// including the snapshot tags Cloudflare created under it.
|
|
1886
|
+
const previousRepository = priorIdentity === undefined
|
|
1867
1887
|
? undefined
|
|
1868
|
-
:
|
|
1869
|
-
if (
|
|
1870
|
-
eveProgress(options, `deleting superseded registry
|
|
1888
|
+
: ownedRegistryRepository(priorIdentity, request.environment, request.name, accountId);
|
|
1889
|
+
if (previousRepository !== undefined) {
|
|
1890
|
+
eveProgress(options, `deleting superseded registry repository ${previousRepository}`);
|
|
1871
1891
|
let deleted = "indeterminate";
|
|
1872
1892
|
try {
|
|
1873
|
-
deleted = await (options.
|
|
1874
|
-
|
|
1893
|
+
deleted = await (options.deleteRegistryRepository ??
|
|
1894
|
+
defaultRegistryRepositoryDelete)({ repository: previousRepository });
|
|
1875
1895
|
}
|
|
1876
1896
|
catch {
|
|
1877
1897
|
deleted = "indeterminate";
|
|
1878
1898
|
}
|
|
1879
1899
|
supersededImageCleanup = deleted === "indeterminate"
|
|
1880
|
-
? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry
|
|
1900
|
+
? check("VAL-LIFE-007", "failed", `The update is healthy and promoted, but the superseded registry repository ${previousRepository} could not be proven deleted.`, `Remove only the recorded repository's tags ${previousRepository}; do not retry or run broad registry cleanup.`)
|
|
1881
1901
|
: check("VAL-LIFE-007", "passed", deleted === "deleted"
|
|
1882
|
-
? "The superseded generation's registry
|
|
1883
|
-
: "The superseded generation's registry
|
|
1902
|
+
? "The superseded generation's registry repository (all tags, including snapshots) was deleted after promotion."
|
|
1903
|
+
: "The superseded generation's registry repository was already absent.");
|
|
1884
1904
|
}
|
|
1885
1905
|
}
|
|
1886
1906
|
let runtimeImageCleanup;
|
|
@@ -1992,7 +2012,6 @@ async function collectEvePreflight(request, options) {
|
|
|
1992
2012
|
snapshot: null,
|
|
1993
2013
|
toolchain: null,
|
|
1994
2014
|
image: null,
|
|
1995
|
-
runtime: null,
|
|
1996
2015
|
secrets: {
|
|
1997
2016
|
runtimeVariableNames: [],
|
|
1998
2017
|
valuesRecorded: false,
|
|
@@ -2005,6 +2024,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2005
2024
|
redactionRegisteredBeforeChildren: false,
|
|
2006
2025
|
},
|
|
2007
2026
|
checks: [],
|
|
2027
|
+
sandboxResumePatch: "absent",
|
|
2008
2028
|
candidateImageId: null,
|
|
2009
2029
|
candidateImageRetainedLocally: false,
|
|
2010
2030
|
writtenPaths: [],
|
|
@@ -2012,6 +2032,13 @@ async function collectEvePreflight(request, options) {
|
|
|
2012
2032
|
};
|
|
2013
2033
|
}
|
|
2014
2034
|
checks.push(...packaging.checks.map(mapPackagingCheck));
|
|
2035
|
+
if (packaging.sandboxResumePatch === "skipped") {
|
|
2036
|
+
warnings.push({
|
|
2037
|
+
id: "EVE_SANDBOX_RESUME_PATCH",
|
|
2038
|
+
message: "Eve's just-bash file is not a version Eden knows; conversations may lose their sandbox after a skill change (eve#4440)",
|
|
2039
|
+
remediation: "Pin Eve to a version Eden knows, or update Eden so its known Eve versions include yours.",
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2015
2042
|
const discardPackagingImage = async () => {
|
|
2016
2043
|
if (packaging.candidateImageId === null ||
|
|
2017
2044
|
builder.discard === undefined) {
|
|
@@ -2058,9 +2085,6 @@ async function collectEvePreflight(request, options) {
|
|
|
2058
2085
|
}
|
|
2059
2086
|
warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
|
|
2060
2087
|
const schedules = await readCompiledSchedules(candidate);
|
|
2061
|
-
// The runtime package is imported lazily so `eden` commands that never
|
|
2062
|
-
// reach deploy checks stay fast; the module specifier is fixed.
|
|
2063
|
-
const { parseEveScheduleCron } = await import("@moinulmoin/eden-runtime-cloudflare");
|
|
2064
2088
|
const expressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) !== undefined);
|
|
2065
2089
|
const unexpressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) === undefined);
|
|
2066
2090
|
if (unexpressible.length > 0) {
|
|
@@ -2208,7 +2232,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2208
2232
|
"VAL-BUILD-007",
|
|
2209
2233
|
];
|
|
2210
2234
|
const runtimeProofComplete = requiredRuntimeChecks.every((id) => runtimeEvidence?.checks.some((value) => value.id === id && value.status === "passed"));
|
|
2211
|
-
const runtimeImageIdentityValid =
|
|
2235
|
+
const runtimeImageIdentityValid = IMAGE_ID_PATTERN.test(runtimeEvidence.imageDigest ?? "");
|
|
2212
2236
|
if (!runtimeProofComplete) {
|
|
2213
2237
|
checks.push(check("VAL-BUILD-005", "failed", "The runtime seam did not return complete proof of Linux/amd64 image construction, official Eve-start boot, and real health.", "Return passed VAL-BUILD-005, VAL-BUILD-006, and VAL-BUILD-007 checks from the disposable runtime runner."));
|
|
2214
2238
|
}
|
|
@@ -2496,7 +2520,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2496
2520
|
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2497
2521
|
const scanned = [];
|
|
2498
2522
|
const deploymentIds = new Set();
|
|
2499
|
-
const images = new Set();
|
|
2500
2523
|
for (const entry of entries) {
|
|
2501
2524
|
const generationRoot = join(generationsRoot, entry);
|
|
2502
2525
|
const recordPath = join(generationRoot, "deployment.json");
|
|
@@ -2519,9 +2542,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2519
2542
|
const deploymentId = parsed.deploymentId;
|
|
2520
2543
|
if (deploymentId !== undefined)
|
|
2521
2544
|
deploymentIds.add(deploymentId);
|
|
2522
|
-
if (typeof parsed.identity.containerImage === "string") {
|
|
2523
|
-
images.add(parsed.identity.containerImage);
|
|
2524
|
-
}
|
|
2525
2545
|
}
|
|
2526
2546
|
}
|
|
2527
2547
|
scanned.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
@@ -2557,7 +2577,6 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2557
2577
|
identity: live.record.identity,
|
|
2558
2578
|
status: live.record.status,
|
|
2559
2579
|
deploymentIds: [...deploymentIds],
|
|
2560
|
-
images: [...images],
|
|
2561
2580
|
worldCloudflare: live.record.worldCloudflare,
|
|
2562
2581
|
durableObjects: historySource?.record.durableObjects,
|
|
2563
2582
|
};
|
|
@@ -2607,19 +2626,20 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2607
2626
|
const migrations = [];
|
|
2608
2627
|
const durableObjectsValid = Array.isArray(rawBindings) &&
|
|
2609
2628
|
Array.isArray(rawMigrations);
|
|
2629
|
+
const invalidHistory = () => ({
|
|
2630
|
+
recordPath,
|
|
2631
|
+
identity: base.identity,
|
|
2632
|
+
status,
|
|
2633
|
+
deploymentId,
|
|
2634
|
+
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2635
|
+
durableObjects: undefined,
|
|
2636
|
+
});
|
|
2610
2637
|
if (durableObjectsValid) {
|
|
2611
2638
|
for (const binding of rawBindings) {
|
|
2612
2639
|
if (typeof binding !== "object" || binding === null ||
|
|
2613
2640
|
typeof binding.name !== "string" ||
|
|
2614
2641
|
typeof binding.class_name !== "string") {
|
|
2615
|
-
return
|
|
2616
|
-
recordPath,
|
|
2617
|
-
identity: base.identity,
|
|
2618
|
-
status,
|
|
2619
|
-
deploymentId,
|
|
2620
|
-
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2621
|
-
durableObjects: undefined,
|
|
2622
|
-
};
|
|
2642
|
+
return invalidHistory();
|
|
2623
2643
|
}
|
|
2624
2644
|
bindings.push(binding);
|
|
2625
2645
|
}
|
|
@@ -2627,15 +2647,8 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2627
2647
|
if (typeof migration !== "object" || migration === null ||
|
|
2628
2648
|
typeof migration.tag !== "string" ||
|
|
2629
2649
|
!Array.isArray(migration.new_sqlite_classes) ||
|
|
2630
|
-
migration.new_sqlite_classes.some((className) => typeof className !== "string")) {
|
|
2631
|
-
return
|
|
2632
|
-
recordPath,
|
|
2633
|
-
identity: base.identity,
|
|
2634
|
-
status,
|
|
2635
|
-
deploymentId,
|
|
2636
|
-
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
2637
|
-
durableObjects: undefined,
|
|
2638
|
-
};
|
|
2650
|
+
(migration.new_sqlite_classes ?? []).some((className) => typeof className !== "string")) {
|
|
2651
|
+
return invalidHistory();
|
|
2639
2652
|
}
|
|
2640
2653
|
migrations.push(migration);
|
|
2641
2654
|
}
|
|
@@ -2651,37 +2664,30 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2651
2664
|
}
|
|
2652
2665
|
async function defaultDestroyTargetRead(request) {
|
|
2653
2666
|
const account = await readWranglerJson(["whoami", "--json"]);
|
|
2654
|
-
const deployments = await
|
|
2655
|
-
"deployments",
|
|
2656
|
-
"list",
|
|
2657
|
-
"--name",
|
|
2658
|
-
request.workerName,
|
|
2659
|
-
"--json",
|
|
2660
|
-
]);
|
|
2661
|
-
const containers = await readWranglerJson([
|
|
2662
|
-
"containers",
|
|
2663
|
-
"list",
|
|
2664
|
-
"--json",
|
|
2665
|
-
"--per-page",
|
|
2666
|
-
"100",
|
|
2667
|
-
]);
|
|
2667
|
+
const { deployments, containerEntries: entries, deploymentEntries } = await readExactTargetInventory(request.workerName, request.containerApplicationName);
|
|
2668
2668
|
const accountId = authenticatedAccountId(account.value);
|
|
2669
2669
|
const workerExists = !deployments.failed &&
|
|
2670
|
-
(
|
|
2671
|
-
const entries = exactContainerEntries(containers.value, request.containerApplicationName);
|
|
2670
|
+
(deploymentEntries?.length ?? 0) > 0;
|
|
2672
2671
|
const firstContainer = entries === undefined ? undefined : entries[0];
|
|
2673
|
-
const
|
|
2674
|
-
let
|
|
2675
|
-
if (
|
|
2672
|
+
const ownedRepositories = request.registryRepositories;
|
|
2673
|
+
let registryTagsPresent;
|
|
2674
|
+
if (ownedRepositories !== undefined && ownedRepositories.length > 0) {
|
|
2676
2675
|
const images = await readWranglerJson([
|
|
2677
2676
|
"containers",
|
|
2678
2677
|
"images",
|
|
2679
2678
|
"list",
|
|
2680
2679
|
"--json",
|
|
2681
2680
|
]);
|
|
2682
|
-
const
|
|
2683
|
-
if (!images.failed &&
|
|
2684
|
-
|
|
2681
|
+
const tagsByRepository = registryTagsByRepository(images.value);
|
|
2682
|
+
if (!images.failed && tagsByRepository !== undefined) {
|
|
2683
|
+
const present = [];
|
|
2684
|
+
for (const [repository, tags] of tagsByRepository) {
|
|
2685
|
+
if (!ownedRepositories.includes(repository))
|
|
2686
|
+
continue;
|
|
2687
|
+
for (const tag of tags)
|
|
2688
|
+
present.push(`${repository}:${tag}`);
|
|
2689
|
+
}
|
|
2690
|
+
registryTagsPresent = present;
|
|
2685
2691
|
}
|
|
2686
2692
|
}
|
|
2687
2693
|
return {
|
|
@@ -2690,21 +2696,22 @@ async function defaultDestroyTargetRead(request) {
|
|
|
2690
2696
|
? {}
|
|
2691
2697
|
: { containerApplicationId: firstContainer.id }),
|
|
2692
2698
|
...(accountId === undefined ? {} : { accountId }),
|
|
2693
|
-
...(
|
|
2699
|
+
...(registryTagsPresent === undefined
|
|
2694
2700
|
? {}
|
|
2695
|
-
: {
|
|
2701
|
+
: { registryTagsPresent }),
|
|
2696
2702
|
};
|
|
2697
2703
|
}
|
|
2698
2704
|
/**
|
|
2699
2705
|
* `wrangler containers images list --json` reports `[{name, tags}]` entries
|
|
2700
|
-
* with the account prefix already stripped from the repository name. The
|
|
2701
|
-
* carries
|
|
2706
|
+
* with the account prefix already stripped from the repository name. The map
|
|
2707
|
+
* carries every tag per repository so owned-repository cleanup can delete
|
|
2708
|
+
* snapshot tags alongside the pushed `candidate` tag.
|
|
2702
2709
|
*/
|
|
2703
|
-
function
|
|
2710
|
+
function registryTagsByRepository(value) {
|
|
2704
2711
|
const collection = jsonCollection(value);
|
|
2705
2712
|
if (collection === undefined)
|
|
2706
2713
|
return undefined;
|
|
2707
|
-
const
|
|
2714
|
+
const tagsByRepository = new Map();
|
|
2708
2715
|
for (const entry of collection) {
|
|
2709
2716
|
if (typeof entry !== "object" || entry === null || !("name" in entry)) {
|
|
2710
2717
|
return undefined;
|
|
@@ -2713,13 +2720,15 @@ function registryImageNames(value) {
|
|
|
2713
2720
|
const tags = "tags" in entry ? entry.tags : undefined;
|
|
2714
2721
|
if (typeof name !== "string" || !Array.isArray(tags))
|
|
2715
2722
|
return undefined;
|
|
2723
|
+
const validTags = [];
|
|
2716
2724
|
for (const tag of tags) {
|
|
2717
2725
|
if (typeof tag !== "string")
|
|
2718
2726
|
return undefined;
|
|
2719
|
-
|
|
2727
|
+
validTags.push(tag);
|
|
2720
2728
|
}
|
|
2729
|
+
tagsByRepository.set(name, validTags);
|
|
2721
2730
|
}
|
|
2722
|
-
return
|
|
2731
|
+
return tagsByRepository;
|
|
2723
2732
|
}
|
|
2724
2733
|
async function defaultWorkerDelete(request) {
|
|
2725
2734
|
const result = await runWranglerWithInput(["delete", "--name", request.name, "--force"], process.cwd(), "y\n");
|
|
@@ -2739,23 +2748,59 @@ async function defaultContainerDelete(request) {
|
|
|
2739
2748
|
}
|
|
2740
2749
|
return "indeterminate";
|
|
2741
2750
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2751
|
+
/**
|
|
2752
|
+
* Deletes every tag in an owned registry repository — the `candidate` tag
|
|
2753
|
+
* Eden pushed plus any `rootfs-snapshot-*`/`rootfs-set-*` tags container
|
|
2754
|
+
* snapshots created under the source image repository. Deleting only
|
|
2755
|
+
* `candidate` would leave snapshot storage behind, so each tag in the
|
|
2756
|
+
* repository is deleted individually.
|
|
2757
|
+
*/
|
|
2758
|
+
async function defaultRegistryRepositoryDelete(request) {
|
|
2759
|
+
const images = await readWranglerJson([
|
|
2760
|
+
"containers",
|
|
2761
|
+
"images",
|
|
2762
|
+
"list",
|
|
2763
|
+
"--json",
|
|
2764
|
+
]);
|
|
2765
|
+
const collection = jsonCollection(images.value);
|
|
2766
|
+
if (images.failed || collection === undefined)
|
|
2767
|
+
return "indeterminate";
|
|
2768
|
+
const entry = collection.find((candidate) => typeof candidate === "object" &&
|
|
2769
|
+
candidate !== null &&
|
|
2770
|
+
candidate.name === request.repository);
|
|
2771
|
+
if (entry === undefined)
|
|
2747
2772
|
return "absent";
|
|
2773
|
+
const tags = entry.tags;
|
|
2774
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string")) {
|
|
2775
|
+
return "indeterminate";
|
|
2748
2776
|
}
|
|
2749
|
-
|
|
2777
|
+
let deletedAny = false;
|
|
2778
|
+
for (const tag of tags) {
|
|
2779
|
+
const result = await runWranglerWithInput([
|
|
2780
|
+
"containers",
|
|
2781
|
+
"images",
|
|
2782
|
+
"delete",
|
|
2783
|
+
`${request.repository}:${tag}`,
|
|
2784
|
+
"--skip-confirmation",
|
|
2785
|
+
], process.cwd());
|
|
2786
|
+
if (result.exitCode === 0) {
|
|
2787
|
+
deletedAny = true;
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
|
|
2791
|
+
return "indeterminate";
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return deletedAny ? "deleted" : "absent";
|
|
2750
2795
|
}
|
|
2751
2796
|
/**
|
|
2752
|
-
* The
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2797
|
+
* The registry repository this generation's deployment record proves Eden
|
|
2798
|
+
* pushed to, or undefined. Ownership holds only when the recorded digest
|
|
2799
|
+
* reference points at the repository derived from the same identity that
|
|
2800
|
+
* produced it; a digest-bearing reference to any other repository was
|
|
2801
|
+
* published outside Eden's push and is never owned here.
|
|
2757
2802
|
*/
|
|
2758
|
-
function
|
|
2803
|
+
function ownedRegistryRepository(identity, environment, name, accountId) {
|
|
2759
2804
|
if (identity.projectId === undefined ||
|
|
2760
2805
|
identity.generationId === undefined ||
|
|
2761
2806
|
identity.containerImage === undefined ||
|
|
@@ -2768,14 +2813,14 @@ function ownedRegistryImageRef(identity, environment, name, accountId) {
|
|
|
2768
2813
|
imageDigestFromReference(identity.containerImage) === undefined) {
|
|
2769
2814
|
return undefined;
|
|
2770
2815
|
}
|
|
2771
|
-
return
|
|
2816
|
+
return repository;
|
|
2772
2817
|
}
|
|
2773
2818
|
/**
|
|
2774
|
-
* The
|
|
2775
|
-
* retained attempt evidence names the target's repository; the account
|
|
2776
|
-
* target key must match the verified destroy record.
|
|
2819
|
+
* The registry repository an indeterminate deploy attempt provably pushed.
|
|
2820
|
+
* The retained attempt evidence names the target's repository; the account
|
|
2821
|
+
* and target key must match the verified destroy record.
|
|
2777
2822
|
*/
|
|
2778
|
-
async function
|
|
2823
|
+
async function attemptRegistryRepository(attemptPath, accountId, targetKey) {
|
|
2779
2824
|
const raw = await readFile(attemptPath, "utf8").catch(() => undefined);
|
|
2780
2825
|
if (raw === undefined)
|
|
2781
2826
|
return undefined;
|
|
@@ -2795,7 +2840,7 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
|
|
|
2795
2840
|
(!("targetKey" in parsed) || parsed.targetKey === targetKey) &&
|
|
2796
2841
|
"accountId" in parsed &&
|
|
2797
2842
|
parsed.accountId === accountId) {
|
|
2798
|
-
return
|
|
2843
|
+
return parsed.imageRepository;
|
|
2799
2844
|
}
|
|
2800
2845
|
if ("imageReference" in parsed &&
|
|
2801
2846
|
typeof parsed.imageReference === "string" &&
|
|
@@ -2805,39 +2850,40 @@ async function attemptRegistryImageRef(attemptPath, accountId, targetKey) {
|
|
|
2805
2850
|
if (repository !== undefined &&
|
|
2806
2851
|
repository.startsWith(expected) &&
|
|
2807
2852
|
(!("targetKey" in parsed) || parsed.targetKey === targetKey)) {
|
|
2808
|
-
return
|
|
2853
|
+
return repository.slice(`registry.cloudflare.com/${accountId}/`.length);
|
|
2809
2854
|
}
|
|
2810
2855
|
}
|
|
2811
2856
|
return undefined;
|
|
2812
2857
|
}
|
|
2813
2858
|
/**
|
|
2814
|
-
* Every registry
|
|
2859
|
+
* Every registry repository this exact target provably owns: the recorded
|
|
2815
2860
|
* digest references in matching deployment records plus the repository
|
|
2816
2861
|
* evidence retained by indeterminate pushes. Never derived by prefix-matching
|
|
2817
|
-
* the registry; each
|
|
2862
|
+
* the registry; each repository is computed from the immutable generation
|
|
2863
|
+
* records.
|
|
2818
2864
|
*/
|
|
2819
|
-
async function
|
|
2865
|
+
async function collectEveDestroyRegistryRepositories(projectRoot, environment, name, record) {
|
|
2820
2866
|
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2821
2867
|
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2822
|
-
const
|
|
2868
|
+
const repositories = new Set();
|
|
2823
2869
|
const targetKey = record.projectId === undefined
|
|
2824
2870
|
? undefined
|
|
2825
2871
|
: stableDeploymentKey(record.projectId, record.accountId, environment, name);
|
|
2826
2872
|
for (const entry of entries) {
|
|
2827
2873
|
const generationRoot = join(generationsRoot, entry);
|
|
2828
2874
|
const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
|
|
2829
|
-
const
|
|
2875
|
+
const deployedRepository = parsed === undefined
|
|
2830
2876
|
? undefined
|
|
2831
|
-
:
|
|
2832
|
-
if (
|
|
2833
|
-
|
|
2877
|
+
: ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
|
|
2878
|
+
if (deployedRepository !== undefined)
|
|
2879
|
+
repositories.add(deployedRepository);
|
|
2834
2880
|
if (targetKey === undefined)
|
|
2835
2881
|
continue;
|
|
2836
|
-
const
|
|
2837
|
-
if (
|
|
2838
|
-
|
|
2882
|
+
const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
|
|
2883
|
+
if (attemptRepository !== undefined)
|
|
2884
|
+
repositories.add(attemptRepository);
|
|
2839
2885
|
}
|
|
2840
|
-
return [...
|
|
2886
|
+
return [...repositories];
|
|
2841
2887
|
}
|
|
2842
2888
|
export async function runEveDestroy(request, options = {}) {
|
|
2843
2889
|
const startedAt = Date.now();
|
|
@@ -2890,11 +2936,11 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2890
2936
|
}
|
|
2891
2937
|
checks.push(check("VAL-LIFE-006-RECORD", "passed", "The immutable deployment record matched the exact target selectors."));
|
|
2892
2938
|
eveProgress(options, "verifying the immutable ownership record");
|
|
2893
|
-
const
|
|
2939
|
+
const ownedRegistryRepositories = await collectEveDestroyRegistryRepositories(request.projectRoot, request.environment, request.name, record.identity);
|
|
2894
2940
|
const readTarget = () => Promise.resolve(reader({
|
|
2895
2941
|
workerName: expectedWorker,
|
|
2896
2942
|
containerApplicationName: expectedContainer,
|
|
2897
|
-
|
|
2943
|
+
registryRepositories: ownedRegistryRepositories,
|
|
2898
2944
|
}));
|
|
2899
2945
|
const reader = options.destroyCloudflareRead ?? defaultDestroyTargetRead;
|
|
2900
2946
|
eveProgress(options, "reading the exact remote inventory");
|
|
@@ -2904,9 +2950,9 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2904
2950
|
}
|
|
2905
2951
|
if (!before.workerExists &&
|
|
2906
2952
|
before.containerApplicationId === undefined &&
|
|
2907
|
-
(
|
|
2908
|
-
(before.
|
|
2909
|
-
before.
|
|
2953
|
+
(ownedRegistryRepositories.length === 0 ||
|
|
2954
|
+
(before.registryTagsPresent !== undefined &&
|
|
2955
|
+
before.registryTagsPresent.length === 0))) {
|
|
2910
2956
|
emit({
|
|
2911
2957
|
ok: true,
|
|
2912
2958
|
status: "absent",
|
|
@@ -2963,17 +3009,22 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2963
3009
|
? "The exact owned Container application was deleted."
|
|
2964
3010
|
: "The exact Container application was already absent."));
|
|
2965
3011
|
}
|
|
2966
|
-
const
|
|
2967
|
-
if (
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3012
|
+
const repositoryDeletes = new Map();
|
|
3013
|
+
if (ownedRegistryRepositories.length > 0) {
|
|
3014
|
+
const deleteRegistryRepository = options.deleteRegistryRepository ?? defaultRegistryRepositoryDelete;
|
|
3015
|
+
// Every tag inside an owned repository is deleted (candidate plus
|
|
3016
|
+
// snapshot tags). When the registry could not be enumerated, still
|
|
3017
|
+
// attempt the exact owned repositories; absence verification below
|
|
3018
|
+
// re-reads the registry when it becomes readable.
|
|
3019
|
+
const presentRepositories = new Set((before.registryTagsPresent ?? []).map((ref) => ref.split(":")[0] ?? ""));
|
|
3020
|
+
const deletable = before.registryTagsPresent === undefined
|
|
3021
|
+
? ownedRegistryRepositories
|
|
3022
|
+
: ownedRegistryRepositories.filter((repository) => presentRepositories.has(repository));
|
|
2972
3023
|
if (deletable.length > 0) {
|
|
2973
|
-
eveProgress(options, `deleting ${deletable.length} registry
|
|
3024
|
+
eveProgress(options, `deleting ${deletable.length} registry repositor${deletable.length === 1 ? "y" : "ies"}`);
|
|
2974
3025
|
}
|
|
2975
|
-
for (const
|
|
2976
|
-
|
|
3026
|
+
for (const repository of deletable) {
|
|
3027
|
+
repositoryDeletes.set(repository, await deleteRegistryRepository({ repository }));
|
|
2977
3028
|
}
|
|
2978
3029
|
}
|
|
2979
3030
|
eveProgress(options, "verifying the exact target is absent");
|
|
@@ -2991,15 +3042,16 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
2991
3042
|
});
|
|
2992
3043
|
fail("EVE_DESTROY_ABSENCE_UNPROVEN", "The exact target could not be proven absent after deletion.");
|
|
2993
3044
|
}
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3045
|
+
const presentTagsAfter = after.registryTagsPresent ?? [];
|
|
3046
|
+
const retainedRepositories = ownedRegistryRepositories.filter((repository) => presentTagsAfter.some((ref) => ref.startsWith(`${repository}:`)) ||
|
|
3047
|
+
(after.registryTagsPresent === undefined &&
|
|
3048
|
+
repositoryDeletes.get(repository) === "indeterminate"));
|
|
3049
|
+
if (ownedRegistryRepositories.length > 0) {
|
|
3050
|
+
checks.push(retainedRepositories.length === 0
|
|
2999
3051
|
? check("VAL-LIFE-006-REGISTRY", "passed", "Every registry image tag recorded for this exact target was deleted or proven absent.")
|
|
3000
|
-
: check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image(s) remain after destroy: ${
|
|
3052
|
+
: check("VAL-LIFE-006-REGISTRY", "failed", `The exact target's registry image tag(s) remain after destroy: ${retainedRepositories.join(", ")}.`, "Retry eden destroy with the same selectors, or remove only the listed repositories' tags with wrangler containers images delete."));
|
|
3001
3053
|
}
|
|
3002
|
-
if (
|
|
3054
|
+
if (retainedRepositories.length > 0) {
|
|
3003
3055
|
emit({
|
|
3004
3056
|
ok: false,
|
|
3005
3057
|
status: "failed",
|
|
@@ -3007,7 +3059,7 @@ async function runEveDestroyLocked(request, options, checks, emit, fail) {
|
|
|
3007
3059
|
name: request.name,
|
|
3008
3060
|
checks,
|
|
3009
3061
|
});
|
|
3010
|
-
fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image(s) remain: ${
|
|
3062
|
+
fail("EVE_DESTROY_IMAGES_RETAINED", `The exact Worker and Container are absent, but registry image tag(s) remain in: ${retainedRepositories.join(", ")}.`);
|
|
3011
3063
|
}
|
|
3012
3064
|
checks.push(check("VAL-LIFE-006", "passed", "Exact absence was verified; clearing this target's CURRENT pointer only."));
|
|
3013
3065
|
const currentPath = join(request.projectRoot, ".eden", "eve-deploy", "targets", `${request.environment}-${request.name}`, "CURRENT");
|