use std::collections::HashMap;
use chrono::Utc;
use thiserror::Error;
use uuid::Uuid;
use crate::cfn_schema::{ShapeContext, lookup as lookup_shaper};
use crate::types::{ManagedResource, OperationStatus, OperationType, ResourceRequest};
#[derive(Debug, Error)]
pub enum CloudControlError {
#[error("Resource of type {type_name} with identifier {identifier} already exists.")]
AlreadyExists {
type_name: String,
identifier: String,
},
#[error("Resource of type {type_name} with identifier {identifier} not found.")]
ResourceNotFound {
type_name: String,
identifier: String,
},
#[error("A resource operation with the specified request token {token} was not found.")]
RequestTokenNotFound { token: String },
#[error("The specified extension {type_name} does not exist in the CloudFormation registry.")]
TypeNotFound { type_name: String },
#[error("{message}")]
InvalidRequest { message: String },
#[error(
"The resource operation request with token {token} cannot be cancelled because its status is {status}."
)]
NotCancellable { token: String, status: String },
}
#[derive(Debug, Default)]
pub struct CloudControlState {
pub resources: HashMap<(String, String), ManagedResource>,
pub requests: HashMap<String, ResourceRequest>,
}
impl CloudControlState {
pub fn create_resource(
&mut self,
type_name: &str,
desired_state: &str,
ctx: &ShapeContext<'_>,
) -> Result<ResourceRequest, CloudControlError> {
let (identifier, stored_model) = if let Some(shaper) = lookup_shaper(type_name) {
let desired_json: serde_json::Value =
serde_json::from_str(desired_state).map_err(|e| {
CloudControlError::InvalidRequest {
message: format!("DesiredState is not valid JSON: {e}"),
}
})?;
let shaped = shaper
.shape_create(&desired_json, ctx)
.map_err(|message| CloudControlError::InvalidRequest { message })?;
let json = serde_json::to_string(&shaped.properties).unwrap_or_default();
(shaped.primary_identifier, json)
} else {
let id = extract_identifier_from_model(desired_state)
.unwrap_or_else(|| Uuid::new_v4().to_string());
(id, desired_state.to_string())
};
let key = (type_name.to_string(), identifier.clone());
if self.resources.contains_key(&key) {
return Err(CloudControlError::AlreadyExists {
type_name: type_name.to_string(),
identifier,
});
}
let resource = ManagedResource {
type_name: type_name.to_string(),
identifier: identifier.clone(),
resource_model: stored_model.clone(),
};
self.resources.insert(key, resource);
let request_token = Uuid::new_v4().to_string();
let request = ResourceRequest {
request_token: request_token.clone(),
type_name: type_name.to_string(),
identifier: identifier.clone(),
operation: OperationType::Create,
operation_status: OperationStatus::Success,
event_time: Utc::now(),
resource_model: Some(stored_model),
status_message: None,
error_code: None,
};
self.requests.insert(request_token, request.clone());
Ok(request)
}
pub fn delete_resource(
&mut self,
type_name: &str,
identifier: &str,
) -> Result<ResourceRequest, CloudControlError> {
let key = (type_name.to_string(), identifier.to_string());
if self.resources.remove(&key).is_none() {
return Err(CloudControlError::ResourceNotFound {
type_name: type_name.to_string(),
identifier: identifier.to_string(),
});
}
let request_token = Uuid::new_v4().to_string();
let request = ResourceRequest {
request_token: request_token.clone(),
type_name: type_name.to_string(),
identifier: identifier.to_string(),
operation: OperationType::Delete,
operation_status: OperationStatus::Success,
event_time: Utc::now(),
resource_model: None,
status_message: None,
error_code: None,
};
self.requests.insert(request_token, request.clone());
Ok(request)
}
pub fn update_resource(
&mut self,
type_name: &str,
identifier: &str,
patch_document: &str,
ctx: &ShapeContext<'_>,
) -> Result<ResourceRequest, CloudControlError> {
let key = (type_name.to_string(), identifier.to_string());
let resource =
self.resources
.get_mut(&key)
.ok_or_else(|| CloudControlError::ResourceNotFound {
type_name: type_name.to_string(),
identifier: identifier.to_string(),
})?;
let previous: serde_json::Value = serde_json::from_str(&resource.resource_model)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let mut model = previous.clone();
let patches: Vec<serde_json::Value> =
serde_json::from_str(patch_document).unwrap_or_default();
for patch in &patches {
apply_json_patch(&mut model, patch);
}
if let Some(shaper) = lookup_shaper(type_name) {
model = shaper
.shape_update(&previous, model, ctx)
.map_err(|message| CloudControlError::InvalidRequest { message })?;
}
let updated_model = serde_json::to_string(&model).unwrap_or_default();
resource.resource_model = updated_model.clone();
let request_token = Uuid::new_v4().to_string();
let request = ResourceRequest {
request_token: request_token.clone(),
type_name: type_name.to_string(),
identifier: identifier.to_string(),
operation: OperationType::Update,
operation_status: OperationStatus::Success,
event_time: Utc::now(),
resource_model: Some(updated_model),
status_message: None,
error_code: None,
};
self.requests.insert(request_token, request.clone());
Ok(request)
}
pub fn get_resource(
&self,
type_name: &str,
identifier: &str,
) -> Result<&ManagedResource, CloudControlError> {
let key = (type_name.to_string(), identifier.to_string());
self.resources
.get(&key)
.ok_or_else(|| CloudControlError::ResourceNotFound {
type_name: type_name.to_string(),
identifier: identifier.to_string(),
})
}
pub fn list_resources(&self, type_name: &str) -> Vec<&ManagedResource> {
self.resources
.values()
.filter(|r| r.type_name == type_name)
.collect()
}
pub fn get_resource_request_status(
&self,
request_token: &str,
) -> Result<&ResourceRequest, CloudControlError> {
self.requests
.get(request_token)
.ok_or_else(|| CloudControlError::RequestTokenNotFound {
token: request_token.to_string(),
})
}
pub fn list_resource_requests(
&self,
operation_filter: Option<&[&str]>,
status_filter: Option<&[&str]>,
) -> Vec<&ResourceRequest> {
self.requests
.values()
.filter(|r| {
if let Some(ops) = operation_filter {
if !ops.contains(&r.operation.as_str()) {
return false;
}
}
if let Some(statuses) = status_filter {
if !statuses.contains(&r.operation_status.as_str()) {
return false;
}
}
true
})
.collect()
}
pub fn cancel_resource_request(
&mut self,
request_token: &str,
) -> Result<ResourceRequest, CloudControlError> {
let request = self.requests.get_mut(request_token).ok_or_else(|| {
CloudControlError::RequestTokenNotFound {
token: request_token.to_string(),
}
})?;
match request.operation_status {
OperationStatus::Pending | OperationStatus::InProgress => {
request.operation_status = OperationStatus::CancelComplete;
Ok(request.clone())
}
_ => Err(CloudControlError::NotCancellable {
token: request_token.to_string(),
status: request.operation_status.as_str().to_string(),
}),
}
}
}
fn extract_identifier_from_model(model_json: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(model_json).ok()?;
let obj = parsed.as_object()?;
for field in &[
"Id",
"Identifier",
"Name",
"Arn",
"BucketName",
"FunctionName",
"TableName",
] {
if let Some(val) = obj.get(*field) {
if let Some(s) = val.as_str() {
return Some(s.to_string());
}
}
}
None
}
fn apply_json_patch(target: &mut serde_json::Value, patch: &serde_json::Value) {
let op = patch.get("op").and_then(|v| v.as_str()).unwrap_or("");
let path = patch.get("path").and_then(|v| v.as_str()).unwrap_or("");
let key = path.trim_start_matches('/');
if key.is_empty() {
return;
}
match op {
"add" | "replace" => {
if let Some(value) = patch.get("value") {
if let Some(obj) = target.as_object_mut() {
obj.insert(key.to_string(), value.clone());
}
}
}
"remove" => {
if let Some(obj) = target.as_object_mut() {
obj.remove(key);
}
}
_ => {} }
}