orchestrator_config/
resource_store.rs1use crate::cli_types::ResourceMetadata;
4use crate::config::DEFAULT_PROJECT_ID;
5use crate::crd_types::CustomResource;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ApplyResult {
12 Created,
14 Configured,
16 Unchanged,
18}
19
20pub const SYSTEM_PROJECT: &str = "_system";
22
23pub fn is_project_scoped(kind: &str) -> bool {
25 matches!(
26 kind,
27 "Agent"
28 | "Workflow"
29 | "Workspace"
30 | "StepTemplate"
31 | "ExecutionProfile"
32 | "EnvStore"
33 | "SecretStore"
34 | "RuntimePolicy"
35 )
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct ResourceStore {
44 #[serde(default)]
45 resources: HashMap<String, CustomResource>,
46 #[serde(skip)]
47 generation: u64,
48}
49
50impl ResourceStore {
51 fn storage_key(kind: &str, metadata: &ResourceMetadata) -> String {
52 let project = metadata
53 .project
54 .as_deref()
55 .filter(|p| !p.trim().is_empty())
56 .unwrap_or(SYSTEM_PROJECT);
57 format!("{}/{}/{}", kind, project, metadata.name)
58 }
59
60 pub fn get(&self, kind: &str, name: &str) -> Option<&CustomResource> {
62 self.get_namespaced(kind, SYSTEM_PROJECT, name)
63 }
64
65 pub fn get_mut_by_key(&mut self, key: &str) -> Option<&mut CustomResource> {
67 self.resources.get_mut(key)
68 }
69
70 pub fn get_namespaced(&self, kind: &str, project: &str, name: &str) -> Option<&CustomResource> {
72 let key = format!("{}/{}/{}", kind, project, name);
73 self.resources.get(&key)
74 }
75
76 pub fn list_by_kind(&self, kind: &str) -> Vec<&CustomResource> {
78 let prefix = format!("{}/", kind);
79 self.resources
80 .iter()
81 .filter(|(k, _)| k.starts_with(&prefix))
82 .map(|(_, v)| v)
83 .collect()
84 }
85
86 pub fn list_by_kind_for_project(&self, kind: &str, project: &str) -> Vec<&CustomResource> {
88 let prefix = format!("{}/{}/", kind, project);
89 self.resources
90 .iter()
91 .filter(|(k, _)| k.starts_with(&prefix))
92 .map(|(_, v)| v)
93 .collect()
94 }
95
96 pub fn put(&mut self, mut cr: CustomResource) -> ApplyResult {
99 if is_project_scoped(&cr.kind)
101 && cr
102 .metadata
103 .project
104 .as_deref()
105 .filter(|p| !p.trim().is_empty())
106 .is_none()
107 {
108 cr.metadata.project = Some(DEFAULT_PROJECT_ID.to_string());
109 }
110 let key = Self::storage_key(&cr.kind, &cr.metadata);
111 self.generation += 1;
112
113 match self.resources.get(&key) {
114 None => {
115 self.resources.insert(key, cr);
116 ApplyResult::Created
117 }
118 Some(existing) => {
119 if existing.spec == cr.spec
120 && existing.api_version == cr.api_version
121 && existing.metadata == cr.metadata
122 {
123 ApplyResult::Unchanged
124 } else {
125 self.resources.insert(key, cr);
126 ApplyResult::Configured
127 }
128 }
129 }
130 }
131
132 pub fn remove(&mut self, kind: &str, name: &str) -> Option<CustomResource> {
134 self.remove_namespaced(kind, SYSTEM_PROJECT, name)
135 }
136
137 pub fn remove_first_by_kind_name(&mut self, kind: &str, name: &str) -> Option<CustomResource> {
140 let suffix = format!("/{}", name);
141 let prefix = format!("{}/", kind);
142 let key = self
143 .resources
144 .keys()
145 .find(|k| k.starts_with(&prefix) && k.ends_with(&suffix) && k.matches('/').count() == 2)
146 .cloned();
147 if let Some(key) = key {
148 let removed = self.resources.remove(&key);
149 if removed.is_some() {
150 self.generation += 1;
151 }
152 return removed;
153 }
154 None
155 }
156
157 pub fn remove_namespaced(
159 &mut self,
160 kind: &str,
161 project: &str,
162 name: &str,
163 ) -> Option<CustomResource> {
164 let key = format!("{}/{}/{}", kind, project, name);
165 let removed = self.resources.remove(&key);
166 if removed.is_some() {
167 self.generation += 1;
168 }
169 removed
170 }
171
172 pub fn remove_all_for_project(&mut self, project: &str) {
174 let pattern = format!("/{}/", project);
175 let before = self.resources.len();
176 self.resources.retain(|key, _| !key.contains(&pattern));
177 if self.resources.len() < before {
178 self.generation += 1;
179 }
180 }
181
182 pub fn generation(&self) -> u64 {
184 self.generation
185 }
186
187 pub fn is_empty(&self) -> bool {
189 self.resources.is_empty()
190 }
191
192 pub fn len(&self) -> usize {
194 self.resources.len()
195 }
196
197 pub fn resources(&self) -> &HashMap<String, CustomResource> {
199 &self.resources
200 }
201
202 pub fn resources_mut(&mut self) -> &mut HashMap<String, CustomResource> {
204 &mut self.resources
205 }
206}