Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .derived/codebase-index/by-spec/006-fleet.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@
"specStatus": "approved"
},
"schemaVersion": "1.1.0",
"shardHash": "6fb00aa7d68b4675557a231d91033bd26c1f35cc227736ebc02dcb79f1d4d5b8"
"shardHash": "368f377cd92a8167348d55be7ea56e2c7ba9bda7491a053de2a624551eb3471e"
}
5 changes: 3 additions & 2 deletions .derived/spec-registry/by-spec/006-fleet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
32 changes: 16 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
44 changes: 44 additions & 0 deletions specs/006-fleet/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<app>`, 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.
Loading