feat(notsecrets): add multi-provider secret resolution system
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
Add a config-driven, strongly-typed secret/env resolution system to notsecrets with 11 provider backends. The system supports explicit key bindings with typed SecretRef enums and a priority chain fallback. Tier 1 providers (implemented): Env, 1Password, dotenvx, SOPS, GSM Tier 2 providers (stubbed): nuenv, direnv, mise, Bitwarden, Vault, dotenvy New types: Provider, ProviderConfig, SecretRef, SecretsConfig, SecretsError, SecretSource, EnumerableSecretSource, SecretResolver Config lives in notsecrets.toml with typed serde deserialization. ISP split: EnumerableSecretSource for backends that support bulk enumeration, SecretSource for single-key lookup only. Includes conformance test suite, 33 new tests (161 workspace total). notstrap EnvInjector migration deferred to follow-up.
This commit is contained in:
92
crates/notsecrets/src/sources/gsm.rs
Normal file
92
crates/notsecrets/src/sources/gsm.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use crate::config::{Provider, SecretRef};
|
||||
use crate::error::SecretsError;
|
||||
use crate::ports::SecretSource;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct GsmSource {
|
||||
project: String,
|
||||
}
|
||||
|
||||
impl GsmSource {
|
||||
pub fn new(project: String) -> Self {
|
||||
Self { project }
|
||||
}
|
||||
}
|
||||
|
||||
impl SecretSource for GsmSource {
|
||||
fn name(&self) -> &str {
|
||||
"gsm"
|
||||
}
|
||||
|
||||
fn provider(&self) -> Provider {
|
||||
Provider::Gsm
|
||||
}
|
||||
|
||||
fn resolve(&self, _key: &str) -> Result<Option<String>, SecretsError> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn resolve_ref(
|
||||
&self,
|
||||
key: &str,
|
||||
secret_ref: &SecretRef,
|
||||
) -> Result<Option<String>, SecretsError> {
|
||||
let SecretRef::Gsm { path } = secret_ref else {
|
||||
return self.resolve(key);
|
||||
};
|
||||
let output = Command::new("gcloud")
|
||||
.args([
|
||||
"secrets",
|
||||
"versions",
|
||||
"access",
|
||||
"latest",
|
||||
"--secret",
|
||||
path,
|
||||
"--project",
|
||||
&self.project,
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| SecretsError::SourceError {
|
||||
name: "gsm".to_string(),
|
||||
source: e.into(),
|
||||
})?;
|
||||
if output.status.success() {
|
||||
Ok(Some(
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string(),
|
||||
))
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(SecretsError::SourceError {
|
||||
name: "gsm".to_string(),
|
||||
source: anyhow::anyhow!("gcloud secrets failed: {stderr}"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn gsm_source_name_and_provider() {
|
||||
let source = GsmSource::new("my-project".to_string());
|
||||
assert_eq!(source.name(), "gsm");
|
||||
assert_eq!(source.provider(), Provider::Gsm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gsm_source_resolve_without_ref_returns_none() {
|
||||
let source = GsmSource::new("my-project".to_string());
|
||||
let result = source.resolve("KEY").unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gsm_source_resolve_ref_wrong_variant_delegates() {
|
||||
let source = GsmSource::new("my-project".to_string());
|
||||
let wrong_ref = SecretRef::Env;
|
||||
let result = source.resolve_ref("KEY", &wrong_ref).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user