Skip to content

Commit eb9d614

Browse files
committed
Add EdgeZero full-migration umbrella design spec
Defines the end-state, current-state gap analysis, and an ordered set of phases (stores, config injection, secret externalization, extractors, legacy-path removal) for moving Trusted Server completely onto EdgeZero. Phase 0 (State<T> extractor + nested #[secret]) is an upstream edgezero prerequisite tracked separately.
1 parent fe82dea commit eb9d614

1 file changed

Lines changed: 212 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Trusted Server → EdgeZero — Full Migration (umbrella design)
2+
3+
- **Status:** Draft for review
4+
- **Date:** 2026-07-02
5+
- **Scope:** Move trusted-server **completely** onto EdgeZero primitives: config push, KV, secret store, config injection without an embedded `trusted-server.toml`, extractor-based handlers, and deletion of every pre-EdgeZero workaround.
6+
- **Shape:** Umbrella roadmap. Defines the end-state, the current-state gap, and an ordered set of phases with dependencies. **Each phase gets its own implementation plan** (`writing-plans`) before code is written.
7+
- **Companion spec:** Phase 0 (`State<T>` extractor + nested `#[secret]`) is an **edgezero-repo** change, specified separately (`…-state-and-nested-secrets-design.md`) and tracked via its own edgezero PR. This umbrella depends on it but does not re-specify it.
8+
9+
---
10+
11+
## 1. End-state
12+
13+
trusted-server is a fully EdgeZero-native app: adapter binaries call `run_app::<App>`; core is platform-neutral; config, KV, and secrets flow exclusively through EdgeZero's `StoreRegistry`; app config is a signed blob published by `ts config push` and read back typed at request time with secrets resolved from the secret store; handlers are `#[action]` functions taking `FromRequest` extractors; and no Fastly-specific or pre-EdgeZero shim remains in core or the adapters.
14+
15+
Concretely, at the end of this migration:
16+
17+
- **No `include_str!` of any `*.toml` config** in any adapter. All four adapters load app config from the EdgeZero config store.
18+
- **No app-level secrets embedded in the pushed config blob.** Secrets live in the EdgeZero secret store; the blob carries only key names, resolved at request time.
19+
- **No bespoke `PlatformConfigStore` / `PlatformSecretStore` / `RuntimeServices`.** Core and adapters use EdgeZero `ConfigStore` / `SecretStore` / `KvStore` via `StoreRegistry`.
20+
- **No `FastlyManagementApiClient`, no `settings_data.rs` chunk resolver, no `config`-crate env overlay, no `Redacted<T>`.**
21+
- **Core handlers are extractor-based**; the per-adapter handler shims are gone.
22+
- **The legacy Fastly `route_request` path, `compat.rs`, and the `edgezero_enabled` / `edgezero_rollout_pct` flags are deleted** (final phase, gated on 100% rollout).
23+
24+
---
25+
26+
## 2. Current-state gap analysis
27+
28+
Verified across `trusted-server-core`, the four adapters, `trusted-server-cli`, and the pinned `edgezero` dependency.
29+
30+
| Concern | Today | Gap to close |
31+
|---|---|---|
32+
| **KV** | ✅ 100% on EdgeZero (`KvStore`/`KvHandle`, re-exported as `PlatformKvStore`) | None (baseline for the pattern) |
33+
| **Routing** | ✅ All 4 adapters route through EdgeZero `RouterService` + `Hooks` | None structurally; handler authoring changes in Phase 4 |
34+
| **Core off `fastly::` types** | ✅ Enforced by `migration_guards.rs` | Keep the guard; extend coverage as adapters shrink |
35+
| **Config load** | ⚠️ Fastly + Axum load the blob from the config store; **Cloudflare + Spin `include_str!` `trusted-server.example.toml`** | Phase 2 |
36+
| **Config injection** | ⚠️ `TrustedServerAppConfig` wraps `Settings`, `SECRET_FIELDS = &[]` → secrets inline in blob; `#[derive(AppConfig)]`/`#[secret]` unused | Phases 2–3 |
37+
| **Config / Secret stores** | ❌ Core uses bespoke `PlatformConfigStore`/`PlatformSecretStore` + `RuntimeServices`; 4× per-adapter `platform.rs` impls; `FastlyManagementApiClient` for writes | Phase 1 |
38+
| **Fastly config chunking** |`settings_data.rs` re-implements EdgeZero's `chunked_config.rs` verbatim in core | Phase 1 |
39+
| **Env overlay** |`from_toml_and_env` + `TRUSTED_SERVER__*` via the `config` crate (test-only, but keeps the dep) | Phase 2 |
40+
| **Handlers → extractors** | ❌ Hand-written `Fn(RequestContext)` shims calling `(&Settings, &RuntimeServices, Request)`; `#[action]`/`FromRequest` unused; **no `State<T>` extractor exists upstream** | Phase 0 (upstream) → Phase 4 |
41+
| **Legacy Fastly path** |`legacy_main`/`route_request` + `compat.rs` + rollout flags live (marked "TODO delete after Phase 5 cutover — #495") | Phase 5 |
42+
43+
**Key architectural constraints discovered:**
44+
45+
1. **No `State`/`Extension` extractor in EdgeZero.** trusted-server threads `Arc<AppState>` (`Settings`, `AuctionOrchestrator`, `IntegrationRegistry`) via closures. Extractor migration needs an upstream `State<T>`**Phase 0**.
46+
2. **`AppConfig<C>` re-parses + verifies + secret-walks the whole blob every request** — too costly for `Settings`. Decision: keep loading `Settings` once at startup into `Arc<Settings>`, exposed via `State<Settings>`, rather than the per-request `AppConfig` extractor (see §4, Decision D1).
47+
3. **Full secret externalization needs nested/array `#[secret]`** because `Settings` is deeply nested → **Phase 0** (edgezero derive change).
48+
4. **Integration proxies are a second, nested `matchit` router** with their own `IntegrationProxy::handle(&Settings, &RuntimeServices, req)` convention — orthogonal to the core route handlers (see §4, Decision D2).
49+
50+
---
51+
52+
## 3. Phase map (ordered, foundation-first)
53+
54+
Each phase leaves **all four adapters building and green**. Dependencies are explicit; within a phase, work can parallelize.
55+
56+
```
57+
Phase 0 (edgezero, external) ── State<T> extractor ─────────────┐
58+
└─ nested/array #[secret] ───┐ │
59+
v v
60+
Phase 1 (stores) ──> Phase 2 (config) ──> Phase 3 (secrets) Phase 4 (extractors)
61+
62+
Phase 5 (legacy removal, gated 100% rollout)
63+
```
64+
65+
- **Phase 1** depends on nothing upstream (EdgeZero store APIs already exist).
66+
- **Phase 2** depends on Phase 1 (config store must be EdgeZero-native first).
67+
- **Phase 3** depends on Phase 2 **and** Phase 0's nested `#[secret]`.
68+
- **Phase 4** depends on Phase 0's `State<T>`; it can run in parallel with 1–3 once Phase 0 lands, but is cleaner after Phase 1 (so handlers pull EdgeZero stores, not `RuntimeServices`).
69+
- **Phase 5** is last and **gated on the edgezero rollout reaching 100%** (issue #495).
70+
71+
---
72+
73+
## 4. Cross-cutting design decisions
74+
75+
**D1 — Config caching, not per-request extraction.** Keep the load-once model: at startup each adapter reads the blob from the config store, verifies the envelope, resolves secrets, validates, and stores `Arc<Settings>` in `AppState`. Handlers read it via `State<Settings>` (Phase 4). Rationale: `AppConfig<Settings>`'s per-request re-parse/verify/secret-walk is prohibitive for a struct this large. Trade-off: config changes require a new deploy/boot to take effect (already true today). *This diverges deliberately from the stock `AppConfig<C>` extractor; documented as such.*
76+
77+
**D2 — Integration proxy router stays put (for now).** Phase 4 migrates the **named/core routes** to extractors. The integration registry's nested `matchit` dispatch and `IntegrationProxy::handle` signature are internal, working, and orthogonal; migrating them is a **follow-up** (Phase 4b, optional), not silently dropped. Called out so the extractor migration isn't mistaken for "all handlers."
78+
79+
**D3 — Secret resolution happens at startup, not per request.** With D1, the startup config load resolves `#[secret]` fields against the secret store once (Phase 3). Adapters must therefore have a secret-store handle available at boot, not only per request. Fastly/Axum already open stores eagerly; Cloudflare/Spin resolve from bindings — confirm boot-time access in Phase 3 scoping.
80+
81+
**D4 — One typed `Settings` as the AppConfig root.** Replace `TrustedServerAppConfig` (wrapper with empty `SECRET_FIELDS`) by deriving `AppConfig` directly on `Settings`, with `#[secret]` on the real secret fields (Phase 3). Removes a transitional indirection.
82+
83+
---
84+
85+
## 5. Phases
86+
87+
### Phase 0 — EdgeZero prerequisites (external, edgezero repo)
88+
89+
**Owner:** edgezero. **Tracked by:** its own spec + PR (link to be added).
90+
**Delivers:** (A) `State<T>` extractor + `RouterBuilder::with_state`; (B) nested/array `#[secret]` in `#[derive(AppConfig)]` + path-aware `secret_walk`.
91+
**Blocks:** Phase 3 (B), Phase 4 (A). **This umbrella consumes it as a versioned dependency** — bump the pinned `edgezero` rev once merged.
92+
93+
---
94+
95+
### Phase 1 — Stores onto EdgeZero `StoreRegistry`
96+
97+
**Goal:** delete trusted-server's bespoke config/secret store layer; route all store access through EdgeZero `ConfigStore` / `SecretStore` / `StoreRegistry` (KV is already there).
98+
99+
**Changes:**
100+
- Replace `PlatformConfigStore` / `PlatformSecretStore` (`platform/traits.rs`, `types.rs`) and the `RuntimeServices` config/secret fields with EdgeZero `ConfigStoreHandle` / `BoundSecretStore` resolved from the per-request registries (`ConfigRegistry` / `SecretRegistry`), matching how KV already works.
101+
- Migrate core secret consumers to `secrets.named(id)?.require_str(key)` / config consumers to the config binding: `proxy.rs` (S3), `request_signing/{signing,rotation}.rs`, `integrations/datadome/{protection,protection_scope}.rs`.
102+
- Delete the 4× per-adapter `platform.rs` config/secret store impls (`FastlyPlatformConfigStore`, `AxumPlatformConfigStore`, `NoopConfigStore`, `Cloudflare…`, and secret equivalents); adapters instead build `ConfigRegistry`/`SecretRegistry` via `dispatch_with_registries` from `[stores.*]` metadata.
103+
- Delete `FastlyManagementApiClient` (`management_api.rs`) — store writes/provisioning move to the EdgeZero CLI provision path.
104+
- Delete `settings_data.rs`'s `FastlyChunkPointer` resolver — EdgeZero's `FastlyConfigStore` resolves chunks transparently. `get_settings_from_config_store` collapses to `ConfigStore::get` + `settings_from_config_blob`.
105+
106+
**Deletions:** `management_api.rs`, `settings_data.rs` chunk resolver, `platform/traits.rs` config/secret traits, 4× `platform.rs` config/secret impls.
107+
**Keeps:** `RuntimeServices` as a shrinking bundle for the still-explicit-arg handlers (removed in Phase 4); `StoreName`/`StoreId` only where the CLI provisioning still needs the management-id split (revisit).
108+
**Acceptance:** all adapters build; `cargo test-fastly/-axum/-cloudflare/-spin` green; secret/config reads exercised in tests go through EdgeZero registries; parity test passes.
109+
110+
---
111+
112+
### Phase 2 — Finish config injection (no embedded `trusted-server.toml`)
113+
114+
**Goal:** every adapter loads app config from the EdgeZero config store; kill compile-time config baking and the legacy env overlay.
115+
116+
**Changes:**
117+
- Derive `AppConfig` on the config root (interim: still `TrustedServerAppConfig` until Phase 3 collapses it onto `Settings`) so all adapters use the same store-load path.
118+
- **Cloudflare** (`adapter-cloudflare/src/app.rs`) and **Spin** (`adapter-spin/src/app.rs`): replace `Settings::from_toml(include_str!(".../trusted-server.example.toml"))` with `get_settings_from_config_store(...)` (now the EdgeZero `ConfigStore` path from Phase 1). Seed each platform's config store (`wrangler.toml` / `runtime-config.toml` / `fastly.toml` local blocks) with the pushed blob.
119+
- Delete `Settings::from_toml_and_env`, `ENVIRONMENT_VARIABLE_PREFIX/SEPARATOR`, and the `config` **dev-dependency**. Any remaining env overlay uses EdgeZero's `EDGEZERO__*` / AppConfig `<APP>__…` layers.
120+
121+
**Deletions:** both `include_str!` config paths, `from_toml_and_env`, `config` crate dep.
122+
**Acceptance:** Cloudflare + Spin serve with store-loaded config (no baked TOML); `ts config push` blob is the single source on all four adapters; tests green.
123+
124+
---
125+
126+
### Phase 3 — Secret externalization (full)
127+
128+
**Goal:** no app-level secret is stored inside the config blob; secrets live in the EdgeZero secret store and resolve at startup (D3).
129+
130+
**Depends on:** Phase 0 (B) nested `#[secret]`, Phase 2.
131+
132+
**Changes:**
133+
- Collapse `TrustedServerAppConfig` onto `Settings` (D4): `#[derive(AppConfig)]` on `Settings`, `#[secret]` / `#[secret(store_ref)]` on the real secret fields (S3 keys, request-signing key refs, DataDome server-side key, integration API keys, etc.), including the **nested** ones enabled by Phase 0.
134+
- Audit `Settings` for the secret inventory **before implementation** — this settles Phase 0's open question B-1 (are any secrets inside arrays?). Feed the answer back to the edgezero PR.
135+
- Delete `Redacted<T>` and its manual redaction handling; `#[secret]` + the secret store replace it.
136+
- Operator migration: `ts` provisions secrets into the secret store (via EdgeZero provision), and a migration guide moves existing inline secrets out of `trusted-server.toml`. `reject_placeholder_secrets` becomes a check on the resolved values at boot.
137+
- Startup load resolves `#[secret]` fields against the secret store (D1/D3), then validates.
138+
139+
**Deletions:** inline secrets in the blob, `Redacted<T>`, `SECRET_FIELDS = &[]` wrapper.
140+
**Acceptance:** pushed blob contains only secret **key names**; boot resolves them; a config with a nested secret validates and serves; operator migration guide published; tests green.
141+
142+
---
143+
144+
### Phase 4 — Handlers → extractors
145+
146+
**Goal:** core route handlers become `#[action]` functions taking `FromRequest` extractors; per-adapter handler shims deleted.
147+
148+
**Depends on:** Phase 0 (A) `State<T>`; cleaner after Phase 1.
149+
150+
**Changes:**
151+
- Introduce `State<Arc<AppState>>` (or narrower `State<Arc<Settings>>` / `State<Arc<AuctionOrchestrator>>` / `State<Arc<IntegrationRegistry>>`) wired via `RouterBuilder::with_state` in each adapter's `Hooks::routes()`.
152+
- Rewrite core `handle_*` (`proxy.rs`, `publisher.rs`, `auction/endpoints.rs`, `request_signing/endpoints.rs`, `ec/*.rs`) from `(&Settings, &RuntimeServices, Request<EdgeBody>)` to `#[action]` signatures using `State<…>`, `Json`/`Query`/`Path`/`Headers`/`Host`, and the store extractors (`Kv`, `Secrets`, `Config`).
153+
- Delete the per-adapter shims (`execute_handler`/`execute_named`/`named_route_handler` + `NamedRouteHandler` enums) and shrink/retire `RuntimeServices` (its store fields already gone in Phase 1; remaining bundle folds into `State` + extractors).
154+
- **EC lifecycle & pre-route filters** (`build_ec_request_state`, `run_pre_route_filters`, `attach_dispatch_extensions`, `FinalizeResponseMiddleware`) are cross-cutting — keep them as **middleware**, not per-arg extractors.
155+
- **Phase 4b (optional follow-up, D2):** migrate the integration proxy nested router / `IntegrationProxy::handle` onto `RouterService` + extractors. Deferred by default.
156+
157+
**Deletions:** per-adapter handler shims, `NamedRouteHandler` enums, `RuntimeServices` (final form).
158+
**Acceptance:** all named routes served via `#[action]` handlers on all adapters; middleware carries EC lifecycle; parity test green.
159+
160+
---
161+
162+
### Phase 5 — Delete the legacy Fastly path (gated on 100% rollout)
163+
164+
**Goal:** remove the pre-EdgeZero Fastly entry path once the EdgeZero rollout is complete.
165+
166+
**Gate:** edgezero rollout at 100% (issue #495). Do not start until confirmed.
167+
168+
**Changes:**
169+
- Delete `legacy_main` / `route_request` (`adapter-fastly/src/main.rs`), `compat.rs` (fastly↔http shim), and the flag machinery (`edgezero_enabled`, `edgezero_rollout_pct`, `select_edgezero_entrypoint`, `should_route_to_edgezero`, IP-bucket hashing).
170+
- `main()` calls the EdgeZero path unconditionally (`run_app::<TrustedServerApp>` shape).
171+
- Retire the `trusted_server_config` rollout-flag reads.
172+
173+
**Deletions:** `legacy_main`, `route_request`, `compat.rs`, rollout flags.
174+
**Acceptance:** Fastly adapter has a single EdgeZero entry path; no rollout flags; full CI gate green; production traffic unaffected (already 100% on EdgeZero by gate definition).
175+
176+
---
177+
178+
## 6. Cruft deletion ledger (rolled into phases)
179+
180+
| Item | File(s) | Phase | Replaced by |
181+
|---|---|---|---|
182+
| Fastly chunk-pointer resolver | `core/src/settings_data.rs` | 1 | EdgeZero `FastlyConfigStore` + `chunked_config.rs` |
183+
| Bespoke config/secret store traits | `core/src/platform/{traits,types,mod}.rs` | 1 | EdgeZero `ConfigStore`/`SecretStore`/`StoreRegistry` |
184+
| 4× per-adapter store impls | `adapter-*/src/platform.rs` | 1 | per-adapter EdgeZero store impls |
185+
| Fastly management REST client | `adapter-fastly/src/management_api.rs` | 1 | EdgeZero CLI provision |
186+
| `include_str!` config baking | `adapter-{cloudflare,spin}/src/app.rs` | 2 | store-loaded config |
187+
| Legacy env overlay + `config` dep | `core/src/settings.rs` (`from_toml_and_env`, `ENVIRONMENT_VARIABLE_*`) | 2 | `EDGEZERO__*` / AppConfig env layers |
188+
| AppConfig wrapper w/ empty `SECRET_FIELDS` | `core/src/config.rs` | 3 | `#[derive(AppConfig)]` on `Settings` |
189+
| `Redacted<T>` | `core/src/redacted.rs` | 3 | `#[secret]` + secret store |
190+
| Per-adapter handler shims | `adapter-*/src/app.rs` | 4 | `#[action]` + extractors |
191+
| Legacy Fastly path + flags + compat | `adapter-fastly/src/{main.rs,compat.rs}` | 5 | single EdgeZero entry path |
192+
193+
**Explicitly NOT cruft (do not remove):** `migration_guards.rs` (intentional `fastly::` ban test), `s3_sigv4.rs` (AWS-domain canonical/hashing), `platform/image_optimizer.rs` (no EdgeZero equivalent yet), EC KV CAS wrapper (`ec/kv*.rs` — needs EdgeZero generation-CAS parity first; revisit, don't delete).
194+
195+
---
196+
197+
## 7. Risks & open questions
198+
199+
| ID | Question | Owner / resolution |
200+
|----|----------|--------------------|
201+
| R1 | Do any `Settings` secrets live inside **arrays**? | Phase 3 audit; feeds edgezero Phase 0 B-1. |
202+
| R2 | `StoreName` vs `StoreId` split — still needed after `management_api.rs` deletion? | Phase 1; drop if only the CLI provision path used it. |
203+
| R3 | EC identity API + Fastly rate limiter are Fastly-only today | Out of scope here; note as a portability follow-up (not blocking). |
204+
| R4 | Cloudflare/Spin boot-time secret-store access for D3 | Confirm in Phase 3 scoping. |
205+
| R5 | Config-change-requires-redeploy (D1) acceptable to operators? | Already true today; confirm no regression expectation. |
206+
| R6 | Phase 4 handler rewrite is large — split by route group? | Yes; per-implementation-plan, group by file (`proxy`, `auction`, `ec`, `request_signing`, `publisher`). |
207+
208+
---
209+
210+
## 8. Next step
211+
212+
Per phase, run `writing-plans` to produce an implementation plan **at phase start** (not upfront for all five) — the plan for Phase N should reflect the state left by Phase N-1. Begin with **Phase 1** once this umbrella is approved and the Phase 0 edgezero PR is merged (or Phase 1 can start immediately since it has no upstream dependency).

0 commit comments

Comments
 (0)