feat: WIP add secrets module and macro crate

This commit is contained in:
Jean-Gabriel Gill-Couture 2025-08-15 14:40:39 -04:00
parent 67f3a23071
commit 2a6a233fb2
11 changed files with 629 additions and 19 deletions

58
Cargo.lock generated
View File

@ -378,7 +378,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serde_urlencoded",
"thiserror 2.0.12",
"thiserror 2.0.14",
"tokio",
"tokio-util",
"tower-service",
@ -473,7 +473,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.14",
]
[[package]]
@ -1823,12 +1823,23 @@ dependencies = [
"temp-dir",
"temp-file",
"tempfile",
"thiserror 2.0.14",
"tokio",
"tokio-util",
"url",
"uuid",
]
[[package]]
name = "harmony-secrets-derive"
version = "0.1.0"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "harmony_cli"
version = "0.1.0"
@ -1962,7 +1973,7 @@ dependencies = [
"non-blank-string-rs",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.14",
]
[[package]]
@ -2621,7 +2632,7 @@ dependencies = [
"pest_derive",
"regex",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.14",
]
[[package]]
@ -2722,7 +2733,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml",
"thiserror 2.0.12",
"thiserror 2.0.14",
"tokio",
"tokio-tungstenite",
"tokio-util",
@ -2747,7 +2758,7 @@ dependencies = [
"serde",
"serde-value",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.14",
]
[[package]]
@ -2785,7 +2796,7 @@ dependencies = [
"pin-project",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 2.0.14",
"tokio",
"tokio-util",
"tracing",
@ -3366,7 +3377,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.12",
"thiserror 2.0.14",
"ucd-trie",
]
@ -3587,6 +3598,15 @@ dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@ -3720,7 +3740,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.12",
"thiserror 2.0.14",
]
[[package]]
@ -4016,7 +4036,7 @@ dependencies = [
"flurry",
"log",
"serde",
"thiserror 2.0.12",
"thiserror 2.0.14",
"tokio",
"tokio-util",
]
@ -4579,7 +4599,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.12",
"thiserror 2.0.14",
"time",
]
@ -4763,9 +4783,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.104"
version = "2.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619"
dependencies = [
"proc-macro2",
"quote",
@ -4899,11 +4919,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
dependencies = [
"thiserror-impl 2.0.12",
"thiserror-impl 2.0.14",
]
[[package]]
@ -4919,9 +4939,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
dependencies = [
"proc-macro2",
"quote",
@ -5251,7 +5271,7 @@ dependencies = [
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"thiserror 2.0.14",
"utf-8",
]

View File

@ -12,6 +12,7 @@ members = [
"harmony_cli",
"k3d",
"harmony_composer",
"harmony_secrets_derive",
]
[workspace.package]

View File

@ -67,6 +67,7 @@ bollard.workspace = true
tar.workspace = true
base64.workspace = true
once_cell = "1.21.3"
thiserror = "2.0.14"
[dev-dependencies]
pretty_assertions.workspace = true

BIN
harmony/harmony.rlib Normal file

Binary file not shown.

View File

@ -9,3 +9,4 @@ pub mod inventory;
pub mod maestro;
pub mod score;
pub mod topology;
pub mod secrets;

View File

@ -0,0 +1,265 @@
//! # Harmony Secrets Module
//!
//! This module provides core abstractions for type-safe secret management within the Harmony framework.
//!
//! ## Design Philosophy
//!
//! The design is centered around three key components:
//!
//! 1. **The `Secret` Trait:** This is the heart of the module. Instead of using strings to identify
//! secrets, we use dedicated, zero-sized types (ZSTs). Each ZST represents a single secret
//! and implements the `Secret` trait to provide metadata (like its namespace and key) and
//! its associated `Value` type. This enables full compile-time verification.
//!
//! 2. **The `Secrets` Struct:** This is the primary user-facing API. It provides the `get` and `set`
//! methods that are generic over any type implementing `Secret`. It's the high-level,
//! easy-to-use entry point for all secret operations.
//!
//! 3. **The `SecretStore` Trait:** This is the low-level backend interface. It defines the contract
//! for how the `Secrets` struct will interact with an actual storage system (like Infisical,
//! a local file, or a database). This decouples the high-level API from the implementation details.
//!
//! ## Example Usage
//!
//! ```
//! // In an external crate (e.g., harmony-okd):
//! use harmony_secrets::{Secret, StoreError};
//!
//! // 1. Define a zero-sized struct for each secret.
//! pub struct KubeadminPassword;
//!
//! // 2. Implement the `Secret` trait to provide metadata.
//! impl Secret for KubeadminPassword {
//! // The associated type defines what you get back.
//! type Value = String;
//!
//! const NAMESPACE: &'static str = "okd-installation";
//! const KEY: &'static str = "kubeadmin-password";
//! }
//!
//! // 3. Use it with the `Secrets` struct.
//! async fn example(secrets: &harmony_secrets::Secrets) -> Result<(), StoreError> {
//! // The API is type-safe. The compiler knows what `Value` to expect.
//! secrets.set::<KubeadminPassword>("password123".to_string()).await?;
//! let password = secrets.get::<KubeadminPassword>().await?;
//! assert_eq!(password, "password123");
//! Ok(())
//! }
//! ```
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
use thiserror::Error;
/// Defines the set of errors that can occur during secret operations.
/// Using `thiserror` provides a great developer experience for error handling.
#[derive(Debug, Error)]
pub enum StoreError {
#[error("Secret not found in store: namespace='{namespace}', key='{key}'")]
NotFound { namespace: String, key: String },
#[error("Permission denied for secret: namespace='{namespace}', key='{key}'")]
PermissionDenied { namespace: String, key: String },
#[error("Failed to deserialize secret value: {0}")]
Deserialization(String),
#[error("Failed to serialize secret value: {0}")]
Serialization(String),
#[error("A backend-specific error occurred: {0}")]
Backend(String),
}
/// A trait that marks a type as representing a single, retrievable secret.
///
/// This trait should be implemented on a unique, zero-sized struct for each secret
/// your module needs to manage. This pattern ensures that all secret access is
/// validated at compile time.
pub trait Secret: 'static + Send + Sync {
/// The data type of the secret's value. This ensures that `get` and `set`
/// operations are fully type-safe. The value must be serializable.
type Value: Serialize + DeserializeOwned + Send;
/// A logical grouping for secrets, similar to a Kubernetes namespace or a
/// directory path. This will be used by the `SecretStore` to organize data.
const NAMESPACE: &'static str;
/// The unique key for the secret within its namespace.
const KEY: &'static str;
}
/// The low-level storage trait that concrete secret backends must implement.
///
/// This trait operates on raw bytes (`Vec<u8>`), keeping it decoupled from any
/// specific serialization format. The `Secrets` struct will handle the
// serialization/deserialization boundary.
#[async_trait]
pub trait SecretStore: Send + Sync {
/// Retrieves the raw byte value of a secret from the backend.
async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError>;
/// Saves the raw byte value of a secret to the backend.
async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError>;
}
/// The primary, user-facing struct for interacting with secrets.
///
/// It provides a high-level, type-safe API that is decoupled from the
/// underlying storage mechanism.
#[derive(Clone)]
pub struct Secrets {
/// A shared, thread-safe reference to the underlying secret store.
store: Arc<dyn SecretStore>,
}
impl Secrets {
/// Creates a new `Secrets` instance with the given store implementation.
pub fn new(store: Arc<dyn SecretStore>) -> Self {
Self { store }
}
/// Retrieves a secret from the store in a fully type-safe manner.
///
/// The type of the secret to retrieve is specified using a generic parameter `S`,
/// which must implement the `Secret` trait. The method returns the `S::Value`
/// associated type, ensuring you always get the data type you expect.
///
/// # Example
/// `let admin_pass = secrets.get::<my_secrets::AdminPassword>().await?;`
pub async fn get<S: Secret>(&self) -> Result<S::Value, StoreError> {
let bytes = self
.store
.get(S::NAMESPACE, S::KEY)
.await
.map_err(|e| match e {
// Preserve the NotFound error for better diagnostics.
StoreError::NotFound { .. } => e,
_ => StoreError::Backend(e.to_string()),
})?;
// The public API uses JSON for serialization. It's robust and human-readable.
serde_json::from_slice(&bytes)
.map_err(|e| StoreError::Deserialization(e.to_string()))
}
/// Saves a secret to the store in a fully type-safe manner.
///
/// The method is generic over the secret type `S`, and the `value` parameter
/// must match the `S::Value` associated type, preventing type mismatch errors
/// at compile time.
///
/// # Example
/// `secrets.set::<my_secrets::AdminPassword>("new-password".to_string()).await?;`
pub async fn set<S: Secret>(&self, value: S::Value) -> Result<(), StoreError> {
let bytes = serde_json::to_vec(&value)
.map_err(|e| StoreError::Serialization(e.to_string()))?;
self.store
.set(S::NAMESPACE, S::KEY, bytes)
.await
.map_err(|e| StoreError::Backend(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
// Define a dummy secret for testing purposes.
struct TestApiKey;
impl Secret for TestApiKey {
type Value = String;
const NAMESPACE: &'static str = "global-tests";
const KEY: &'static str = "api-key";
}
struct ComplexSecret;
#[derive(Serialize, serde::Deserialize, PartialEq, Debug, Clone)]
struct ComplexValue {
user: String,
permissions: Vec<String>,
}
impl Secret for ComplexSecret {
type Value = ComplexValue;
const NAMESPACE: &'static str = "complex-tests";
const KEY: &'static str = "user-data";
}
// A mock implementation of the `SecretStore` that uses an in-memory HashMap.
#[derive(Default)]
struct MockStore {
data: Mutex<HashMap<String, Vec<u8>>>,
}
#[async_trait]
impl SecretStore for MockStore {
async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError> {
let path = format!("{}/{}", namespace, key);
let data = self.data.lock().unwrap();
data.get(&path)
.cloned()
.ok_or_else(|| StoreError::NotFound {
namespace: namespace.to_string(),
key: key.to_string(),
})
}
async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError> {
let path = format!("{}/{}", namespace, key);
let mut data = self.data.lock().unwrap();
data.insert(path, value);
Ok(())
}
}
#[tokio::test]
async fn test_set_and_get_simple_secret() {
let store = Arc::new(MockStore::default());
let secrets = Secrets::new(store);
let api_key_value = "secret-key-12345".to_string();
secrets
.set::<TestApiKey>(api_key_value.clone())
.await
.unwrap();
let retrieved_key = secrets.get::<TestApiKey>().await.unwrap();
assert_eq!(retrieved_key, api_key_value);
}
#[tokio::test]
async fn test_set_and_get_complex_secret() {
let store = Arc::new(MockStore::default());
let secrets = Secrets::new(store);
let complex_value = ComplexValue {
user: "test-user".to_string(),
permissions: vec!["read".to_string(), "write".to_string()],
};
secrets
.set::<ComplexSecret>(complex_value.clone())
.await
.unwrap();
let retrieved_value = secrets.get::<ComplexSecret>().await.unwrap();
assert_eq!(retrieved_value, complex_value);
}
#[tokio::test]
async fn test_get_nonexistent_secret() {
let store = Arc::new(MockStore::default());
let secrets = Secrets::new(store);
let result = secrets.get::<TestApiKey>().await;
assert!(matches!(result, Err(StoreError::NotFound { .. })));
if let Err(StoreError::NotFound { namespace, key }) = result {
assert_eq!(namespace, TestApiKey::NAMESPACE);
assert_eq!(key, TestApiKey::KEY);
}
}
}

View File

@ -22,12 +22,18 @@ pub struct OPNSenseFirewall {
host: LogicalHost,
}
// TODO figure out a design to have a unique identifiere for this firewall
// I think a project identifier would be good enough, then the secrets module configuration will
// point to the project's vault and this opnsense modules doesn't need to know anything about it
const OPNSENSE_CREDENTIALS: &str = "OPNSENSE_CREDENTIALS";
impl OPNSenseFirewall {
pub fn get_ip(&self) -> IpAddress {
self.host.ip
}
pub async fn new(host: LogicalHost, port: Option<u16>, username: &str, password: &str) -> Self {
// let credentials = Secrets::get_by_name(OPNSENSE_CREDENTIALS)
Self {
opnsense_config: Arc::new(RwLock::new(
opnsense_config::Config::from_credentials(host.ip, port, username, password).await,

View File

@ -0,0 +1,51 @@
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::Serialize;
use crate::{interpret::InterpretError, score::Score, topology::Topology};
/// Create and manage Tenant Credentials.
///
/// This is meant to be used by cluster administrators who need to provide their tenant users and
/// services with credentials to access their resources.
#[derive(Debug, Clone, Serialize)]
pub struct TenantCredentialScore;
impl<T: Topology + TenantCredentialManager> Score<T> for TenantCredentialScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
todo!()
}
fn name(&self) -> String {
todo!()
}
}
#[async_trait]
pub trait TenantCredentialManager {
async fn create_user(&self) -> Result<TenantCredentialBundle, InterpretError>;
}
#[derive(Debug, Clone)]
pub struct CredentialMetadata {
pub tenant_id: String,
pub credential_id: String,
pub description: String,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone)]
pub enum CredentialData {
/// Used to store login instructions destined to a human. Akin to AWS login instructions email
/// upon new console user creation.
PlainText(String),
}
pub struct TenantCredentialBundle {
_metadata: CredentialMetadata,
_content: CredentialData,
}
impl TenantCredentialBundle {}

View File

@ -0,0 +1,13 @@
[package]
name = "harmony-secrets-derive"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
proc-macro2 = "1.0"
proc-macro-crate = "3.3"
syn = "2.0"

View File

@ -0,0 +1,152 @@
use syn::DeriveInput;
use syn::parse_macro_input;
use proc_macro::TokenStream;
use proc_macro_crate::{FoundCrate, crate_name};
use quote::quote;
use syn::{
Ident, LitStr, Meta, Token, Type,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
/// A helper struct to parse the contents of the `#[secret(...)]` attribute.
/// This makes parsing robust and allows for better error handling.
struct SecretAttributeArgs {
namespace: LitStr,
key: LitStr,
value_type: Type,
}
impl Parse for SecretAttributeArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
// The attributes are parsed as a comma-separated list of `key = value` pairs.
let parsed_args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
let mut namespace = None;
let mut key = None;
let mut value_type = None;
for arg in parsed_args {
if let Meta::NameValue(nv) = arg {
let ident_str = nv.path.get_ident().map(Ident::to_string);
match ident_str.as_deref() {
Some("namespace") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
if let syn::Lit::Str(lit) = expr_lit.lit {
namespace = Some(lit);
continue;
}
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a string literal for `namespace`",
));
}
Some("key") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
if let syn::Lit::Str(lit) = expr_lit.lit {
key = Some(lit);
continue;
}
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a string literal for `key`",
));
}
Some("value_type") => {
if let syn::Expr::Lit(expr_lit) = nv.value {
// This is the key improvement: parse the string literal's content as a Type.
if let syn::Lit::Str(lit) = expr_lit.lit {
value_type = Some(lit.parse::<Type>()?);
continue;
}
}
// This allows for the improved syntax: `value_type = String`
if let syn::Expr::Path(expr_path) = nv.value {
value_type = Some(Type::Path(expr_path.into()));
continue;
}
return Err(syn::Error::new_spanned(
nv.value,
"Expected a type path (e.g., `String` or `Vec<u8>`) for `value_type`",
));
}
_ => {}
}
}
return Err(syn::Error::new_spanned(
arg,
"Unsupported attribute key. Must be `namespace`, `key`, or `value_type`.",
));
}
Ok(SecretAttributeArgs {
namespace: namespace.ok_or_else(|| {
syn::Error::new(input.span(), "Missing required attribute `namespace`")
})?,
key: key
.ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `key`"))?,
value_type: value_type.ok_or_else(|| {
syn::Error::new(input.span(), "Missing required attribute `value_type`")
})?,
})
}
}
#[proc_macro_derive(Secret, attributes(secret))]
pub fn derive_secret(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Ensure this is a unit struct (e.g., `struct MySecret;`)
if !matches!(&input.data, syn::Data::Struct(s) if s.fields.is_empty()) {
return syn::Error::new_spanned(
&input.ident,
"#[derive(Secret)] can only be used on unit structs.",
)
.to_compile_error()
.into();
}
// Find the `#[secret(...)]` attribute.
let secret_attr = input
.attrs
.iter()
.find(|attr| attr.path().is_ident("secret"))
.ok_or_else(|| syn::Error::new_spanned(&input.ident, "Missing `#[secret(...)]` attribute."))
.and_then(|attr| attr.parse_args::<SecretAttributeArgs>());
let args = match secret_attr {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
// Find the path to the `harmony_secrets` crate to make the macro work anywhere.
let secret_crate_path = match crate_name("harmony-secrets") {
Ok(FoundCrate::Itself) => quote!(crate),
Ok(FoundCrate::Name(name)) => {
let ident = Ident::new(&name, proc_macro2::Span::call_site());
quote!(::#ident)
}
Err(e) => {
return syn::Error::new(proc_macro2::Span::call_site(), e.to_string())
.to_compile_error()
.into();
}
};
let struct_ident = &input.ident;
let namespace = args.namespace;
let key = args.key;
let value_type = args.value_type;
let expanded = quote! {
impl #secret_crate_path::Secret for #struct_ident {
type Value = #value_type;
const NAMESPACE: &'static str = #namespace;
const KEY: &'static str = #key;
}
};
TokenStream::from(expanded)
}

View File

@ -0,0 +1,100 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Attribute, Meta};
use quote::quote;
use proc_macro_crate::crate_name;
#[proc_macro_derive(Secret, attributes(secret))]
pub fn derive_secret(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Verify this is a unit struct
if !matches!(&input.data, syn::Data::Struct(data) if data.fields.is_empty()) {
return syn::Error::new_spanned(
input.ident,
"#[derive(Secret)] only supports unit structs (e.g., `struct MySecret;`)",
)
.to_compile_error()
.into();
}
// Parse the #[secret(...)] attribute
let (namespace, key, value_type) = match parse_secret_attributes(&input.attrs) {
Ok(attrs) => attrs,
Err(e) => return e.into_compile_error().into(),
};
// Get the path to the harmony_secrets crate
let secret_crate_path = match crate_name("harmony-secrets") {
Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate),
Ok(proc_macro_crate::FoundCrate::Name(name)) => {
let ident = quote::format_ident!("{}", name);
quote!(::#ident)
}
Err(_) => {
return syn::Error::new_spanned(
&input.ident,
"harmony-secrets crate not found in dependencies",
)
.to_compile_error()
.into();
}
};
let struct_ident = input.ident;
TokenStream::from(quote! {
impl #secret_crate_path::Secret for #struct_ident {
type Value = #value_type;
const NAMESPACE: &'static str = #namespace;
const KEY: &'static str = #key;
}
})
}
fn parse_secret_attributes(attrs: &[Attribute]) -> syn::Result<(String, String, syn::Type)> {
let secret_attr = attrs
.iter()
.find(|attr| attr.path().is_ident("secret"))
.ok_or_else(|| {
syn::Error::new_spanned(
attrs.first().unwrap_or_else(|| &attrs[0]),
"missing #[secret(...)] attribute",
)
})?;
let mut namespace = None;
let mut key = None;
let mut value_type = None;
if let Meta::List(meta_list) = &secret_attr.parse_meta()? {
for nested in &meta_list.nested {
if let syn::NestedMeta::Meta(Meta::NameValue(nv)) = nested {
if nv.path.is_ident("namespace") {
if let syn::Lit::Str(lit) = &nv.lit {
namespace = Some(lit.value());
}
} else if nv.path.is_ident("key") {
if let syn::Lit::Str(lit) = &nv.lit {
key = Some(lit.value());
}
} else if nv.path.is_ident("value_type") {
if let syn::Lit::Str(lit) = &nv.lit {
value_type = Some(syn::parse_str::<syn::Type>(&lit.value())?);
}
}
}
}
}
Ok((
namespace.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `namespace` in #[secret(...)]")
})?,
key.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `key` in #[secret(...)]")
})?,
value_type.ok_or_else(|| {
syn::Error::new_spanned(secret_attr, "missing `value_type` in #[secret(...)]")
})?,
))
}