winterbaume_cloudcontrol/
state.rs1use std::collections::HashMap;
2
3use chrono::Utc;
4use thiserror::Error;
5use uuid::Uuid;
6
7use crate::types::{ManagedResource, OperationStatus, OperationType, ResourceRequest};
8
9#[derive(Debug, Error)]
12pub enum CloudControlError {
13 #[error("Resource of type {type_name} with identifier {identifier} already exists.")]
14 AlreadyExists {
15 type_name: String,
16 identifier: String,
17 },
18 #[error("Resource of type {type_name} with identifier {identifier} not found.")]
19 ResourceNotFound {
20 type_name: String,
21 identifier: String,
22 },
23 #[error("A resource operation with the specified request token {token} was not found.")]
24 RequestTokenNotFound { token: String },
25 #[error("The specified extension {type_name} does not exist in the CloudFormation registry.")]
26 TypeNotFound { type_name: String },
27 #[error("{message}")]
28 InvalidRequest { message: String },
29 #[error(
30 "The resource operation request with token {token} cannot be cancelled because its status is {status}."
31 )]
32 NotCancellable { token: String, status: String },
33}
34
35#[derive(Debug, Default)]
36pub struct CloudControlState {
37 pub resources: HashMap<(String, String), ManagedResource>,
39 pub requests: HashMap<String, ResourceRequest>,
41}
42
43impl CloudControlState {
44 pub fn create_resource(
46 &mut self,
47 type_name: &str,
48 desired_state: &str,
49 ) -> Result<ResourceRequest, CloudControlError> {
50 let identifier = extract_identifier_from_model(desired_state)
52 .unwrap_or_else(|| Uuid::new_v4().to_string());
53
54 let key = (type_name.to_string(), identifier.clone());
55 if self.resources.contains_key(&key) {
56 return Err(CloudControlError::AlreadyExists {
57 type_name: type_name.to_string(),
58 identifier,
59 });
60 }
61
62 let resource = ManagedResource {
63 type_name: type_name.to_string(),
64 identifier: identifier.clone(),
65 resource_model: desired_state.to_string(),
66 };
67 self.resources.insert(key, resource);
68
69 let request_token = Uuid::new_v4().to_string();
70 let request = ResourceRequest {
71 request_token: request_token.clone(),
72 type_name: type_name.to_string(),
73 identifier: identifier.clone(),
74 operation: OperationType::Create,
75 operation_status: OperationStatus::Success,
76 event_time: Utc::now(),
77 resource_model: Some(desired_state.to_string()),
78 status_message: None,
79 error_code: None,
80 };
81 self.requests.insert(request_token, request.clone());
82
83 Ok(request)
84 }
85
86 pub fn delete_resource(
88 &mut self,
89 type_name: &str,
90 identifier: &str,
91 ) -> Result<ResourceRequest, CloudControlError> {
92 let key = (type_name.to_string(), identifier.to_string());
93 if self.resources.remove(&key).is_none() {
94 return Err(CloudControlError::ResourceNotFound {
95 type_name: type_name.to_string(),
96 identifier: identifier.to_string(),
97 });
98 }
99
100 let request_token = Uuid::new_v4().to_string();
101 let request = ResourceRequest {
102 request_token: request_token.clone(),
103 type_name: type_name.to_string(),
104 identifier: identifier.to_string(),
105 operation: OperationType::Delete,
106 operation_status: OperationStatus::Success,
107 event_time: Utc::now(),
108 resource_model: None,
109 status_message: None,
110 error_code: None,
111 };
112 self.requests.insert(request_token, request.clone());
113
114 Ok(request)
115 }
116
117 pub fn update_resource(
119 &mut self,
120 type_name: &str,
121 identifier: &str,
122 patch_document: &str,
123 ) -> Result<ResourceRequest, CloudControlError> {
124 let key = (type_name.to_string(), identifier.to_string());
125 let resource =
126 self.resources
127 .get_mut(&key)
128 .ok_or_else(|| CloudControlError::ResourceNotFound {
129 type_name: type_name.to_string(),
130 identifier: identifier.to_string(),
131 })?;
132
133 let mut model: serde_json::Value = serde_json::from_str(&resource.resource_model)
135 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
136 let patches: Vec<serde_json::Value> =
137 serde_json::from_str(patch_document).unwrap_or_default();
138 for patch in &patches {
139 apply_json_patch(&mut model, patch);
140 }
141 let updated_model = serde_json::to_string(&model).unwrap_or_default();
142 resource.resource_model = updated_model.clone();
143
144 let request_token = Uuid::new_v4().to_string();
145 let request = ResourceRequest {
146 request_token: request_token.clone(),
147 type_name: type_name.to_string(),
148 identifier: identifier.to_string(),
149 operation: OperationType::Update,
150 operation_status: OperationStatus::Success,
151 event_time: Utc::now(),
152 resource_model: Some(updated_model),
153 status_message: None,
154 error_code: None,
155 };
156 self.requests.insert(request_token, request.clone());
157
158 Ok(request)
159 }
160
161 pub fn get_resource(
163 &self,
164 type_name: &str,
165 identifier: &str,
166 ) -> Result<&ManagedResource, CloudControlError> {
167 let key = (type_name.to_string(), identifier.to_string());
168 self.resources
169 .get(&key)
170 .ok_or_else(|| CloudControlError::ResourceNotFound {
171 type_name: type_name.to_string(),
172 identifier: identifier.to_string(),
173 })
174 }
175
176 pub fn list_resources(&self, type_name: &str) -> Vec<&ManagedResource> {
178 self.resources
179 .values()
180 .filter(|r| r.type_name == type_name)
181 .collect()
182 }
183
184 pub fn get_resource_request_status(
186 &self,
187 request_token: &str,
188 ) -> Result<&ResourceRequest, CloudControlError> {
189 self.requests
190 .get(request_token)
191 .ok_or_else(|| CloudControlError::RequestTokenNotFound {
192 token: request_token.to_string(),
193 })
194 }
195
196 pub fn list_resource_requests(
198 &self,
199 operation_filter: Option<&[&str]>,
200 status_filter: Option<&[&str]>,
201 ) -> Vec<&ResourceRequest> {
202 self.requests
203 .values()
204 .filter(|r| {
205 if let Some(ops) = operation_filter {
206 if !ops.contains(&r.operation.as_str()) {
207 return false;
208 }
209 }
210 if let Some(statuses) = status_filter {
211 if !statuses.contains(&r.operation_status.as_str()) {
212 return false;
213 }
214 }
215 true
216 })
217 .collect()
218 }
219
220 pub fn cancel_resource_request(
222 &mut self,
223 request_token: &str,
224 ) -> Result<ResourceRequest, CloudControlError> {
225 let request = self.requests.get_mut(request_token).ok_or_else(|| {
226 CloudControlError::RequestTokenNotFound {
227 token: request_token.to_string(),
228 }
229 })?;
230
231 match request.operation_status {
234 OperationStatus::Pending | OperationStatus::InProgress => {
235 request.operation_status = OperationStatus::CancelComplete;
236 Ok(request.clone())
237 }
238 _ => Err(CloudControlError::NotCancellable {
239 token: request_token.to_string(),
240 status: request.operation_status.as_str().to_string(),
241 }),
242 }
243 }
244}
245
246fn extract_identifier_from_model(model_json: &str) -> Option<String> {
249 let parsed: serde_json::Value = serde_json::from_str(model_json).ok()?;
250 let obj = parsed.as_object()?;
251
252 for field in &[
254 "Id",
255 "Identifier",
256 "Name",
257 "Arn",
258 "BucketName",
259 "FunctionName",
260 "TableName",
261 ] {
262 if let Some(val) = obj.get(*field) {
263 if let Some(s) = val.as_str() {
264 return Some(s.to_string());
265 }
266 }
267 }
268 None
269}
270
271fn apply_json_patch(target: &mut serde_json::Value, patch: &serde_json::Value) {
273 let op = patch.get("op").and_then(|v| v.as_str()).unwrap_or("");
274 let path = patch.get("path").and_then(|v| v.as_str()).unwrap_or("");
275
276 let key = path.trim_start_matches('/');
278 if key.is_empty() {
279 return;
280 }
281
282 match op {
283 "add" | "replace" => {
284 if let Some(value) = patch.get("value") {
285 if let Some(obj) = target.as_object_mut() {
286 obj.insert(key.to_string(), value.clone());
287 }
288 }
289 }
290 "remove" => {
291 if let Some(obj) = target.as_object_mut() {
292 obj.remove(key);
293 }
294 }
295 _ => {} }
297}