greentic_operator/
secrets_client.rs1use crate::dev_store_path;
2use anyhow::{Result as AnyhowResult, anyhow};
3use async_trait::async_trait;
4use greentic_secrets_lib::{
5 Result as SecretResult, SecretError, SecretsManager, SecretsStore,
6 core::{Error as CoreError, seed::DevStore},
7};
8use std::{
9 path::{Path, PathBuf},
10 sync::Arc,
11};
12
13pub struct SecretsClient {
14 store: Arc<DevStore>,
15 store_path: Option<PathBuf>,
16}
17
18impl SecretsClient {
19 pub fn open(bundle_root: &Path) -> AnyhowResult<Self> {
20 let override_path = dev_store_path::override_path();
21 if let Some(path) =
22 dev_store_path::find_existing_with_override(bundle_root, override_path.as_deref())
23 {
24 return Self::open_with_path(path);
25 }
26 let store_path = dev_store_path::ensure_path(bundle_root)?;
27 let store = DevStore::with_path(store_path.clone())
28 .map_err(|err| anyhow!("failed to open dev secrets store: {err}"))?;
29 Ok(Self {
30 store: Arc::new(store),
31 store_path: Some(store_path),
32 })
33 }
34
35 pub fn open_with_path(path: PathBuf) -> AnyhowResult<Self> {
36 let store = DevStore::with_path(path.clone())
37 .map_err(|err| anyhow!("failed to open dev secrets store: {err}"))?;
38 Ok(Self {
39 store: Arc::new(store),
40 store_path: Some(path),
41 })
42 }
43
44 pub fn store_path(&self) -> Option<&Path> {
45 self.store_path.as_deref()
46 }
47}
48
49#[async_trait]
50impl SecretsManager for SecretsClient {
51 async fn read(&self, path: &str) -> SecretResult<Vec<u8>> {
52 let result = self.store.get(path).await;
53 match result {
54 Ok(value) => Ok(value),
55 Err(CoreError::NotFound { entity }) => Err(SecretError::NotFound(entity)),
56 Err(err) => Err(SecretError::Backend(err.to_string().into())),
57 }
58 }
59
60 async fn write(&self, _: &str, _: &[u8]) -> SecretResult<()> {
61 Err(SecretError::Permission(
62 "dev secrets store is read-only".into(),
63 ))
64 }
65
66 async fn delete(&self, _: &str) -> SecretResult<()> {
67 Err(SecretError::Permission(
68 "dev secrets store is read-only".into(),
69 ))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use greentic_secrets_lib::{
77 SecretFormat, SeedDoc, SeedEntry, SeedValue,
78 core::seed::{ApplyOptions, DevStore, apply_seed},
79 };
80 use tempfile::tempdir;
81 use tokio::runtime::Runtime;
82
83 #[test]
84 fn reads_seeded_secret_from_dev_store() -> anyhow::Result<()> {
85 let dir = tempdir()?;
86 let store_path = dir.path().join("secrets.env");
87 let store = DevStore::with_path(store_path.clone())?;
88 let seed = SeedDoc {
89 entries: vec![SeedEntry {
90 uri: "secrets://demo/acme/_/mypack/my_secret".to_string(),
91 format: SecretFormat::Text,
92 value: SeedValue::Text {
93 text: "hello world".to_string(),
94 },
95 description: None,
96 }],
97 };
98 let runtime = Runtime::new()?;
99 let report =
100 runtime.block_on(async { apply_seed(&store, &seed, ApplyOptions::default()).await });
101 assert_eq!(report.ok, 1);
102 let client = SecretsClient::open_with_path(store_path.clone())?;
103 let value = runtime
104 .block_on(async { client.read("secrets://demo/acme/_/mypack/my_secret").await })?;
105 assert_eq!(value, b"hello world".to_vec());
106 Ok(())
107 }
108}