diff --git a/.claude/skills/deploying-trusted-server-to-fastly/SKILL.md b/.claude/skills/deploying-trusted-server-to-fastly/SKILL.md index 08609e1929..681952b322 100644 --- a/.claude/skills/deploying-trusted-server-to-fastly/SKILL.md +++ b/.claude/skills/deploying-trusted-server-to-fastly/SKILL.md @@ -40,9 +40,18 @@ accounts and harmless for single-service ones, so prefer it by default. - **Always `ts config push --dry-run` first.** It prints the exact target physical store; confirm it is the service-scoped one, not a shared default, before writing. -- **Always pass `--service-id=`** to `fastly compute publish`. A checked-in - `fastly.toml` may pin a different (or dead) `service_id`; without the flag, - publish targets that one. +- **Never deploy with `fastly compute publish`.** This app declares Config, KV, + and Secret Stores, and only EdgeZero's managed deploy links each selected + physical store to the service version under its logical ID. Publishing with + the Fastly CLI clones the active version's links, so the new version is + missing the links the runtime opens: app config fails to load and every + publisher route returns 500 while `/health` still answers 200. Deploy with + `ts deploy --adapter fastly --service-id= --application-release `, + with `EDGEZERO_MANIFEST` set to the release's `edgezero.toml`, or EdgeZero's + `deploy-fastly` action. +- **Always pass `--service-id=`** to any deploy or publish command. A + checked-in `fastly.toml` may pin a different (or dead) `service_id`; without + the flag, the command targets that one. - **`ts config push` only upserts entries**; the target store must already exist (`fastly config-store create`) before pushing. - **Verify a shared store stayed untouched** by recording its `updated` diff --git a/AGENTS.md b/AGENTS.md index dd1dd52337..a86d5bf0a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,8 +58,9 @@ cargo build --package trusted-server-adapter-fastly --release --target wasm32-wa # Run locally with Fastly simulator fastly compute serve -# Deploy to Fastly -fastly compute publish +# Deploy to Fastly (managed path; links logical stores to the version) +EDGEZERO_MANIFEST="/edgezero.toml" \ + ts deploy --adapter fastly --service-id --application-release "" # Run Axum dev server (native — no Viceroy) cargo run -p trusted-server-adapter-axum diff --git a/Cargo.lock b/Cargo.lock index e915a0ea8b..796abdb4fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1498,15 +1498,19 @@ dependencies = [ [[package]] name = "edgezero-adapter" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", "toml", + "walkdir", ] [[package]] name = "edgezero-adapter-axum" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "anyhow", "async-trait", @@ -1534,7 +1538,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-cloudflare" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "anyhow", "async-trait", @@ -1557,7 +1561,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "anyhow", "async-stream", @@ -1586,7 +1590,7 @@ dependencies = [ [[package]] name = "edgezero-adapter-spin" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "anyhow", "async-trait", @@ -1613,7 +1617,7 @@ dependencies = [ [[package]] name = "edgezero-cli" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "chrono", "clap", @@ -1638,7 +1642,7 @@ dependencies = [ [[package]] name = "edgezero-core" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "anyhow", "async-compression", @@ -1669,7 +1673,7 @@ dependencies = [ [[package]] name = "edgezero-macros" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero?rev=683202c66948146ec360f490126f1d60f920288b#683202c66948146ec360f490126f1d60f920288b" +source = "git+https://github.com/stackpop/edgezero?tag=v0.0.9#f7176742d46aa4d1a04ce4588b606afda388fd30" dependencies = [ "log", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index f5c81ecf4b..a9aaedc991 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,12 +56,12 @@ cssparser = "0.36" derive_more = { version = "2.0", features = ["display", "error"] } directories = "5" ed25519-dalek = { version = "2.2", features = ["rand_core"] } -edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b", default-features = false } -edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b", default-features = false } -edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b", default-features = false } -edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b", default-features = false } -edgezero-cli = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b" } -edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "683202c66948146ec360f490126f1d60f920288b", default-features = false } +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9", default-features = false } +edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9", default-features = false } +edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9", default-features = false } +edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9", default-features = false } +edgezero-cli = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9" } +edgezero-core = { git = "https://github.com/stackpop/edgezero", tag = "v0.0.9", default-features = false } env_logger = "0.11" error-stack = "0.6" esi = "0.7.2" diff --git a/crates/trusted-server-adapter-fastly/src/app.rs b/crates/trusted-server-adapter-fastly/src/app.rs index d1da9b61fb..1d58c229e4 100644 --- a/crates/trusted-server-adapter-fastly/src/app.rs +++ b/crates/trusted-server-adapter-fastly/src/app.rs @@ -90,10 +90,8 @@ use std::sync::Arc; use crate::rate_limiter::{FastlyRateLimiter, RATE_COUNTER_NAME}; use edgezero_adapter_fastly::context::FastlyRequestContext; -use edgezero_adapter_fastly::runtime_env_config; use edgezero_core::app::{App, Hooks, StoreMetadata, StoresMetadata}; use edgezero_core::context::RequestContext; -use edgezero_core::env_config::EnvConfig; use edgezero_core::error::EdgeError; use edgezero_core::http::{ HandlerFuture, HeaderValue, Method, Request, Response, StatusCode, header, @@ -162,11 +160,19 @@ pub(crate) struct RuntimeStoreConfig { } impl RuntimeStoreConfig { - pub(crate) fn from_env(env: &EnvConfig) -> Self { + /// Store bindings for the Fastly runtime. + /// + /// Store selection is a deploy-time input; no store selector reaches the + /// Fastly runtime. `EdgeZero` links each selected physical store to the + /// service version under its logical ID, so the runtime opens stores by + /// logical ID and reads the config entry under that same ID for every + /// publication target. Staging isolation comes from linking a different + /// physical store, never from a different key. + pub(crate) fn logical() -> Self { Self { - config_store_name: StoreName::from(env.store_name("config", DEFAULT_CONFIG_STORE_ID)), - config_key: env.store_key("config", DEFAULT_CONFIG_STORE_ID), - secret_store_name: StoreName::from(env.store_name("secrets", DEFAULT_SECRET_STORE_ID)), + config_store_name: StoreName::from(DEFAULT_CONFIG_STORE_ID), + config_key: DEFAULT_CONFIG_STORE_ID.to_owned(), + secret_store_name: StoreName::from(DEFAULT_SECRET_STORE_ID), } } } @@ -1334,8 +1340,7 @@ impl Hooks for TrustedServerApp { } fn routes() -> RouterService { - let runtime_env = runtime_env_config(Self::stores()); - let stores = RuntimeStoreConfig::from_env(&runtime_env); + let stores = RuntimeStoreConfig::logical(); Self::router_with_state(&stores).0 } @@ -1376,7 +1381,6 @@ mod tests { use edgezero_core::app::Hooks as _; use edgezero_core::body::Body; use edgezero_core::context::RequestContext; - use edgezero_core::env_config::EnvConfig; use edgezero_core::http::{ HeaderValue, Method, Request, Response, StatusCode, header, request_builder, }; @@ -1447,32 +1451,8 @@ mod tests { } #[test] - fn runtime_store_config_maps_logical_store_names_and_config_key() { - let env = EnvConfig::from_vars([ - ( - "EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME", - "physical_config", - ), - ( - "EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY", - "active_config", - ), - ( - "EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME", - "ts_secrets", - ), - ]); - - let stores = RuntimeStoreConfig::from_env(&env); - - assert_eq!(stores.config_store_name.as_ref(), "physical_config"); - assert_eq!(stores.config_key, "active_config"); - assert_eq!(stores.secret_store_name.as_ref(), "ts_secrets"); - } - - #[test] - fn runtime_store_config_uses_logical_defaults_without_overrides() { - let stores = RuntimeStoreConfig::from_env(&EnvConfig::default()); + fn runtime_store_config_opens_logical_store_ids_and_key() { + let stores = RuntimeStoreConfig::logical(); assert_eq!(stores.config_store_name.as_ref(), "trusted_server_config"); assert_eq!(stores.config_key, "trusted_server_config"); diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index 1f2668b0a6..b8f814a01c 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -2,8 +2,6 @@ use std::sync::Arc; use edgezero_adapter_fastly::config_store::FastlyConfigStore as EdgeZeroFastlyConfigStore; use edgezero_adapter_fastly::request::into_core_request; -use edgezero_adapter_fastly::runtime_env_config; -use edgezero_core::app::Hooks as _; use edgezero_core::body::Body as EdgeBody; use edgezero_core::config_store::ConfigStoreHandle; use edgezero_core::error::EdgeError; @@ -325,8 +323,7 @@ fn heap_mib() -> String { /// Handles a request through the `EdgeZero` router path. fn edgezero_main(mut req: FastlyRequest, sandbox: &mut Sandbox, ordinal: u64, request_id: &str) { - let runtime_env = runtime_env_config(TrustedServerApp::stores()); - let runtime_stores = RuntimeStoreConfig::from_env(&runtime_env); + let runtime_stores = RuntimeStoreConfig::logical(); // Short-circuit the sandbox counters probe before app construction. It must // not build the application: polling it would otherwise increment the very diff --git a/crates/trusted-server-adapter-fastly/src/sandbox.rs b/crates/trusted-server-adapter-fastly/src/sandbox.rs index 228106047a..db825427a1 100644 --- a/crates/trusted-server-adapter-fastly/src/sandbox.rs +++ b/crates/trusted-server-adapter-fastly/src/sandbox.rs @@ -13,6 +13,8 @@ use std::time::Duration; use edgezero_core::app::App; use trusted_server_core::settings::Settings; +#[cfg(feature = "reusable-sandbox")] +use trusted_server_core::settings_data::DEFAULT_CONFIG_STORE_ID; use crate::app::AppState; @@ -258,7 +260,7 @@ pub(crate) fn resolve_mode(raw: RawLimits) -> ServeMode { }) } -/// Suffixes of the service-scoped runtime-environment keys holding the bounds. +/// Keys holding the sandbox bounds in the application's own config store. #[cfg(any(feature = "reusable-sandbox", test))] const KEY_MAX_REQUESTS: &str = "TS__SANDBOX__MAX_REQUESTS"; #[cfg(any(feature = "reusable-sandbox", test))] @@ -268,22 +270,6 @@ const KEY_TIMEOUT_MS: &str = "TS__SANDBOX__TIMEOUT_MS"; #[cfg(any(feature = "reusable-sandbox", test))] const KEY_MAX_MEMORY_MIB: &str = "TS__SANDBOX__MAX_MEMORY_MIB"; -/// Builds the service-scoped runtime-environment key for a bound. -/// -/// `EdgeZero`'s own `service_scoped_runtime_env_key` is private, so the shape is -/// reproduced here. It must stay identical to the one `edgezero provision` -/// writes. -/// -/// Follow-up: a public `EdgeZero` key-construction or lookup helper would let -/// this duplication go. Until such an API exists this implementation stays, so -/// the key shape has exactly one definition on our side. The `TS__SANDBOX__*` -/// suffixes and the limit-validation policy in [`resolve_mode`] are -/// application-owned either way and would not move. -#[cfg(any(feature = "reusable-sandbox", test))] -fn scoped_key(service_id: &str, suffix: &str) -> String { - format!("EDGEZERO__SERVICES__{service_id}__{suffix}") -} - /// Collects raw limits from a fallible key lookup. /// /// Separated from the config-store call so the failure modes are testable @@ -293,16 +279,15 @@ fn scoped_key(service_id: &str, suffix: &str) -> String { /// Diagnostics are returned rather than logged. This runs before the logger /// exists, so anything logged here would be discarded. #[cfg(any(feature = "reusable-sandbox", test))] -fn collect_raw_limits(service_id: &str, mut lookup: F) -> (RawLimits, Vec) +fn collect_raw_limits(mut lookup: F) -> (RawLimits, Vec) where E: core::fmt::Display, F: FnMut(&str) -> Result, E>, { let mut diagnostics = Vec::new(); - let mut read = |suffix: &str| -> Option { - let key = scoped_key(service_id, suffix); - match lookup(&key) { + let mut read = |key: &str| -> Option { + match lookup(key) { Ok(Some(raw)) => match raw.trim().parse::() { Ok(value) => Some(value), Err(e) => { @@ -332,75 +317,46 @@ where (limits, diagnostics) } -/// Reads the sandbox bounds from the runtime-environment config store. +/// Reads the sandbox bounds from the application's own config store. /// -/// These keys deliberately bypass `edgezero_adapter_fastly::runtime_env_config`. -/// That helper resolves a closed allowlist — adapter host and port, logging -/// settings, and per-store `__NAME`/`__KEY` selectors — and silently drops -/// everything else, so a sandbox key routed through it would always read as -/// absent and reuse would never engage. Still true at the pinned revision. +/// The bounds live beside the app config under the logical store ID that a +/// managed deployment links, so no deprecated selector store is involved. +/// `EdgeZero` removed `edgezero_runtime_env` when store selection moved to +/// deploy-time resource links, and nothing in this repository provisions it. +/// +/// These keys are read directly rather than through an `EdgeZero` environment +/// helper. Those resolve a closed allowlist, adapter host and port, logging +/// settings, and per-store `__NAME`/`__KEY` selectors, and silently drop +/// everything else, so a sandbox key routed through one would always read as +/// absent and reuse would never engage. /// /// Uses [`fastly::ConfigStore::try_get`], never `get`: `get` panics on a /// lookup error, and this runs in `main` before the health probe, so a panic /// here would take down liveness rather than merely disabling reuse. /// /// Any failure resolves to [`RawLimits::default`], and hence to -/// [`ServeMode::Single`]: an absent or unopenable store, an empty service id, -/// a failed lookup, or a value that does not parse as a `u64`. +/// [`ServeMode::Single`]: an unopenable store, a failed lookup, or a value +/// that does not parse as a `u64`. #[cfg(feature = "reusable-sandbox")] pub(crate) fn read_raw_limits() -> (RawLimits, Vec) { - use edgezero_adapter_fastly::RUNTIME_ENV_STORE_NAME; - - let Ok(store) = fastly::ConfigStore::try_open(RUNTIME_ENV_STORE_NAME) else { + let Ok(store) = fastly::ConfigStore::try_open(DEFAULT_CONFIG_STORE_ID) else { return ( RawLimits::default(), vec![format!( - "sandbox reuse disabled: config store `{RUNTIME_ENV_STORE_NAME}` unavailable" + "sandbox reuse disabled: config store `{DEFAULT_CONFIG_STORE_ID}` unavailable" )], ); }; - // Viceroy reports a service id of twenty-two zeros, which is a valid - // scope for key construction. Only an empty id is unusable. - let service_id = fastly::compute_runtime::service_id(); - if service_id.is_empty() { - return ( - RawLimits::default(), - vec!["sandbox reuse disabled: no service id available for key scoping".to_owned()], - ); - } - - collect_raw_limits(service_id, |key| store.try_get(key)) + collect_raw_limits(|key| store.try_get(key)) } #[cfg(test)] mod tests { - use std::cell::Cell; + use std::cell::{Cell, RefCell}; use super::*; - #[test] - fn scoped_key_matches_the_edgezero_shape() { - // Viceroy reports twenty-two zeros locally; it is a valid scope. - let service_id = "0000000000000000000000"; - - assert_eq!( - scoped_key(service_id, KEY_MAX_REQUESTS), - "EDGEZERO__SERVICES__0000000000000000000000__TS__SANDBOX__MAX_REQUESTS", - "should reproduce the service-scoped key edgezero writes" - ); - assert_eq!( - scoped_key(service_id, KEY_MAX_LIFETIME_MS), - "EDGEZERO__SERVICES__0000000000000000000000__TS__SANDBOX__MAX_LIFETIME_MS", - "should scope the lifetime bound the same way" - ); - assert_eq!( - scoped_key(service_id, KEY_TIMEOUT_MS), - "EDGEZERO__SERVICES__0000000000000000000000__TS__SANDBOX__TIMEOUT_MS", - "should scope the wait timeout the same way" - ); - } - fn full(max_requests: u64) -> RawLimits { RawLimits { max_requests: Some(max_requests), @@ -579,8 +535,7 @@ mod tests { fn a_failed_lookup_degrades_to_single_request_instead_of_panicking() { // `ConfigStore::get` panics on a lookup error and runs before the // health probe, so the fallible path must absorb the error. - let (limits, diagnostics) = - collect_raw_limits::("svc", |_key| Err(LookupFailed)); + let (limits, diagnostics) = collect_raw_limits::(|_key| Err(LookupFailed)); assert_eq!( limits, @@ -606,7 +561,7 @@ mod tests { #[test] fn an_unparseable_value_degrades_to_single_request() { - let (limits, diagnostics) = collect_raw_limits::("svc", |key| { + let (limits, diagnostics) = collect_raw_limits::(|key| { Ok(Some(if key.ends_with(KEY_MAX_REQUESTS) { "ten".to_owned() } else { @@ -655,7 +610,7 @@ mod tests { Some("4294967295"), Some("4294967296"), ] { - let (limits, _) = collect_raw_limits::("example-service", |key| { + let (limits, _) = collect_raw_limits::(|key| { Ok(if key.ends_with("TS__SANDBOX__MAX_MEMORY_MIB") { memory.map(str::to_owned) } else { @@ -671,9 +626,36 @@ mod tests { } } + #[test] + fn limits_are_looked_up_unscoped_in_the_app_config_store() { + // The bounds live in the application's own config store under the + // logical id a deployment links, so every lookup must use the bare + // key. Asserting on the constants alone would only fail if someone + // edited them and this test together, and the sibling tests match on + // `ends_with`, so a service-scoped or otherwise prefixed lookup would + // slip past them. Capture the exact keys requested instead. + let requested = RefCell::new(Vec::new()); + + let (_limits, _diagnostics) = collect_raw_limits::(|key| { + requested.borrow_mut().push(key.to_owned()); + Ok(None) + }); + + assert_eq!( + requested.borrow().as_slice(), + [ + KEY_MAX_REQUESTS, + KEY_MAX_MEMORY_MIB, + KEY_MAX_LIFETIME_MS, + KEY_TIMEOUT_MS, + ], + "should look up each sandbox bound by its unscoped app-config key" + ); + } + #[test] fn absent_keys_report_nothing_and_stay_single_request() { - let (limits, diagnostics) = collect_raw_limits::("svc", |_key| Ok(None)); + let (limits, diagnostics) = collect_raw_limits::(|_key| Ok(None)); assert_eq!( limits, @@ -688,7 +670,7 @@ mod tests { #[test] fn a_complete_store_enables_reuse_end_to_end() { - let (limits, diagnostics) = collect_raw_limits::("svc", |key| { + let (limits, diagnostics) = collect_raw_limits::(|key| { Ok(Some( if key.ends_with(KEY_MAX_REQUESTS) { "10" diff --git a/crates/trusted-server-cli/src/run.rs b/crates/trusted-server-cli/src/run.rs index f77ebb17d2..1639fd3cbd 100644 --- a/crates/trusted-server-cli/src/run.rs +++ b/crates/trusted-server-cli/src/run.rs @@ -593,7 +593,7 @@ mod tests { let Command::Config(ConfigCommand::Push(push)) = args.command else { panic!("expected config push command"); }; - assert!(push.staging, "should target the derived staging key"); + assert!(push.staging, "should target the staging publication flow"); Args::try_parse_from([ "ts", @@ -616,7 +616,7 @@ mod tests { }; assert!( diff.staging, - "should compare against the derived staging key" + "should compare against the staging publication flow" ); Args::try_parse_from([ diff --git a/crates/trusted-server-cli/tests/config_store_defaults.rs b/crates/trusted-server-cli/tests/config_store_defaults.rs index 23c5f86e9b..2d120855d0 100644 --- a/crates/trusted-server-cli/tests/config_store_defaults.rs +++ b/crates/trusted-server-cli/tests/config_store_defaults.rs @@ -133,8 +133,14 @@ fn config_push_resolves_the_physical_name_and_explicit_key() { ); } +// The canonical `__KEY` selector is the push destination when `--key` is absent. +// Under EdgeZero v0.0.8 the CLI ignored it and always wrote the logical id; +// v0.0.9 returns the selector verbatim so the pushed entry and the runtime +// that reads it cannot diverge. This test runs the Axum adapter: the Fastly +// runtime reads the logical id for every target, and the Fastly CLI rejects +// any other `__KEY` value, so the override only applies off Fastly. #[test] -fn config_push_does_not_use_the_runtime_key_override_without_the_key_flag() { +fn config_push_writes_the_runtime_key_override_without_the_key_flag() { let project = project(); let key_var = format!( "EDGEZERO__STORES__CONFIG__{}__KEY", @@ -145,7 +151,11 @@ fn config_push_does_not_use_the_runtime_key_override_without_the_key_flag() { let entries = stored_entries(&project); assert_eq!(entries.len(), 1); assert!( - entries.contains_key(CONFIG_BLOB_KEY), - "runtime-only key override should not move the CLI's write destination" + entries.contains_key("active_config"), + "the canonical key selector should be the push destination" + ); + assert!( + !entries.contains_key(CONFIG_BLOB_KEY), + "the logical id should not also be written" ); } diff --git a/crates/trusted-server-core/src/settings_data.rs b/crates/trusted-server-core/src/settings_data.rs index 07108fac64..2999eccf35 100644 --- a/crates/trusted-server-core/src/settings_data.rs +++ b/crates/trusted-server-core/src/settings_data.rs @@ -42,10 +42,10 @@ pub fn config_key(env: &EnvConfig) -> String { /// Resolves the `EdgeZero` app-config store name from the process environment. /// -/// Native adapters such as Axum use this wrapper. Fastly instead supplies an -/// `EnvConfig` populated from service-scoped entries in `edgezero_runtime_env` -/// and resolves the store name from that configuration. Without an override, -/// both paths use the manifest default logical store ID. +/// Native adapters such as Axum use this wrapper. Fastly reads no selector at +/// runtime: it opens the store under the logical ID that a managed deployment +/// links to the selected physical store. Without an override, this wrapper +/// also returns the manifest default logical store ID. #[must_use] pub fn default_config_store_name() -> StoreName { config_store_name(&EnvConfig::from_env()) @@ -53,10 +53,10 @@ pub fn default_config_store_name() -> StoreName { /// Resolves the app-config blob key from the process environment. /// -/// Native adapters such as Axum use this wrapper. Fastly resolves the key from -/// service-scoped entries in `edgezero_runtime_env` instead. A runtime `__KEY` -/// override must match `ts config push --key`; an ordinary push without that -/// flag writes at the logical store ID, regardless of the `__KEY` override. +/// Native adapters such as Axum use this wrapper and honour a `__KEY` +/// override. The Fastly runtime always reads the entry under the logical store +/// ID, and its CLI rejects any other `--key` or `__KEY` value, so no override +/// applies there. #[must_use] pub fn default_config_key() -> String { config_key(&EnvConfig::from_env()) diff --git a/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml b/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml index eae7220a84..551d275d98 100644 --- a/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml +++ b/crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml @@ -66,28 +66,23 @@ key = "api_key" data = "test-api-key" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "integration_admin_password" data = "integration-admin-password-32-bytes-ok" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "integration_proxy_secret" data = "integration-test-proxy-secret-32-bytes-ok" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "integration_ec_passphrase" data = "integration-test-ec-secret-padded-32" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "integration_partner_token_alpha" data = "integration-test-token-alpha-32-bytes-ok" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "integration_partner_token_bravo" data = "integration-test-token-bravo-32-bytes-ok" [local_server.config_stores] - [local_server.config_stores.edgezero_runtime_env] - format = "inline-toml" - [local_server.config_stores.edgezero_runtime_env.contents] - EDGEZERO__SERVICES__0000000000000000000000__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME = "ts_secrets" - # Generated integration configs inject the manifest-default app-config blob # into the store required by the Fastly entry point. # GENERATED_TRUSTED_SERVER_CONFIG_STORES diff --git a/crates/trusted-server-integration-tests/tests/common/config.rs b/crates/trusted-server-integration-tests/tests/common/config.rs index 0b793833f5..db4e011c5e 100644 --- a/crates/trusted-server-integration-tests/tests/common/config.rs +++ b/crates/trusted-server-integration-tests/tests/common/config.rs @@ -93,8 +93,7 @@ fn cloudflare_binding_json(binding: &serde_json::Value) -> TestResult { mod tests { const FASTLY_CONFIG: &str = include_str!("../../../../fastly.toml"); const VICEROY_TEMPLATE: &str = include_str!("../../fixtures/configs/viceroy-template.toml"); - const VICEROY_SECRET_STORE_MAPPING_KEY: &str = - "EDGEZERO__SERVICES__0000000000000000000000__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME"; + const LOGICAL_SECRET_STORE_ID: &str = "trusted_server_secrets"; #[test] fn local_fastly_config_defines_runtime_kv_stores() { @@ -121,26 +120,28 @@ mod tests { } #[test] - fn local_fastly_secret_store_mapping_is_service_scoped() { + fn local_fastly_secret_store_is_exposed_under_its_logical_id() { for (name, config) in [ ("fastly.toml", FASTLY_CONFIG), ("Viceroy integration template", VICEROY_TEMPLATE), ] { let parsed: toml::Value = toml::from_str(config).expect("should parse Fastly configuration"); - let runtime_env = - &parsed["local_server"]["config_stores"]["edgezero_runtime_env"]["contents"]; + let local_server = &parsed["local_server"]; - assert_eq!( - runtime_env[VICEROY_SECRET_STORE_MAPPING_KEY].as_str(), - Some("ts_secrets"), - "{name} should scope the secret-store mapping to Viceroy's service ID" + assert!( + local_server["secret_stores"][LOGICAL_SECRET_STORE_ID].is_array(), + "{name} should expose the secret store under its logical ID" + ); + assert!( + local_server["secret_stores"].get("ts_secrets").is_none(), + "{name} should not define the legacy physical secret store" ); assert!( - runtime_env - .get("EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME") + local_server["config_stores"] + .get("edgezero_runtime_env") .is_none(), - "{name} should not define the ignored unscoped secret-store mapping" + "{name} should not define the legacy runtime selector store" ); } } @@ -149,9 +150,9 @@ mod tests { fn local_fastly_config_defines_starter_secret_references() { let parsed: toml::Value = toml::from_str(FASTLY_CONFIG).expect("should parse root fastly.toml"); - let entries = parsed["local_server"]["secret_stores"]["ts_secrets"] + let entries = parsed["local_server"]["secret_stores"][LOGICAL_SECRET_STORE_ID] .as_array() - .expect("fastly.toml should define ts_secrets"); + .expect("fastly.toml should define trusted_server_secrets"); for key in [ "publisher_proxy_secret", diff --git a/crates/trusted-server-js/build.rs b/crates/trusted-server-js/build.rs index 0d4306f15c..a9148c8d93 100644 --- a/crates/trusted-server-js/build.rs +++ b/crates/trusted-server-js/build.rs @@ -24,12 +24,16 @@ const TEST_VAR: &str = "TSJS_TEST"; fn main() { // Cargo scans directories recursively, so these cover every TS source. + // Every path must exist: a missing entry makes Cargo rerun this script on + // every build, the opposite of the intent. for path in [ "lib/src", "lib/build-all.mjs", + "lib/build-prebid-external.mjs", "lib/package.json", "lib/package-lock.json", "lib/tsconfig.json", + "lib/vite.config.ts", ] { println!("cargo:rerun-if-changed={path}"); } diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 8b851bd82c..9466e1f3df 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -215,31 +215,61 @@ deploy. Adapter passthrough arguments must now follow a `--` separator; unknown flags before `--` (including the renamed-away `--stage`) are rejected at parse time rather than forwarded. This is a change: passthrough args previously worked without the separator, so existing runbooks and CI jobs that pass -adapter flags directly need the `--` added: +adapter flags directly need the `--` added. Trusted Server declares Config, KV, +and Secret Stores, so EdgeZero treats every Fastly deploy as managed and +requires a verified application release root via `--application-release`; a +bare `ts deploy --adapter fastly` without a release is accepted only for +store-free applications. The CLI loads `edgezero.toml` from the working +directory unless `EDGEZERO_MANIFEST` names another file, and it rejects a +manifest outside the release root, so select the release's own manifest. + +Managed deployment also requires a nonempty `FASTLY_API_TOKEN` in the process +environment; signing in with `ts auth login` or a Fastly CLI profile alone +does not supply the token used by the adapter's direct API calls. Export the +token before running: ```bash -ts deploy --adapter fastly --service-id --staging -ts deploy --adapter fastly -- --comment "release" -``` - -A staged deploy only redirects the staged version's config selector at the -`_staging` key — it does not copy the production config blob -there. Push the staged config before probing the staged version: +EDGEZERO_MANIFEST="/edgezero.toml" \ + ts deploy --adapter fastly --service-id --application-release "" --staging +EDGEZERO_MANIFEST="/edgezero.toml" \ + ts deploy --adapter fastly --service-id --application-release "" -- --comment "release" +``` + +`` is an extracted immutable application release. Build the +application CLI and Fastly package, then use EdgeZero's +`package-application-release-fastly` action to assemble the release archive. +The `deploy-fastly` action consumes that prebuilt archive, verifies and extracts +it, selects the manifest, and supplies the release root. See EdgeZero's GitHub +Actions deployment guide (`docs/guide/deploy-github-actions.md` in the EdgeZero +repository) for the producer and consumer workflow. + +A staged deploy selects the physical Config Store from the staging environment +and links it to the staged version under the logical store ID. The staged +runtime reads the `` key from that store. It does not copy +the production config blob there. Push the staged config before probing the +staged version: ```bash -ts config push --adapter fastly --staging -ts config diff --adapter fastly --staging -``` - -The staged version resolves its app-config key through the version-linked -`edgezero_runtime_env` store. After `ts config push --staging`, the staged -binary reads `_staging` while the active production version -continues to read the production key. +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config push --adapter fastly --staging +EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= \ + ts config diff --adapter fastly --staging +``` + +The config key is fixed on Fastly: production, staging, and local Viceroy all +read ``. `--staging` does not choose a different store or +key: the physical store is whatever `EDGEZERO__STORES__CONFIG____NAME` +resolves to in the shell or deploy job that runs the push, falling back to the +logical ID when it is unset. If production and staging select the same +physical Config Store, a staged push overwrites the production entry and +production instances load it on their next start. Give staging its own +physical Config Store and set its selector explicitly when pushing, as the +commands above do. `--staging` on `config push` / `config diff` writes and compares the -`_staging` key in the same store. It is mutually exclusive -with `--key`: the staging key is derived from the store's logical id, so an -explicit key would be written where nothing reads it. +`` key in the physical store selected by the current +environment. It is mutually exclusive with `--key`, and on Fastly any `--key` +other than the logical store ID is rejected for every target, staged or not. Inspect and verify deployments with the deploy lifecycle commands. All three are Fastly-only — the axum, cloudflare, and spin adapters reject them: diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 81d936ec2c..f39ad638c6 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -2825,125 +2825,37 @@ EdgeZero entry point. The former `edgezero_enabled` and `edgezero_rollout_pct` canary keys are no longer read. `[stores.config].default` in `edgezero.toml` supplies the logical config store -ID and default blob key, currently `trusted_server_config`. Fastly has no -process environment. Its entry point reads service-scoped overrides from the -`edgezero_runtime_env` Config Store before opening the app-config store: - -```mermaid -flowchart TD - A[Manifest default store ID] --> B[Resolve store name and blob key] - C[Service-scoped entries in edgezero_runtime_env] --> B - B --> D[Open the resolved resource-link name] - D --> E[Read the selected blob key from the linked physical store] -``` - -For this logical ID, the runtime selectors are: - -```text -EDGEZERO__SERVICES____STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME -EDGEZERO__SERVICES____STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY -``` - -The runtime ignores unscoped entries. Missing or blank selectors fall back to -the logical ID. A resource link must exist under the resolved name, not always -under `trusted_server_config`. - -### Initial setup with a service-specific store - -Fastly store names are account-level. Choose a physical name that is not used -by another service. The default physical name is safe only if the service owns -that store exclusively. - -Create the Fastly service and an editable service version before provisioning -non-default mappings. Select its ID through top-level `service_id` in -`fastly.toml` or `FASTLY_SERVICE_ID`. If both are set, they must agree. Do not -reuse the checked-in service ID for your deployment. Without a service ID, -provisioning rejects non-default mappings before creating resources. - -The following example is for initial setup before the service receives traffic. -Replace the service ID and choose your own physical store name: - -```bash -export FASTLY_SERVICE_ID="" -export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=example_config - -ts provision --adapter fastly --dry-run -ts provision --adapter fastly -``` - -Provisioning creates the stores and persists the selected name in the -service-scoped `edgezero_runtime_env` entry. Keep all intended store-name -overrides set when provisioning, including any [secret-store mapping](/guide/fastly#secret-stores). -Provisioning reconciles mappings for all declared stores, so omitting a previous -override can remove it. - -For an existing service, Fastly does not reapply `[setup]` entries. Follow the -provisioner's resource-link instructions. Both the app-config store and the -runtime-env store must be linked to the same editable version. For this example: - -```bash -fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \ - --resource-id --name example_config -fastly resource-link create --service-id "$FASTLY_SERVICE_ID" --version latest --autoclone \ - --resource-id --name edgezero_runtime_env - -ts config push --adapter fastly --dry-run -ts config push --adapter fastly -fastly compute publish --service-id "$FASTLY_SERVICE_ID" --version latest -``` - -Look up each store ID by its name before linking. Confirm the push dry run names -`example_config`, not the account-level default. Publish the application to the -linked version only after seeding its config store; a missing or invalid blob -makes application startup fail closed. Do not activate a new service's empty -version before uploading the application. If your deployment separates upload -from activation, activate the prepared version with -`fastly service-version activate --service-id "$FASTLY_SERVICE_ID" --version ` -only after both the code and config are ready. - -Keep the `__NAME` override in your deployment environment for **every subsequent -push**. The CLI reads its process environment, not the service's persisted -runtime mapping. Omitting the override can write to the wrong physical store. -Reject empty values in deployment scripts rather than relying on the fallback. - -For a live service, changing entries in its active `edgezero_runtime_env` store -changes runtime selection immediately, independently of service-version -activation. Do not use the initial-setup sequence to migrate a live mapping. -Prepare and seed the destination and make its resource link available to the -active version before switching the selector, or use an isolated staged runtime -configuration. - -An existing deployment may instead link a service-specific physical store under -the logical name `trusted_server_config`, with no runtime `__NAME` override. -That alias works, but the CLI still needs the physical-name override on every -push. Do not add a runtime override unless a link under the newly selected name -also exists. - -### Selecting another blob key - -A normal push writes at the logical store ID. A runtime `__KEY` override does -not change that write destination. To select another production key, first push -with `ts config push --adapter fastly --key `, then set the matching -service-scoped `__KEY` entry in `edgezero_runtime_env`. Changing that entry affects -the active service immediately. Do not point the production selector at a -staging key; staged deployments need their own runtime-env store. +ID, currently `trusted_server_config`. The Fastly runtime opens the store by +that logical ID and reads the entry under the same key on every target. Store +selection is a deploy-time input: a managed `ts deploy --adapter fastly` links +the physical store named by +`EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME` to the service version +under the logical ID. No selector is read from a Config Store at runtime, and +on Fastly the CLI rejects any `--key` or `__KEY` selector other than the +logical ID. + +Keep the `__NAME` selector set in the shell or deploy job for every push, so +`ts config push` writes into the same physical store the deployment links. +Isolate staging by selecting a different physical store. See +[Secret Stores](/guide/fastly#secret-stores) and the +[CLI lifecycle commands](/guide/cli#lifecycle-commands) for the full deploy and +staging flow. ### Local development -The repository's Viceroy configuration uses the default logical app-config name -and key. Clear production overrides for the local push: +The repository's Viceroy configuration uses the logical app-config store ID and +key. Clear any production key override for the local push: ```bash -env -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME \ - -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY \ +env -u EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__KEY \ ts config push --adapter fastly --local ``` -`--local` writes under `[local_server.config_stores.]` in the -tracked `fastly.toml`. If you customize Viceroy's service-scoped runtime selectors, -keep that local name and the pushed key aligned with them. Review the generated -diff and do not commit deployment-specific app-config entries. Credentials belong -in secret stores; the app-config blob contains their key references. +`--local` writes under `[local_server.config_stores.trusted_server_config]` in +the tracked `fastly.toml`, the logical store ID, regardless of any `__NAME` +selector. Review the generated diff and do not commit deployment-specific +app-config entries. Credentials belong in secret stores; the app-config blob +contains their key references. ### Rollback diff --git a/docs/guide/edgezero.md b/docs/guide/edgezero.md index ed9c4f2f2c..2107f95f56 100644 --- a/docs/guide/edgezero.md +++ b/docs/guide/edgezero.md @@ -16,8 +16,10 @@ version operations use the adapter commands declared in `edgezero.toml`. Configuration is startup state, not a live control plane. A successful push does not change a running instance until the target's restart or deployment path loads that value. Use `--staging` only with the corresponding staging -deployment flow: it writes `LOGICAL_ID_staging` in the same physical store, -while production continues to read the ordinary logical key. +deployment flow. On Fastly the entry key is always the logical store ID, so +staging isolation comes from selecting a different physical store with +`EDGEZERO__STORES__CONFIG____NAME`; see +[the CLI guide](/guide/cli#lifecycle-commands). ## Logical and physical stores diff --git a/docs/guide/fastly.md b/docs/guide/fastly.md index fa64280542..6cf1e5e38c 100644 --- a/docs/guide/fastly.md +++ b/docs/guide/fastly.md @@ -271,6 +271,16 @@ Used for storing public configuration (e.g., public keys, key metadata): fastly config-store create --name jwks_store ``` +Trusted Server's app config lives under logical store ID +`trusted_server_config`. Select its physical store in each deployment +environment, the same way as the secret store below. Production and staging +isolate their app config by selecting different physical stores, because the +config entry key is the logical ID on every target: + +```bash +export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME= +``` + ### Secret Stores Trusted Server keeps static app-config credentials under logical store ID @@ -281,33 +291,37 @@ runtime-managed store. Create or select the service before provisioning a non-default mapping. Set its ID in `fastly.toml` or `FASTLY_SERVICE_ID`; if both are set, they must agree. Provisioning rejects non-default mappings without a service ID. Set the physical -mapping before provisioning, alongside any config-store or KV-store overrides: +mapping in the selected deployment environment before provisioning and +deploying, alongside any config-store or KV-store overrides: ```bash export EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets ts provision --adapter fastly ``` -Provisioning creates or reuses the physical store and persists this runtime -mapping in Fastly Config Store `edgezero_runtime_env`, scoped to the current -Fastly service: - -```text -EDGEZERO__SERVICES____STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets -``` - -The runtime ignores legacy unscoped entries. The Fastly service must link both -`ts_secrets` and `edgezero_runtime_env` to the active service version. The -custom streaming entry point reads the service-scoped mapping before loading -app config, so every startup and reload resolves static credentials from -`ts_secrets` while the portable manifest continues to declare -`trusted_server_secrets`. - -The same runtime mapping mechanism applies to app-config stores. See -[Fastly runtime config stores](/guide/configuration#fastly-runtime-config-store) -for the initial provisioning and linking sequence, subsequent CLI pushes, and -precautions when changing a live mapping. A process-environment override alone -does not configure the Fastly runtime. +Provisioning creates or reuses the physical store. A managed deployment reads +the selector from its deployment environment and links the selected physical +store to the target service version under the logical ID +`trusted_server_secrets`. The runtime opens the store by that logical ID; no +selector is stored in a Config Store and no service ID appears in the variable +name. + +Each GitHub Environment or deploy process uses the same canonical names and may +select different physical resources. Production and staging can therefore run +identical package bytes against different stores. A staged deploy links the +staging environment's selected stores into only the staged version. Selected +resources must already exist before deployment. + +The custom streaming entry point opens `trusted_server_secrets` before loading +app config, so every startup and reload resolves static credentials from the +physical store linked under that ID while the portable manifest continues to +declare `trusted_server_secrets`. Local Viceroy configuration exposes the store +under the logical ID directly; see `[local_server.secret_stores]` in +`fastly.toml`. + +The app-config store is selected the same way, through +`EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME` in the deployment +environment. No runtime mapping is stored in a Config Store. Create the separate request-signing store when that feature is enabled: @@ -315,8 +329,56 @@ Create the separate request-signing store when that feature is enabled: fastly secret-store create --name signing_keys ``` -Do not copy the same app credential store under a second hardcoded -`trusted_server_secrets` Fastly link. Configure the mapping instead. +The deployment creates the `trusted_server_secrets` link itself from the +selected physical store. Do not create that link by hand. + +### Upgrading from EdgeZero v0.0.8 + +Services deployed before this change link the physical store directly, such as +`ts_secrets`, together with an `edgezero_runtime_env` Config Store. This binary +opens `trusted_server_secrets` instead, and only a managed deployment creates +that link. A plain `fastly compute publish` clones the active version's links, +so it activates a version whose link is missing: app config fails to load and +every publisher route returns 500 while `/health` keeps answering 200. + +When upgrading an existing service: + +- Deploy only through the managed path, so the `trusted_server_secrets` link + exists before activation: run + `ts deploy --adapter fastly --application-release ` with + `EDGEZERO_MANIFEST` set to the release's `edgezero.toml`, or use EdgeZero's + `deploy-fastly` action. +- Move each physical-name selector into the deploy environment before the + first managed deploy. Copy every service-scoped + `EDGEZERO__SERVICES____STORES______NAME` entry from + `edgezero_runtime_env` to the unscoped + `EDGEZERO__STORES______NAME` variable. An unset selector falls + back to the logical ID, so the deploy links whichever account-level store + carries that name, which can belong to another service. +- Select a KV store for logical ID `trusted_server_kv`. The managed deploy + links every declared store, and this one is usually absent on Fastly. Set + `EDGEZERO__STORES__KV__TRUSTED_SERVER_KV__NAME` to an existing KV store or + run `ts provision --adapter fastly`; otherwise the deploy stops with + "selected Fastly KV store `trusted_server_kv` does not exist". +- Re-push staging app config under the logical key into the staging Config + Store, with `EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME` set to + that store. Entries previously pushed under `_staging` are + ignored. Do not rerun an existing `ts config push --adapter fastly --staging` + runbook unchanged: `--staging` no longer selects a separate key, so without + the `__NAME` selector it writes the production entry in the logical-ID store, + and production instances load it on their next start. +- Remove any `EDGEZERO__STORES__CONFIG____KEY` selectors. On Fastly the + CLI rejects any config key other than the logical store ID, so a leftover + selector makes `ts config push` and `ts deploy` fail until it is removed. +- Delete the stale `edgezero_runtime_env` link and any `*_staging` config + entries only after no active, staged, or rollback version depends on them. + A version-linked selector on an older staged version still reads + `_staging`, so removing the entry while that version is + deployed makes its publisher routes return 500. This release no longer + reads that store. If you set the optional `TS__SANDBOX__*` reuse bounds + there, move them into the app-config store under the logical ID + `trusted_server_config`, without the `EDGEZERO__SERVICES____` + prefix. ## Create EC KV Store @@ -340,7 +402,8 @@ passphrase = "ec_passphrase" ec_store = "ec_identity_store" ``` -Store the high-entropy passphrase under that key in `ts_secrets`. The resolved +Store the high-entropy passphrase under that key in the physical store +selected for `trusted_server_secrets`. The resolved value, rather than the key name, must contain at least 32 characters. Pipe the value over standard input so it never appears in the process argument list: @@ -378,7 +441,8 @@ Run the repository smoke from a clean shell: The script creates an isolated application config, applies its publisher-origin overrides, and runs strict validation. It then executes `ts config push --adapter fastly --local`, adds all three required entries to -`[local_server.secret_stores.ts_secrets]`, and starts `fastly compute serve` +`[local_server.secret_stores.trusted_server_secrets]`, and starts +`fastly compute serve` through Viceroy. The required keys are `handler_password`, `publisher_proxy_secret`, and `ec_passphrase`. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index d83b0cc863..a6ee362256 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -207,17 +207,31 @@ Restart or redeploy instances after secret rotation. See ## Deploy to Fastly +Trusted Server declares Config, KV, and Secret Stores, so deployment must go +through EdgeZero's managed path. That path links each selected physical store +to the service version under its logical ID; `fastly compute publish` clones +the active version's links instead and activates a version whose logical links +are missing. + ```bash -TRUSTED_SERVER__GIT_VERSION=v1.3.0 fastly compute publish +TRUSTED_SERVER__GIT_VERSION=v1.3.0 \ + EDGEZERO_MANIFEST="/edgezero.toml" \ + ts deploy --adapter fastly --service-id --application-release "" ``` +See [the Fastly guide](/guide/fastly#upgrading-from-edgezero-v0-0-8) when +upgrading a service that was deployed before logical store links, and +[the CLI guide](/guide/cli#lifecycle-commands) for where `` +comes from. + The build compiles `TRUSTED_SERVER__GIT_VERSION` into the binary and reports it -in the `x-ts-version` response header. The Fastly service version is in -`x-ts-fastly-version`. Without the variable, a build reports the checked-out -tag, else the branch, else the first 6 characters of the commit, and a build -outside a git checkout omits the header. The value is public on every -response, so pass a neutral value such as the commit hash when a branch name -is sensitive. +in the `x-ts-version` response header, so the variable must be set for the +build the deploy runs, not just for the activation step. The Fastly service +version is in `x-ts-fastly-version`. Without the variable, a build reports the +checked-out tag, else the branch, else the first 6 characters of the commit, +and a build outside a git checkout omits the header. The value is public on +every response, so pass a neutral value such as the commit hash when a branch +name is sensitive. ## Next Steps diff --git a/docs/superpowers/plans/2026-09-16-edgezero-fastly-store-selectors.md b/docs/superpowers/plans/2026-09-16-edgezero-fastly-store-selectors.md new file mode 100644 index 0000000000..7eff8e023b --- /dev/null +++ b/docs/superpowers/plans/2026-09-16-edgezero-fastly-store-selectors.md @@ -0,0 +1,333 @@ +# EdgeZero Fastly Store Selector Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restore Trusted Server staging deployments by consuming EdgeZero PR 381's canonical Fastly store-selector behavior and removing the superseded service-scoped configuration. + +**Architecture:** Pin every EdgeZero workspace dependency to the upstream PR branch so the CLI and adapters share one revision. Keep Trusted Server's strict runtime secret resolution, while moving the Fastly entry point, local Viceroy configuration, its regression test, and operator guidance to the logical-ID store model: EdgeZero links each physical store selected by a canonical `EDGEZERO__STORES__...` deployment selector under its logical ID, and the runtime opens stores by that ID. + +**Tech Stack:** Rust 1.95, Cargo Git dependencies, Fastly Compute/Viceroy TOML configuration, Markdown documentation, GitHub CLI. + +--- + +## File Map + +- `Cargo.toml`: select the EdgeZero PR 381 branch for all EdgeZero crates. +- `Cargo.lock`: lock all EdgeZero packages to the branch revision. +- `crates/trusted-server-adapter-fastly/src/main.rs`, `crates/trusted-server-adapter-fastly/src/app.rs`: open stores and the config entry by logical ID instead of reading `edgezero_runtime_env`. +- `fastly.toml`: expose the local Viceroy secret store under its logical ID and drop the runtime selector store. +- `crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml`, `scripts/template-cache-local-test.sh`: keep generated Viceroy configurations aligned with the root Fastly configuration. +- `crates/trusted-server-integration-tests/tests/common/config.rs`: regress the logical-ID store exposure in both Viceroy configurations. +- `docs/guide/fastly.md`: explain deploy-time canonical selector handling and physical store linking. +- `docs/guide/cli.md`: explain staging Config Store selection under the restored EdgeZero deployment model. +- `docs/superpowers/specs/2026-09-16-edgezero-fastly-store-selectors-design.md`: approved design, already committed. +- `docs/superpowers/plans/2026-09-16-edgezero-fastly-store-selectors.md`: this execution plan. + +### Task 1: Create the Tracking Issue + +**Files:** + +- No repository files. + +- [ ] **Step 1: Draft the issue** + +Use a bug title such as `Align Fastly staging store selectors with EdgeZero PR 381` and include: + +- the observed `publisher.proxy_secret` resolution failure; +- the impact that staging cannot build application state; +- the root cause that EdgeZero `v0.0.8` reads service-scoped selectors while the selected GitHub Environment supplies canonical selectors; +- the expected canonical key `EDGEZERO__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME=ts_secrets`; +- the dependency on `https://github.com/stackpop/edgezero/pull/381`; +- acceptance criteria covering the branch dependency, Viceroy fixtures, documentation, and validation. + +- [ ] **Step 2: Create the issue and record its number** + +Run: + +```bash +gh issue create --repo IABTechLab/trusted-server --title "Align Fastly staging store selectors with EdgeZero PR 381" --label bug --body-file +``` + +Expected: GitHub prints the new issue URL. Save its issue number for the pull request body. + +### Task 2: Expose Viceroy Stores Under Their Logical IDs + +**Files:** + +- Modify: `crates/trusted-server-integration-tests/tests/common/config.rs:48` +- Modify: `fastly.toml:56` +- Modify: `crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml:69` +- Modify: `scripts/template-cache-local-test.sh:372` + +- [ ] **Step 1: Change the regression test first** + +Replace `local_fastly_secret_store_mapping_is_service_scoped` with +`local_fastly_secret_store_is_exposed_under_its_logical_id`. Assert that both +parsed configurations define `[[local_server.secret_stores.trusted_server_secrets]]` +and define no `edgezero_runtime_env` Config Store. Point the starter secret +reference test at the logical ID as well. + +- [ ] **Step 2: Run the focused test and verify it fails** + +Run from `crates/trusted-server-integration-tests` using the host target: + +```bash +cargo test --test integration local_fastly --target aarch64-apple-darwin +``` + +Expected: FAIL because both TOML files still name the store `ts_secrets` and +still define `edgezero_runtime_env`. + +- [ ] **Step 3: Update the Viceroy configurations** + +In `fastly.toml`, the Viceroy template, and the template-cache script rename +`[[local_server.secret_stores.ts_secrets]]` to +`[[local_server.secret_stores.trusted_server_secrets]]`. Remove the +`[local_server.config_stores.edgezero_runtime_env]` table and its contents from +`fastly.toml` and the template. Adjust the nearby root `fastly.toml` comment to +say Viceroy exposes the store under its logical ID. + +- [ ] **Step 4: Run the focused test and verify it passes** + +Run the same focused `cargo test` command. + +Expected: PASS. + +- [ ] **Step 5: Commit the regression and fixtures** + +```bash +git add fastly.toml scripts/template-cache-local-test.sh crates/trusted-server-integration-tests/fixtures/configs/viceroy-template.toml crates/trusted-server-integration-tests/tests/common/config.rs +git commit -m "Expose Viceroy stores under their logical IDs" +``` + +### Task 3: Consume the EdgeZero PR Branch + +**Files:** + +- Modify: `Cargo.toml:59` +- Modify: `Cargo.lock` +- Modify: `crates/trusted-server-adapter-fastly/src/main.rs:92` +- Modify: `crates/trusted-server-adapter-fastly/src/app.rs:158` + +- [ ] **Step 1: Select the upstream branch consistently** + +Replace `tag = "v0.0.8"` on all six `edgezero-*` workspace dependencies with: + +```toml +branch = "fix/fastly-environment-store-selectors" +``` + +- [ ] **Step 2: Refresh the lockfile through a targeted CLI check** + +Run: + +```bash +cargo check --package trusted-server-cli --target aarch64-apple-darwin +``` + +Expected: Cargo fetches the branch, locks every EdgeZero package to the same Git +revision, and the Trusted Server CLI compiles. If upstream API changes cause a +compiler failure, make only the call-site changes required by that error and add +a focused parsing or behavior test before the production edit. + +The branch head removes `edgezero_adapter_fastly::runtime_env_config`. Replace +`RuntimeStoreConfig::from_env` with `RuntimeStoreConfig::logical()`: open +`trusted_server_config` and `trusted_server_secrets` by logical ID and read +the config entry under `trusted_server_config` for every target. Cover the +logical IDs and key with a unit test in `app.rs`. + +- [ ] **Step 3: Verify the lockfile revision is consistent** + +Run: + +```bash +rg -n -A3 'name = "edgezero-' Cargo.lock +``` + +Expected: all Git-sourced EdgeZero packages use the branch URL and the same +commit fragment; no EdgeZero entry remains on `tag=v0.0.8`. + +- [ ] **Step 4: Run the CLI test suite** + +Run: + +```bash +./scripts/test-cli.sh +``` + +Expected: all non-ignored tests pass. + +- [ ] **Step 5: Commit the dependency update** + +```bash +git add Cargo.toml Cargo.lock crates/trusted-server-adapter-fastly crates/trusted-server-cli +git commit -m "Consume canonical EdgeZero deployment selectors" +``` + +Only stage `crates/trusted-server-cli` if compiler-driven compatibility changes +were necessary. + +### Task 4: Align Operator Documentation + +**Files:** + +- Modify: `docs/guide/fastly.md:264` +- Modify: `docs/guide/cli.md:115` + +- [ ] **Step 1: Update Fastly store-selector guidance** + +Keep the canonical export example. Replace the statements that provisioning +persists a service-scoped key and that unscoped keys are ignored with the PR 381 +contract: + +- provisioning creates/reuses the physical store; +- a managed deployment reads canonical selectors from its selected environment + and links each selected physical store to the target version under its + logical ID; +- the runtime opens stores by logical ID; nothing is stored in + `edgezero_runtime_env`; +- staging links its selected physical stores into only the staged version; +- canonical environment-variable names contain no service ID; +- the selected physical resources must already exist. + +- [ ] **Step 2: Update staging CLI guidance** + +Clarify that `ts config push --staging` writes the logical key into the +physical Config Store selected by the staging environment, and that the staged +binary reads the same `` key there. State that production and +staging may select the same or different physical stores. + +- [ ] **Step 3: Check for stale service-scoped guidance** + +Run: + +```bash +rg -n 'EDGEZERO__SERVICES__|service-scoped mapping|ignored unscoped|edgezero_runtime_env' fastly.toml docs crates/trusted-server-integration-tests --glob '!docs/superpowers/**' +``` + +Expected: no matches outside archived design material. + +- [ ] **Step 4: Format documentation** + +Run: + +```bash +cd docs && npm run format +``` + +Expected: formatter exits successfully without unrelated changes. + +- [ ] **Step 5: Commit documentation** + +```bash +git add docs/guide/fastly.md docs/guide/cli.md +git commit -m "Document deploy-time Fastly store selection" +``` + +### Task 5: Verify the Complete Change + +**Files:** + +- Verify all modified files. + +- [ ] **Step 1: Run formatting checks** + +```bash +cargo fmt --all -- --check +cd docs && npm run format +``` + +Expected: both commands exit successfully. + +- [ ] **Step 2: Run target-matched Rust checks** + +```bash +cargo check-fastly +cargo check-axum +cargo check-cloudflare +cargo check-spin +``` + +Expected: all adapters compile with the selected EdgeZero branch. + +- [ ] **Step 3: Run target-matched tests** + +```bash +cargo test-fastly +cargo test-axum +cargo test-cloudflare +cargo test-spin +./scripts/test-cli.sh +cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test integration local_fastly +cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity +``` + +Expected: all tests pass. Run socket-binding tests with loopback access if the +sandbox blocks them. + +- [ ] **Step 4: Run target-matched Clippy** + +```bash +cargo clippy-fastly +cargo clippy-axum +cargo clippy-cloudflare +cargo clippy-cloudflare-wasm +cargo clippy-spin-native +cargo clippy-spin-wasm +``` + +Expected: every command exits successfully with warnings denied by the aliases. + +- [ ] **Step 5: Run JavaScript and documentation gates** + +```bash +(cd crates/trusted-server-js/lib && node build-all.mjs && npx vitest run && npm run format) +(cd docs && npm run format) +``` + +Expected: the JavaScript build and tests pass, and both formatters exit +successfully without unrelated changes. + +- [ ] **Step 6: Review the final diff** + +```bash +git diff origin/main...HEAD --check +git diff origin/main...HEAD --stat +git status --short +``` + +Expected: no whitespace errors, only planned files changed, and the worktree is +clean after final commits. + +### Task 6: Publish the Pull Request + +**Files:** + +- No additional repository files. + +- [ ] **Step 1: Push the isolated branch** + +```bash +git push -u origin fix/align-edgezero-pr-381 +``` + +- [ ] **Step 2: Draft the PR from the repository template** + +Use a title such as `Align Fastly staging store selectors with EdgeZero PR 381`. +Lead with the staging failure and resulting canonical store resolution. Include: + +- the EdgeZero branch dependency and upstream PR link; +- the canonical Viceroy fixtures and regression test; +- documentation changes; +- exact validation results; +- `Closes #`; +- a note that the branch pin will be replaced by the release tag after upstream + merges. + +- [ ] **Step 3: Create the pull request** + +```bash +gh pr create --repo IABTechLab/trusted-server --base main --head fix/align-edgezero-pr-381 --title "Align Fastly staging store selectors with EdgeZero PR 381" --body-file +``` + +Expected: GitHub prints the new pull request URL. diff --git a/docs/superpowers/specs/2026-09-16-edgezero-fastly-store-selectors-design.md b/docs/superpowers/specs/2026-09-16-edgezero-fastly-store-selectors-design.md new file mode 100644 index 0000000000..0dfa91648a --- /dev/null +++ b/docs/superpowers/specs/2026-09-16-edgezero-fastly-store-selectors-design.md @@ -0,0 +1,67 @@ +# EdgeZero Fastly Store Selector Alignment + +## Problem + +Trusted Server currently depends on EdgeZero `v0.0.8`, whose Fastly staging +deployment flow persists and reads service-scoped runtime store selectors. +Staging can therefore fall back to the logical secret-store name +`trusted_server_secrets` even when the selected GitHub Environment sets the +canonical selector to the physical store `ts_secrets`. Runtime settings then +fail while resolving required secret references such as +`publisher.proxy_secret`. + +EdgeZero PR 381 restores canonical deploy-time selectors and makes managed +deployments link each selected physical Config, KV, and Secret Store to the +target service version under its logical ID. The runtime opens stores by +logical ID and no longer reads selectors from an `edgezero_runtime_env` Config +Store; that store is legacy. Trusted Server needs to consume that behavior and +remove guidance for the superseded service-scoped and runtime-selector models. + +## Design + +All EdgeZero workspace dependencies are pinned with `tag = "v0.0.9"`, the +release that carries the merged PR 381 ("Bind Fastly stores per deployment +environment"). Pinning every crate to one tag ensures the adapter, CLI, core, +and platform-specific crates resolve to the same revision. This replaces the +interim `rev =` pin on the unmerged upstream branch and closes issue #1195. + +The Fastly entry point will stop reading `edgezero_runtime_env`. It will open +the config and secret stores by their logical IDs, `trusted_server_config` and +`trusted_server_secrets`, and read the config entry under +`trusted_server_config` for every target. Staging isolation comes from the +physical store the staging environment selects, not from a different key. + +Local Viceroy configuration will expose the secret store under the logical ID +`trusted_server_secrets` and drop the `edgezero_runtime_env` Config Store, so +local runs exercise the same store-opening path as a deployed version. + +The Fastly and CLI guides will describe GitHub Environment or deploy-process +variables as deployment inputs. They will no longer instruct operators to +construct a selector containing a Fastly service ID or to persist that key +manually. EdgeZero remains responsible for linking the selected physical stores +to the production or staged version during deployment. + +## Failure Handling + +Trusted Server will keep failing startup when a required secret reference +cannot be resolved. This change fixes the deployment mapping and resource links +rather than weakening runtime validation or falling back to another store. + +The pull request will state that its branch dependency is temporary and link to +EdgeZero PR 381. If upstream changes its CLI API before merge, Trusted Server's +compile and CLI tests will expose the mismatch before deployment. + +## Validation + +Validation will cover: + +- lockfile resolution of every EdgeZero crate to one upstream revision; +- Trusted Server CLI parsing and behavior tests on the host target; +- Fastly adapter compilation and tests where supported by the local toolchain; +- Rust formatting and target-matched Clippy for changed Rust surfaces; +- searches confirming service-scoped store-selector instructions and + `edgezero_runtime_env` references are removed from current operator + documentation and local Fastly configuration. + +The issue will record the staging failure, expected canonical selector behavior, +and the dependency on EdgeZero PR 381. The pull request will close that issue. diff --git a/edgezero.toml b/edgezero.toml index b40b20281b..64e6e0f1f0 100644 --- a/edgezero.toml +++ b/edgezero.toml @@ -48,7 +48,11 @@ auth-logout = "fastly profile delete" auth-status = "fastly profile list" build = "cargo build --bin trusted-server-adapter-fastly --release --target wasm32-wasip1 --color always" serve = "fastly compute serve" -deploy = "fastly compute publish" +# No `deploy` command: this app declares stores, so EdgeZero always takes the +# managed deployment path, which links each selected physical store to the +# service version under its logical ID. `fastly compute publish` would clone +# the active version's links and omit the logical links the runtime opens, so +# the key is omitted rather than left here for someone to copy into a runbook. # -- Axum (native dev server) ----------------------------------------------- diff --git a/fastly.toml b/fastly.toml index 9486bb82d2..19ec5a3627 100644 --- a/fastly.toml +++ b/fastly.toml @@ -51,31 +51,26 @@ build = """ # Local-only values for the three secret references in # trusted-server.example.toml. These are public development fixtures. - [[local_server.secret_stores.ts_secrets]] + # App-config references use logical `trusted_server_secrets`. Viceroy + # exposes the store under that ID, matching the resource link a + # deployment creates for the selected physical store. + [[local_server.secret_stores.trusted_server_secrets]] key = "publisher_proxy_secret" data = "local-only-publisher-proxy-secret" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "ec_passphrase" data = "local-only-ec-passphrase-32-bytes" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "handler_password" data = "local-only-handler-password" - [[local_server.secret_stores.ts_secrets]] + [[local_server.secret_stores.trusted_server_secrets]] key = "tinybird_auction_append_token" data = "test-tinybird-auction-append-token" - # App-config references use logical `trusted_server_secrets`; the - # edgezero_runtime_env mapping below resolves it to physical `ts_secrets`. - [local_server.config_stores] - [local_server.config_stores.edgezero_runtime_env] - format = "inline-toml" - [local_server.config_stores.edgezero_runtime_env.contents] - EDGEZERO__SERVICES__0000000000000000000000__STORES__SECRETS__TRUSTED_SERVER_SECRETS__NAME = "ts_secrets" - [local_server.config_stores.trusted_server_config] format = "inline-toml" [local_server.config_stores.trusted_server_config.contents] diff --git a/scripts/smoke-fastly.sh b/scripts/smoke-fastly.sh index c2e8b01b99..65635c2d23 100755 --- a/scripts/smoke-fastly.sh +++ b/scripts/smoke-fastly.sh @@ -31,7 +31,27 @@ trap cleanup EXIT INT TERM mkdir -p "$FASTLY_PROJECT" cp "$REPO_ROOT/edgezero.toml" "$EDGEZERO_MANIFEST" -cp "$REPO_ROOT/fastly.toml" "$FASTLY_MANIFEST" +# The base manifest defines the same secret keys this smoke appends. Drop its +# entries on copy, or removing an appended block would still leave a resolvable +# value and the missing-secret cases could not fail. +# The base manifest indents its table headers; the blocks this smoke appends +# are unindented. Match either form. +awk ' + /^[[:space:]]*\[\[local_server\.secret_stores\.trusted_server_secrets\]\][[:space:]]*$/ { + if ((getline key_line) <= 0 || (getline data_line) <= 0) { + exit 2 + } + removed++ + next + } + { print } + END { + if (removed < 1) { + print "expected at least one base secret block to strip; found 0" > "/dev/stderr" + exit 1 + } + } +' "$REPO_ROOT/fastly.toml" >"$FASTLY_MANIFEST" ln -s "$REPO_ROOT/crates" "$FASTLY_PROJECT/crates" smoke_resolve_ts_binary "$REPO_ROOT" @@ -90,7 +110,7 @@ write_without_secret() { local destination="$2" local missing_key="$3" awk -v missing_key="$missing_key" ' - $0 == "[[local_server.secret_stores.ts_secrets]]" { + /^[[:space:]]*\[\[local_server\.secret_stores\.trusted_server_secrets\]\][[:space:]]*$/ { header = $0 if ((getline key_line) <= 0 || (getline data_line) <= 0) { exit 2 @@ -131,15 +151,15 @@ run_case missing-config "$BASE_PORT" 500 \ ) cat >>"$FASTLY_MANIFEST" <