Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
Add store support for Spin adapter (KV, config, secret)#253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
32998df1022cd75bb8051d64e95d53dfa1c917ff578a4e227ac8facba0ec8e6cf57f58ec7dc14a905d5a11aa46258c7b6eb5129dac0011033b995e54644225ff0633bc9d1e258956abad0d6d60File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -6,6 +6,7 @@ bin/ | ||
| pkg/ | ||
| target/ | ||
| .wrangler/ | ||
| .spin/ | ||
| .edgezero/ | ||
| # env | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| //! Spin adapter config store: wraps `spin_sdk::variables`. | ||
| use edgezero_core::config_store::{ConfigStore, ConfigStoreError}; | ||
| /// Config store backed by Spin component variables. | ||
| pub struct SpinConfigStore { | ||
| inner: SpinConfigBackend, | ||
| } | ||
| enum SpinConfigBackend { | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| Spin, | ||
| #[cfg(test)] | ||
| InMemory(std::collections::HashMap<String, String>), | ||
| /// Never constructed; keeps the enum inhabited outside production Spin and tests. | ||
| #[cfg(not(any(all(feature = "spin", target_arch = "wasm32"), test)))] | ||
| _Uninhabited(std::convert::Infallible), | ||
| } | ||
| impl SpinConfigStore { | ||
| /// Create a new `SpinConfigStore` using the Spin variables API. | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| pub fn new() -> Self { | ||
| Self { | ||
| inner: SpinConfigBackend::Spin, | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
| fn from_entries(entries: impl IntoIterator<Item = (String, String)>) -> Self { | ||
| Self { | ||
| inner: SpinConfigBackend::InMemory(entries.into_iter().collect()), | ||
| } | ||
| } | ||
| } | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| impl Default for SpinConfigStore { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } | ||
| impl ConfigStore for SpinConfigStore { | ||
| fn get(&self, _key: &str) -> Result<Option<String>, ConfigStoreError> { | ||
| match &self.inner { | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| SpinConfigBackend::Spin => { | ||
| use spin_sdk::variables; | ||
prk-Jr marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| match variables::get(_key) { | ||
| Ok(value) => Ok(Some(value)), | ||
| Err(variables::Error::Undefined(_)) => Ok(None), | ||
| Err(variables::Error::InvalidName(msg)) => { | ||
| Err(ConfigStoreError::invalid_key(msg)) | ||
| } | ||
| Err(e) => Err(ConfigStoreError::unavailable(e.to_string())), | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
| SpinConfigBackend::InMemory(data) => Ok(data.get(_key).cloned()), | ||
| #[cfg(not(any(all(feature = "spin", target_arch = "wasm32"), test)))] | ||
| SpinConfigBackend::_Uninhabited(never) => match *never {}, | ||
| } | ||
| } | ||
| } | ||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| // These contract tests exercise the InMemory backend (not the real Spin | ||
| // variables API). Dotted keys such as "contract.key.a" are valid here but | ||
| // would trigger `InvalidName` on the real Spin backend, which requires | ||
| // lowercase variable names without dots. Real-backend behaviour is | ||
| // verified by the smoke tests in scripts/smoke_test_config.sh. | ||
| edgezero_core::config_store_contract_tests!(spin_config_store_contract, { | ||
| SpinConfigStore::from_entries([ | ||
| ("contract.key.a".to_string(), "value_a".to_string()), | ||
| ("contract.key.b".to_string(), "value_b".to_string()), | ||
| ]) | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| //! Spin KV store adapter. | ||
| //! | ||
| //! Wraps `spin_sdk::key_value::Store` to implement the | ||
| //! `edgezero_core::key_value_store::KvStore` trait. | ||
| //! | ||
| //! # Limitations | ||
| //! | ||
| //! - **TTL**: The Spin KV API has no TTL support. Calls to | ||
| //! `put_bytes_with_ttl` return `KvError::Validation` without writing. | ||
| //! - **Listing**: `spin_sdk::key_value::Store::get_keys()` returns all keys | ||
| //! with no prefix or cursor support. `list_keys_page` therefore returns | ||
| //! `KvError::Validation` instead of materializing the whole store. | ||
| //! | ||
| //! # Note | ||
| //! | ||
| //! This module is only compiled when the `spin` feature is enabled and the | ||
| //! target is `wasm32`. | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| use async_trait::async_trait; | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| use bytes::Bytes; | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| use edgezero_core::key_value_store::{KvError, KvPage, KvStore}; | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| use std::time::Duration; | ||
| /// KV store backed by the Spin KV API. | ||
| /// | ||
| /// Wraps a `spin_sdk::key_value::Store` handle obtained via | ||
| /// `Store::open(label)`. | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| pub struct SpinKvStore { | ||
| store: spin_sdk::key_value::Store, | ||
| } | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| impl SpinKvStore { | ||
| /// Open a Spin KV store by label. | ||
| /// | ||
| /// The `label` must match a `key_value_stores` entry in `spin.toml`. | ||
| /// Returns `KvError::Internal` if the store cannot be opened. | ||
| pub fn open(label: &str) -> Result<Self, KvError> { | ||
| let store = spin_sdk::key_value::Store::open(label) | ||
| .map_err(|e| KvError::Internal(anyhow::anyhow!("failed to open kv store: {e}")))?; | ||
| Ok(Self { store }) | ||
| } | ||
| /// Open the default EdgeZero KV store label (`"EDGEZERO_KV"`). | ||
| pub fn open_default() -> Result<Self, KvError> { | ||
| Self::open(edgezero_core::manifest::DEFAULT_KV_STORE_NAME) | ||
| } | ||
| } | ||
| #[cfg(all(feature = "spin", target_arch = "wasm32"))] | ||
| #[async_trait(?Send)] | ||
| impl KvStore for SpinKvStore { | ||
| async fn get_bytes(&self, key: &str) -> Result<Option<Bytes>, KvError> { | ||
| self.store | ||
| .get(key) | ||
| .map(|opt| opt.map(Bytes::from)) | ||
| .map_err(|e| KvError::Internal(anyhow::anyhow!("get failed: {e}"))) | ||
| } | ||
| async fn put_bytes(&self, key: &str, value: Bytes) -> Result<(), KvError> { | ||
| self.store | ||
| .set(key, value.as_ref()) | ||
| .map_err(|e| KvError::Internal(anyhow::anyhow!("set failed: {e}"))) | ||
| } | ||
| async fn put_bytes_with_ttl( | ||
prk-Jr marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| &self, | ||
| _key: &str, | ||
| _value: Bytes, | ||
| _ttl: Duration, | ||
| ) -> Result<(), KvError> { | ||
| Err(KvError::Validation( | ||
| "Spin KV does not support TTL; use put_bytes for non-expiring values".to_string(), | ||
| )) | ||
| } | ||
| async fn delete(&self, key: &str) -> Result<(), KvError> { | ||
| self.store | ||
| .delete(key) | ||
| .map_err(|e| KvError::Internal(anyhow::anyhow!("delete failed: {e}"))) | ||
| } | ||
| async fn exists(&self, key: &str) -> Result<bool, KvError> { | ||
| self.store | ||
| .exists(key) | ||
| .map_err(|e| KvError::Internal(anyhow::anyhow!("exists failed: {e}"))) | ||
| } | ||
| async fn list_keys_page( | ||
prk-Jr marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| &self, | ||
| _prefix: &str, | ||
| _cursor: Option<&str>, | ||
| _limit: usize, | ||
| ) -> Result<KvPage, KvError> { | ||
| Err(KvError::Validation( | ||
| "Spin KV key listing is unsupported because Store::get_keys() is unbounded".to_string(), | ||
| )) | ||
| } | ||
| } | ||
| // TODO: integration tests require the Spin runtime. | ||
| // Test `SpinKvStore` as part of a Spin E2E test suite. | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.