Skip to main content

agent_orchestrator/resource/
env_store.rs

1use crate::cli_types::{EnvStoreSpec, OrchestratorResource, ResourceKind, ResourceSpec};
2use crate::config::{EnvStoreConfig, OrchestratorConfig};
3use anyhow::{Result, anyhow};
4
5use super::{ApplyResult, RegisteredResource, Resource, ResourceMetadata};
6
7#[derive(Debug, Clone)]
8/// Builtin manifest adapter for non-sensitive `EnvStore` resources.
9pub struct EnvStoreResource {
10    /// Resource metadata from the manifest.
11    pub metadata: ResourceMetadata,
12    /// Manifest spec payload for the env store.
13    pub spec: EnvStoreSpec,
14}
15
16impl Resource for EnvStoreResource {
17    fn kind(&self) -> ResourceKind {
18        ResourceKind::EnvStore
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 = EnvStoreConfig {
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.env_stores,
37            self.name(),
38            incoming,
39        ))
40    }
41
42    fn to_yaml(&self) -> Result<String> {
43        super::manifest_yaml(
44            ResourceKind::EnvStore,
45            &self.metadata,
46            ResourceSpec::EnvStore(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            .env_stores
58            .get(name)
59            .map(|store| Self {
60                metadata: super::metadata_with_name(name),
61                spec: EnvStoreSpec {
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.env_stores.remove(name).is_some())
75            .unwrap_or(false)
76    }
77}
78
79/// Builds a typed `EnvStoreResource` from a generic manifest wrapper.
80pub(super) fn build_env_store(resource: OrchestratorResource) -> Result<RegisteredResource> {
81    let OrchestratorResource {
82        kind,
83        metadata,
84        spec,
85        ..
86    } = resource;
87    if kind != ResourceKind::EnvStore {
88        return Err(anyhow!("resource kind/spec mismatch for EnvStore"));
89    }
90    match spec {
91        ResourceSpec::EnvStore(spec) => Ok(RegisteredResource::EnvStore(EnvStoreResource {
92            metadata,
93            spec,
94        })),
95        _ => Err(anyhow!("resource kind/spec mismatch for EnvStore")),
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::resource::test_fixtures::make_config;
103
104    fn make_env_store(name: &str) -> EnvStoreResource {
105        EnvStoreResource {
106            metadata: super::super::metadata_with_name(name),
107            spec: EnvStoreSpec {
108                data: [("KEY".to_string(), "value".to_string())].into(),
109            },
110        }
111    }
112
113    #[test]
114    fn env_store_apply_and_get() {
115        let mut config = make_config();
116        let store = make_env_store("my-env");
117        assert_eq!(
118            store.apply(&mut config).expect("apply"),
119            ApplyResult::Created
120        );
121
122        let loaded =
123            EnvStoreResource::get_from(&config, "my-env").expect("env store should be present");
124        assert_eq!(loaded.spec.data.get("KEY").unwrap(), "value");
125        assert_eq!(loaded.kind(), ResourceKind::EnvStore);
126    }
127
128    #[test]
129    fn env_store_apply_unchanged() {
130        let mut config = make_config();
131        let store = make_env_store("es-unchanged");
132        assert_eq!(
133            store.apply(&mut config).expect("apply"),
134            ApplyResult::Created
135        );
136        assert_eq!(
137            store.apply(&mut config).expect("apply"),
138            ApplyResult::Unchanged
139        );
140    }
141
142    #[test]
143    fn env_store_delete() {
144        let mut config = make_config();
145        let store = make_env_store("es-del");
146        store.apply(&mut config).expect("apply");
147        assert!(EnvStoreResource::delete_from(&mut config, "es-del"));
148        assert!(EnvStoreResource::get_from(&config, "es-del").is_none());
149    }
150
151    #[test]
152    fn env_store_validate_rejects_empty_name() {
153        let store = make_env_store("");
154        assert!(store.validate().is_err());
155    }
156
157    #[test]
158    fn env_store_to_yaml() {
159        let store = make_env_store("yaml-es");
160        let yaml = store.to_yaml().expect("should serialize");
161        assert!(yaml.contains("EnvStore"));
162        assert!(yaml.contains("yaml-es"));
163    }
164
165    #[test]
166    fn env_store_get_from_returns_none_for_missing() {
167        let config = make_config();
168        assert!(EnvStoreResource::get_from(&config, "no-such").is_none());
169    }
170}