diff --git a/.derived/codebase-index/by-spec/006-fleet.json b/.derived/codebase-index/by-spec/006-fleet.json index cd7cf86..0729d1b 100644 --- a/.derived/codebase-index/by-spec/006-fleet.json +++ b/.derived/codebase-index/by-spec/006-fleet.json @@ -28,5 +28,5 @@ "specStatus": "approved" }, "schemaVersion": "1.1.0", - "shardHash": "6fb00aa7d68b4675557a231d91033bd26c1f35cc227736ebc02dcb79f1d4d5b8" + "shardHash": "368f377cd92a8167348d55be7ea56e2c7ba9bda7491a053de2a624551eb3471e" } diff --git a/.derived/spec-registry/by-spec/006-fleet.json b/.derived/spec-registry/by-spec/006-fleet.json index 32bc808..6df11b0 100644 --- a/.derived/spec-registry/by-spec/006-fleet.json +++ b/.derived/spec-registry/by-spec/006-fleet.json @@ -26,13 +26,14 @@ "2026-07-20: the addon transferred out; this spec narrowed, not retired", "Amendment (2026-07-21): spec 011 tenant lifecycle", "Amendment (2026-07-22): remove-gate confirmation attributes", - "Amendment (2026-07-23): deploy-chosen container port" + "Amendment (2026-07-23): deploy-chosen container port", + "Amendment (2026-07-23): the in-pod two-stage E2E and the per-app ingress allow (fleet-native 0.2.0)" ], "specPath": "specs/006-fleet/spec.md", "status": "approved", "summary": "Milestone M3: operate stamped apps. The unit of placement is \"one EnRaHiTu container + one volume + one ingress\" on the existing hetzner-k3s cluster. deployd-api-rs (the OAP-era Rust K8s orchestrator, axum + hiqlite) donates its orchestration core as a napi-rs addon (the hiqlite-native pattern): the axum HTTP layer disappears, the K8s knowledge stays. A fleet/ Encore service exposes deploy / status / update / backup over the addon. Done-when is a fleet of ten stamped apps on one box with update and backup exercised.\n", "title": "Fleet: deployd's core as an in-process addon, placing EnRaHiTu apps" }, - "shardHash": "67db401155879258649c91fd1e408de84c840ddaa8223f2d4e31692f4ebc4539", + "shardHash": "72d72f721d4dd5e549b5b761cde13bde18d6a736420ae93488d5bc5f9f36cef0", "specVersion": "1.1.0" } diff --git a/package-lock.json b/package-lock.json index 9204466..87a1007 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@opentelemetry/sdk-trace-base": "^2.10.0", "@opentelemetry/sdk-trace-node": "^2.10.0", "@opentelemetry/semantic-conventions": "^1.43.0", - "@statecrafting/fleet-native": "0.1.0", + "@statecrafting/fleet-native": "0.2.0", "@statecrafting/governance-native": "0.1.0", "@statecrafting/hiqlite-native": "0.1.0", "encore.dev": "^1.57.9", @@ -1289,23 +1289,23 @@ "license": "MIT" }, "node_modules/@statecrafting/fleet-native": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native/-/fleet-native-0.1.0.tgz", - "integrity": "sha512-LI5Wuy/EmwZWyJzWJ+fMECs196pw2KzCLM2C96w/RJv7eMxpJpD0KoReOV8TVIoXimGTD2cMrEcBLZzPJw6pEA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native/-/fleet-native-0.2.0.tgz", + "integrity": "sha512-en8ee6JyrigKahzmQmhzVbGJj3fjseAIeHVNLIxlWYoPH0EyRq+H3FHI/QoJ+FhWw6osysDQR0rAMhl7pBXlNQ==", "license": "AGPL-3.0", "engines": { "node": ">=24" }, "optionalDependencies": { - "@statecrafting/fleet-native-darwin-arm64": "0.1.0", - "@statecrafting/fleet-native-linux-arm64-gnu": "0.1.0", - "@statecrafting/fleet-native-linux-x64-gnu": "0.1.0" + "@statecrafting/fleet-native-darwin-arm64": "0.2.0", + "@statecrafting/fleet-native-linux-arm64-gnu": "0.2.0", + "@statecrafting/fleet-native-linux-x64-gnu": "0.2.0" } }, "node_modules/@statecrafting/fleet-native-darwin-arm64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-darwin-arm64/-/fleet-native-darwin-arm64-0.1.0.tgz", - "integrity": "sha512-nA+Ohtc/mapbctvnmdKZSHICe7Um0ipUhOPPB2m8Pv+DVfFlbk4xxn7G0CqPKhW1uZqQU5MvdI92DnmhXuQjtQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-darwin-arm64/-/fleet-native-darwin-arm64-0.2.0.tgz", + "integrity": "sha512-bQv97SH+sJqT4BiYP/AAQUgI5gUapJW8fWqnRPHPG2b4SwEHUzaOTVoPsaOoEG8ggabYHeiPrZfe4CXwLlsC+g==", "cpu": [ "arm64" ], @@ -1319,9 +1319,9 @@ } }, "node_modules/@statecrafting/fleet-native-linux-arm64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-linux-arm64-gnu/-/fleet-native-linux-arm64-gnu-0.1.0.tgz", - "integrity": "sha512-Dv2G0HR9Q++jrVq7Yav06oJINbi2SVBh49Yc6GLahz/UBWVWW1XZcYH6TZMupjNJx4prVro9ceMH2U4jh/CDiA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-linux-arm64-gnu/-/fleet-native-linux-arm64-gnu-0.2.0.tgz", + "integrity": "sha512-4JvUYNVWhK/5zu4s4E1Dr3xcBozTP2EaD1dW6h7M3KkK8IQo4QkKQd/ILS8a3mslwNDSCiyo3cXJ+h73+Aa4bQ==", "cpu": [ "arm64" ], @@ -1335,9 +1335,9 @@ } }, "node_modules/@statecrafting/fleet-native-linux-x64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-linux-x64-gnu/-/fleet-native-linux-x64-gnu-0.1.0.tgz", - "integrity": "sha512-2i9i0ldlFo/gDPGmzWwDU31cNMN94Ql5fQjKnkykvT+4D+VylJeMI/oQbnCKst8b4vWSVRwmZYK44RjDOXyBeQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@statecrafting/fleet-native-linux-x64-gnu/-/fleet-native-linux-x64-gnu-0.2.0.tgz", + "integrity": "sha512-mhH+LBA9u4Ve4egUlpZMUiZIhY8/y2/xHjlZgIUvYky9AGUZ/N6JA0oQnG/kYCbEXipAGWar/uiLE6C2dLZbLw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 9afedeb..473869a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@opentelemetry/sdk-trace-base": "^2.10.0", "@opentelemetry/sdk-trace-node": "^2.10.0", "@opentelemetry/semantic-conventions": "^1.43.0", - "@statecrafting/fleet-native": "0.1.0", + "@statecrafting/fleet-native": "0.2.0", "@statecrafting/governance-native": "0.1.0", "@statecrafting/hiqlite-native": "0.1.0", "encore.dev": "^1.57.9", diff --git a/specs/006-fleet/spec.md b/specs/006-fleet/spec.md index 103aa8c..38588f0 100644 --- a/specs/006-fleet/spec.md +++ b/specs/006-fleet/spec.md @@ -420,3 +420,47 @@ it to `ghcr-pull` (see the spec 009 §4.4 note and the spec 010 catalog correction; the semantics here are unchanged from §3 finding #2: a pre-provisioned Secret NAME, operator-created per tenant namespace, because fleet's RBAC deliberately grants nothing on secrets). + +## Amendment (2026-07-23): the in-pod two-stage E2E and the per-app ingress allow (fleet-native 0.2.0) + +The first fully in-pod two-stage fleet E2E ran 2026-07-23 from the +production control-plane pod's own ServiceAccount, through the governed +verbs only. Stage 1 placed `ealen/echo-server` on port 4000: namespace, +Deployment, PVC, Service, Ingress up in about 39 seconds, public 200 on +`/health` with a valid Let's Encrypt certificate. Stage 2 placed the +private enrahitu chassis image on port 8080 (after copying `ghcr-pull` +into the tenant namespace; reflector does not sync it, consistent with +finding #2's operator-provisioned semantics). Both apps were then removed +through the strict remove gate, so the full deploy/remove lifecycle ran +in-pod; the deploy and remove attestations for both apps are recorded +(record_seq 2 through 5). + +Stage 2 surfaced one real defect, the namespace-wide port pin. The +addon's `fleet-allow-ingress-nginx` NetworkPolicy was namespace-scoped +(`podSelector: {}`) but pinned the deploying app's single port, and its +create-or-tolerate-409 application meant the first app placed into a +tenant namespace froze the allowed port set for every later app. The +chassis rolled out Ready, because kubelet probes bypass NetworkPolicy, +and served 502 from the edge: the same silent-failure class the +deploy-chosen-port amendment above killed, one layer down. Live +mitigation: the policy was patched by hand to also admit 8080, after +which the chassis served correctly from the public edge. + +The durable fix is statecrafting spec 006's amendment (2026-07-23, +`fleet-native` 0.2.0): the ingress allow becomes per-app +(`fleet-allow-ingress-`, podSelector on the app's selector labels, a +port list of exactly the app's port), `removeApp` deletes it with the +app's other per-app resources, and the namespace-scoped deny-all + egress +baseline pair is unchanged. This spec's consumer pin moves from `0.1.0` +to `0.2.0`; the napi surface and the TS facade are unchanged, so no code +in `backend/fleet/` moves. + +Operator residuals from the run, recorded here because this spec is the +live operational record: + +- The hand-patched namespace-wide `fleet-allow-ingress-nginx` in the test + tenant's namespace predates 0.2.0 and is deleted once the 0.2.0-pinned + image is live and placements have per-app policies. NetworkPolicies are + additive, so the ordering is safe in both directions. +- `fleet-allow-egress` admits only DNS and outbound 443: a placed cell + cannot reach SMTP, a residual for cells that send mail.