diff --git a/crates/edgezero-adapter-fastly/src/cli.rs b/crates/edgezero-adapter-fastly/src/cli.rs index 2c0d9bc1..c2f766d6 100644 --- a/crates/edgezero-adapter-fastly/src/cli.rs +++ b/crates/edgezero-adapter-fastly/src/cli.rs @@ -132,7 +132,7 @@ const FASTLY_INSTALL_HINT: &str = "install the Fastly CLI (https://www.fastly.co /// The config store the runtime opens for `EDGEZERO__*` overrides. Compute@Edge /// has no process env, so the runtime reads its config-store KEY selector from -/// here (see `env_config_from_runtime_dictionary` in lib.rs). +/// here (see `runtime_env_config` in lib.rs). const RUNTIME_ENV_STORE: &str = "edgezero_runtime_env"; /// Base name of the staging twin of [`RUNTIME_ENV_STORE`]. The actual store is @@ -546,12 +546,11 @@ impl Adapter for FastlyCliAdapter { // Store named `edgezero_runtime_env`. Compute@Edge has no // process env, so `EDGEZERO__STORES__CONFIG____KEY` and // similar overrides have to come from a platform Config Store - // the runtime opens by name (see - // `env_config_from_runtime_dictionary` in lib.rs). Provision - // owns the store creation alongside the operator's declared - // stores so the runtime override path is wired correctly out - // of the box; if the store already appears in - // `[setup.config_stores.edgezero_runtime_env]`, skip. + // the runtime opens by name (see `runtime_env_config` in + // lib.rs). Provision owns the store creation alongside the + // operator's declared stores so the runtime override path is + // wired correctly out of the box; if the store already appears + // in `[setup.config_stores.edgezero_runtime_env]`, skip. let runtime_env_kind = "config"; let runtime_env_name = "edgezero_runtime_env"; if dry_run { diff --git a/crates/edgezero-adapter-fastly/src/lib.rs b/crates/edgezero-adapter-fastly/src/lib.rs index 4df9ef7d..20335c80 100644 --- a/crates/edgezero-adapter-fastly/src/lib.rs +++ b/crates/edgezero-adapter-fastly/src/lib.rs @@ -25,7 +25,9 @@ pub mod response; pub mod secret_store; #[cfg(feature = "fastly")] -use edgezero_core::app::{Hooks, StoresMetadata}; +use edgezero_core::app::Hooks; +#[cfg(any(feature = "fastly", test))] +use edgezero_core::app::StoresMetadata; #[cfg(feature = "fastly")] use edgezero_core::env_config::EnvConfig; #[cfg(feature = "fastly")] @@ -139,7 +141,7 @@ where F: FnOnce(&fastly::Request, &mut Extensions), { let stores = A::stores(); - let env = env_config_from_runtime_dictionary(stores); + let env = runtime_env_config(stores); let logging = logging_from_env(&env); if logging.use_fastly_logger && !A::owns_logging() { let endpoint = logging.endpoint.as_deref().unwrap_or("stdout"); @@ -158,23 +160,24 @@ where } /// Build an [`EnvConfig`] from the optional `edgezero_runtime_env` -/// Fastly Config Store. Compute@Edge has no process env -- the -/// `EDGEZERO__*` runtime overrides spec 5.2/5.4 expects must come -/// from a Config Store the operator pre-populates (locally via -/// `fastly.toml`'s `[local_server.config_stores.edgezero_runtime_env]` -/// block; remotely via a `fastly config-store` named `edgezero_runtime_env`). +/// Fastly Config Store. /// -/// The Cloudflare adapter does the same thing through `env.var(...)` -/// (lib.rs:55) -- Workers also have no `std::env`. Mirroring the -/// approach here closes the spec 12.7 gap where `__KEY` runtime -/// overrides silently fell back to the binding's default id. +/// Compute@Edge has no process env, so the `EDGEZERO__*` runtime overrides +/// (logging settings, per-store platform names, the config-store `__KEY` +/// selector) come from a Config Store the operator pre-populates: locally via +/// `fastly.toml`'s `[local_server.config_stores.edgezero_runtime_env]` block, +/// remotely via a `fastly config-store` named `edgezero_runtime_env`. /// -/// If the store is missing or empty, returns an empty `EnvConfig` -- -/// the rest of the runtime then uses the baked-in defaults (which is -/// what the pre-fix code did, just without the env-driven override -/// path the spec promises). +/// If the store is missing or empty, returns an empty `EnvConfig` and the rest +/// of the runtime uses its baked-in defaults. +/// +/// [`run_app`] calls this itself. A custom Fastly entry point that bypasses +/// [`run_app`] should call it with its own `A::stores()` so staged and +/// overridden store selectors resolve identically. #[cfg(feature = "fastly")] -fn env_config_from_runtime_dictionary(stores: StoresMetadata) -> EnvConfig { +#[must_use] +#[inline] +pub fn runtime_env_config(stores: StoresMetadata) -> EnvConfig { use fastly::ConfigStore; use std::iter::empty; let Ok(dict) = ConfigStore::try_open("edgezero_runtime_env") else { @@ -194,6 +197,17 @@ fn env_config_from_runtime_dictionary(stores: StoresMetadata) -> EnvConfig { ); return EnvConfig::from_vars(empty::<(String, String)>()); }; + let vars = runtime_env_keys(stores) + .into_iter() + .filter_map(|key| dict.get(&key).map(|value| (key, value))); + EnvConfig::from_vars(vars) +} + +/// The `EDGEZERO__*` keys the Fastly runtime looks up: the fixed adapter and +/// logging settings, plus a `__NAME` selector for every declared store id and +/// a `__KEY` selector for config-store ids only. +#[cfg(any(feature = "fastly", test))] +fn runtime_env_keys(stores: StoresMetadata) -> Vec { let mut keys: Vec = vec![ "EDGEZERO__ADAPTER__HOST".to_owned(), "EDGEZERO__ADAPTER__PORT".to_owned(), @@ -217,10 +231,7 @@ fn env_config_from_runtime_dictionary(stores: StoresMetadata) -> EnvConfig { } } } - let vars = keys - .into_iter() - .filter_map(|key| dict.get(&key).map(|value| (key, value))); - EnvConfig::from_vars(vars) + keys } /// Dispatch with a config store wired explicitly. Use `run_app` for @@ -270,3 +281,50 @@ mod tests { assert!(logging.use_fastly_logger); } } + +#[cfg(test)] +mod runtime_env_key_tests { + use super::runtime_env_keys; + use edgezero_core::app::{StoreMetadata, StoresMetadata}; + + fn contains(keys: &[String], key: &str) -> bool { + keys.iter().any(|candidate| candidate.as_str() == key) + } + + #[test] + fn runtime_env_keys_name_every_store_and_key_only_config_stores() { + let stores = StoresMetadata { + config: Some(StoreMetadata { + default: "main", + ids: &["main", "edge"], + }), + kv: Some(StoreMetadata { + default: "cache", + ids: &["cache"], + }), + secrets: Some(StoreMetadata { + default: "vault", + ids: &["vault"], + }), + }; + + let keys = runtime_env_keys(stores); + + assert!(contains(&keys, "EDGEZERO__ADAPTER__HOST")); + assert!(contains(&keys, "EDGEZERO__ADAPTER__PORT")); + assert!(contains(&keys, "EDGEZERO__LOGGING__LEVEL")); + assert!(contains(&keys, "EDGEZERO__LOGGING__ENDPOINT")); + assert!(contains(&keys, "EDGEZERO__LOGGING__USE_FASTLY_LOGGER")); + assert!(contains(&keys, "EDGEZERO__LOGGING__ECHO_STDOUT")); + + assert!(contains(&keys, "EDGEZERO__STORES__CONFIG__MAIN__NAME")); + assert!(contains(&keys, "EDGEZERO__STORES__CONFIG__EDGE__NAME")); + assert!(contains(&keys, "EDGEZERO__STORES__KV__CACHE__NAME")); + assert!(contains(&keys, "EDGEZERO__STORES__SECRETS__VAULT__NAME")); + + assert!(contains(&keys, "EDGEZERO__STORES__CONFIG__MAIN__KEY")); + assert!(contains(&keys, "EDGEZERO__STORES__CONFIG__EDGE__KEY")); + assert!(!contains(&keys, "EDGEZERO__STORES__KV__CACHE__KEY")); + assert!(!contains(&keys, "EDGEZERO__STORES__SECRETS__VAULT__KEY")); + } +} diff --git a/scripts/smoke_test_config_key_override.sh b/scripts/smoke_test_config_key_override.sh index 244e82b2..f9663d75 100755 --- a/scripts/smoke_test_config_key_override.sh +++ b/scripts/smoke_test_config_key_override.sh @@ -151,7 +151,7 @@ upper() { # Seed the Fastly local config store `edgezero_runtime_env` with the # runtime override env vars. The Fastly Compute@Edge runtime has no # process env, so EDGEZERO__* overrides are read from this dedicated -# Config Store (see env_config_from_runtime_dictionary in +# Config Store (see runtime_env_config in # crates/edgezero-adapter-fastly/src/lib.rs). $1 is the fastly.toml # path; $2 is the per-row __KEY override value (empty -> no override). seed_fastly_runtime_env() {