agent_orchestrator/resource/
secret_store.rs1use crate::cli_types::{OrchestratorResource, ResourceKind, ResourceSpec, SecretStoreSpec};
2use crate::config::{OrchestratorConfig, SecretStoreConfig};
3use anyhow::{Result, anyhow};
4
5use super::{ApplyResult, RegisteredResource, Resource, ResourceMetadata};
6
7#[derive(Debug, Clone)]
8pub struct SecretStoreResource {
10 pub metadata: ResourceMetadata,
12 pub spec: SecretStoreSpec,
14}
15
16impl Resource for SecretStoreResource {
17 fn kind(&self) -> ResourceKind {
18 ResourceKind::SecretStore
19 }
20
21 fn name(&self) -> &str {
22 &self.metadata.name
23 }
24
25 fn validate(&self) -> Result<()> {
26 super::validate_resource_name(self.name())?;
27 Ok(())
28 }
29
30 fn apply(&self, config: &mut OrchestratorConfig) -> Result<ApplyResult> {
31 let incoming = SecretStoreConfig {
32 data: self.spec.data.clone(),
33 };
34 let project = config.ensure_project(self.metadata.project.as_deref());
35 Ok(super::helpers::apply_to_map(
36 &mut project.secret_stores,
37 self.name(),
38 incoming,
39 ))
40 }
41
42 fn to_yaml(&self) -> Result<String> {
43 super::manifest_yaml(
44 ResourceKind::SecretStore,
45 &self.metadata,
46 ResourceSpec::SecretStore(self.spec.clone()),
47 )
48 }
49
50 fn get_from_project(
51 config: &OrchestratorConfig,
52 name: &str,
53 project_id: Option<&str>,
54 ) -> Option<Self> {
55 config
56 .project(project_id)?
57 .secret_stores
58 .get(name)
59 .map(|store| Self {
60 metadata: super::metadata_with_name(name),
61 spec: SecretStoreSpec {
62 data: store.data.clone(),
63 },
64 })
65 }
66
67 fn delete_from_project(
68 config: &mut OrchestratorConfig,
69 name: &str,
70 project_id: Option<&str>,
71 ) -> bool {
72 config
73 .project_mut(project_id)
74 .map(|project| project.secret_stores.remove(name).is_some())
75 .unwrap_or(false)
76 }
77}
78
79pub(super) fn build_secret_store(resource: OrchestratorResource) -> Result<RegisteredResource> {
81 let OrchestratorResource {
82 kind,
83 metadata,
84 spec,
85 ..
86 } = resource;
87 if kind != ResourceKind::SecretStore {
88 return Err(anyhow!("resource kind/spec mismatch for SecretStore"));
89 }
90 match spec {
91 ResourceSpec::SecretStore(spec) => {
92 Ok(RegisteredResource::SecretStore(SecretStoreResource {
93 metadata,
94 spec,
95 }))
96 }
97 _ => Err(anyhow!("resource kind/spec mismatch for SecretStore")),
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::resource::test_fixtures::make_config;
105
106 fn make_secret_store(name: &str) -> SecretStoreResource {
107 SecretStoreResource {
108 metadata: super::super::metadata_with_name(name),
109 spec: SecretStoreSpec {
110 data: [("API_KEY".to_string(), "sk-secret".to_string())].into(),
111 },
112 }
113 }
114
115 #[test]
116 fn secret_store_apply_and_get() {
117 let mut config = make_config();
118 let store = make_secret_store("my-secrets");
119 assert_eq!(
120 store.apply(&mut config).expect("apply"),
121 ApplyResult::Created
122 );
123
124 let loaded = SecretStoreResource::get_from(&config, "my-secrets")
125 .expect("secret store should be present");
126 assert_eq!(loaded.spec.data.get("API_KEY").unwrap(), "sk-secret");
127 assert_eq!(loaded.kind(), ResourceKind::SecretStore);
128
129 assert!(
131 config
132 .default_project()
133 .unwrap()
134 .secret_stores
135 .contains_key("my-secrets")
136 );
137 }
138
139 #[test]
140 fn secret_store_apply_unchanged() {
141 let mut config = make_config();
142 let store = make_secret_store("ss-unchanged");
143 assert_eq!(
144 store.apply(&mut config).expect("apply"),
145 ApplyResult::Created
146 );
147 assert_eq!(
148 store.apply(&mut config).expect("apply"),
149 ApplyResult::Unchanged
150 );
151 }
152
153 #[test]
154 fn secret_store_delete() {
155 let mut config = make_config();
156 let store = make_secret_store("ss-del");
157 store.apply(&mut config).expect("apply");
158 assert!(SecretStoreResource::delete_from(&mut config, "ss-del"));
159 assert!(SecretStoreResource::get_from(&config, "ss-del").is_none());
160 }
161
162 #[test]
163 fn secret_store_validate_rejects_empty_name() {
164 let store = make_secret_store("");
165 assert!(store.validate().is_err());
166 }
167
168 #[test]
169 fn secret_store_to_yaml() {
170 let store = make_secret_store("yaml-ss");
171 let yaml = store.to_yaml().expect("should serialize");
172 assert!(yaml.contains("SecretStore"));
173 assert!(yaml.contains("yaml-ss"));
174 }
175
176 #[test]
177 fn secret_store_get_from_returns_none_for_missing() {
178 let config = make_config();
179 assert!(SecretStoreResource::get_from(&config, "no-such").is_none());
180 }
181
182 #[test]
183 fn secret_store_and_env_store_same_name_coexist() {
184 use crate::cli_types::EnvStoreSpec;
185 use crate::resource::env_store::EnvStoreResource;
186
187 let mut config = make_config();
188
189 let env = EnvStoreResource {
191 metadata: super::super::metadata_with_name("shared"),
192 spec: EnvStoreSpec {
193 data: [("ENV_KEY".to_string(), "env_val".to_string())].into(),
194 },
195 };
196 env.apply(&mut config).expect("apply env");
197
198 let secret = make_secret_store("shared");
200 secret.apply(&mut config).expect("apply secret");
201
202 assert!(EnvStoreResource::get_from(&config, "shared").is_some());
204 assert!(SecretStoreResource::get_from(&config, "shared").is_some());
205 }
206}