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