use chrono::Utc;
use http::StatusCode;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use fakecloud_aws::arn::Arn;
use fakecloud_aws::xml::xml_escape;
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::service::CloudFormationService;
use crate::state::StackResource;
use crate::template;
const NS: &str = "http://cloudformation.amazonaws.com/doc/2010-05-15/";
fn rand_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{nanos:x}-{seq:x}")
}
fn xml_response(action: &str, inner: String, request_id: &str) -> AwsResponse {
let body = format!(
r#"<{action}Response xmlns="{NS}">
<{action}Result>
{inner}
</{action}Result>
<ResponseMetadata>
<RequestId>{rid}</RequestId>
</ResponseMetadata>
</{action}Response>"#,
action = action,
NS = NS,
inner = inner,
rid = xml_escape(request_id),
);
AwsResponse::xml(StatusCode::OK, body)
}
fn xml_response_no_result(action: &str, request_id: &str) -> AwsResponse {
let body = format!(
r#"<{action}Response xmlns="{NS}">
<ResponseMetadata>
<RequestId>{rid}</RequestId>
</ResponseMetadata>
</{action}Response>"#,
action = action,
NS = NS,
rid = xml_escape(request_id),
);
AwsResponse::xml(StatusCode::OK, body)
}
fn members_xml<F>(items: &[Value], render: F) -> String
where
F: Fn(&Value) -> String,
{
items
.iter()
.map(|v| format!(" <member>\n{}\n </member>", render(v)))
.collect::<Vec<_>>()
.join("\n")
}
fn store<'a>(
extras: &'a mut BTreeMap<String, BTreeMap<String, Value>>,
category: &str,
) -> &'a mut BTreeMap<String, Value> {
extras.entry(category.to_string()).or_default()
}
fn missing(name: &str) -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
format!("{name} is required"),
)
}
fn has_collection_param(params: &BTreeMap<String, String>, field: &str) -> bool {
let prefix = format!("{field}.");
params.keys().any(|k| k.starts_with(&prefix))
}
fn require_scalar(params: &BTreeMap<String, String>, field: &str) -> Result<(), AwsServiceError> {
if params.get(field).is_some() {
Ok(())
} else {
Err(missing(field))
}
}
fn require_collection(
params: &BTreeMap<String, String>,
field: &str,
) -> Result<(), AwsServiceError> {
if has_collection_param(params, field) {
Ok(())
} else {
Err(missing(field))
}
}
impl CloudFormationService {
pub(crate) fn handle_extra_action(
&self,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let action = req.action.clone();
let params = Self::get_all_params(req);
let aid = req.account_id.clone();
let rid = req.request_id.clone();
match action.as_str() {
"CreateChangeSet" => {
let stack_name = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let cs_name = params
.get("ChangeSetName")
.ok_or_else(|| missing("ChangeSetName"))?
.clone();
let template_body = params.get("TemplateBody").cloned().unwrap_or_default();
let cs_params = CloudFormationService::extract_parameters(¶ms);
let cs_tags = CloudFormationService::extract_tags(¶ms);
let cs_notif = CloudFormationService::extract_notification_arns(¶ms);
let stack_lookup: Option<(String, Vec<crate::state::StackResource>)> = {
let accounts = self.state.read();
accounts.get(&aid).and_then(|s| {
s.stacks
.values()
.find(|st| {
(st.name == stack_name || st.stack_id == stack_name)
&& st.status != "DELETE_COMPLETE"
})
.map(|st| (st.stack_id.clone(), st.resources.clone()))
})
};
let mut full_params: BTreeMap<String, String> = cs_params.clone();
full_params
.entry("AWS::Region".to_string())
.or_insert_with(|| req.region.clone());
full_params
.entry("AWS::AccountId".to_string())
.or_insert_with(|| aid.clone());
full_params
.entry("AWS::StackName".to_string())
.or_insert_with(|| stack_name.clone());
full_params
.entry("AWS::Partition".to_string())
.or_insert_with(|| "aws".to_string());
full_params
.entry("AWS::URLSuffix".to_string())
.or_insert_with(|| "amazonaws.com".to_string());
if let Some((sid, _)) = &stack_lookup {
full_params
.entry("AWS::StackId".to_string())
.or_insert_with(|| sid.clone());
}
let mut changes: Vec<Value> = Vec::new();
if !template_body.trim().is_empty() {
let parsed =
template::parse_template(&template_body, &full_params).unwrap_or_default();
let existing_resources = stack_lookup
.as_ref()
.map(|(_, r)| r.clone())
.unwrap_or_default();
let existing_by_id: BTreeMap<&str, &crate::state::StackResource> =
existing_resources
.iter()
.map(|r| (r.logical_id.as_str(), r))
.collect();
let new_by_id: BTreeMap<&str, &template::ResourceDefinition> = parsed
.resources
.iter()
.map(|r| (r.logical_id.as_str(), r))
.collect();
for r in &parsed.resources {
if let Some(existing) = existing_by_id.get(r.logical_id.as_str()) {
let replacement = if existing.resource_type != r.resource_type {
"True"
} else {
"Conditional"
};
changes.push(json!({
"Type": "Resource",
"ResourceChange": {
"Action": "Modify",
"LogicalResourceId": r.logical_id,
"PhysicalResourceId": existing.physical_id,
"ResourceType": r.resource_type,
"Replacement": replacement,
}
}));
} else {
changes.push(json!({
"Type": "Resource",
"ResourceChange": {
"Action": "Add",
"LogicalResourceId": r.logical_id,
"ResourceType": r.resource_type,
}
}));
}
}
for r in &existing_resources {
if !new_by_id.contains_key(r.logical_id.as_str()) {
changes.push(json!({
"Type": "Resource",
"ResourceChange": {
"Action": "Remove",
"LogicalResourceId": r.logical_id,
"PhysicalResourceId": r.physical_id,
"ResourceType": r.resource_type,
}
}));
}
}
}
let id = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("changeSet/{cs_name}/{}", rand_id()),
)
.to_string();
let stack_id_str = stack_lookup
.as_ref()
.map(|(s, _)| s.clone())
.unwrap_or_else(|| {
Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("stack/{stack_name}/{}", rand_id()),
)
.to_string()
});
let entry = json!({
"Id": id,
"ChangeSetName": cs_name,
"StackId": stack_id_str,
"StackName": stack_name,
"Status": "CREATE_COMPLETE",
"ExecutionStatus": "AVAILABLE",
"TemplateBody": template_body,
"Parameters": cs_params,
"Tags": cs_tags,
"NotificationArns": cs_notif,
"Changes": changes,
});
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
store(&mut state.extras, "change_sets").insert(id.clone(), entry);
Ok(xml_response(
"CreateChangeSet",
format!(
" <Id>{}</Id>\n <StackId>{}</StackId>",
xml_escape(&id),
xml_escape(&stack_id_str)
),
&rid,
))
}
"DescribeChangeSet" => {
let cs = params
.get("ChangeSetName")
.ok_or_else(|| missing("ChangeSetName"))?
.clone();
let stack_filter = params.get("StackName").cloned();
let accounts = self.state.read();
let entry = accounts.get(&aid)
.and_then(|s| s.extras.get("change_sets"))
.and_then(|m| m.values().find(|v| {
let id_match = v["Id"].as_str() == Some(&cs)
|| v["ChangeSetName"].as_str() == Some(&cs);
let stack_match = stack_filter.as_deref().is_none_or(|sf| {
v["StackName"].as_str() == Some(sf)
|| v["StackId"].as_str() == Some(sf)
});
id_match && stack_match
}))
.cloned()
.unwrap_or_else(|| json!({"ChangeSetName": cs.clone(), "Status": "CREATE_COMPLETE", "ExecutionStatus": "AVAILABLE"}));
let changes_xml = entry["Changes"]
.as_array()
.map(|arr| {
let mut out = String::new();
for change in arr {
let rc = &change["ResourceChange"];
out.push_str(" <member>\n");
out.push_str(&format!(
" <Type>{}</Type>\n",
xml_escape(change["Type"].as_str().unwrap_or("Resource"))
));
out.push_str(" <ResourceChange>\n");
out.push_str(&format!(
" <Action>{}</Action>\n",
xml_escape(rc["Action"].as_str().unwrap_or(""))
));
out.push_str(&format!(
" <LogicalResourceId>{}</LogicalResourceId>\n",
xml_escape(rc["LogicalResourceId"].as_str().unwrap_or(""))
));
if let Some(pid) = rc["PhysicalResourceId"].as_str() {
out.push_str(&format!(
" <PhysicalResourceId>{}</PhysicalResourceId>\n",
xml_escape(pid)
));
}
out.push_str(&format!(
" <ResourceType>{}</ResourceType>\n",
xml_escape(rc["ResourceType"].as_str().unwrap_or(""))
));
if let Some(replacement) = rc["Replacement"].as_str() {
out.push_str(&format!(
" <Replacement>{}</Replacement>\n",
xml_escape(replacement)
));
}
out.push_str(" </ResourceChange>\n");
out.push_str(" </member>");
out.push('\n');
}
out
})
.unwrap_or_default();
let inner = format!(
" <ChangeSetName>{}</ChangeSetName>\n <ChangeSetId>{}</ChangeSetId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <Status>{}</Status>\n <ExecutionStatus>{}</ExecutionStatus>\n <Changes>\n{} </Changes>",
xml_escape(entry["ChangeSetName"].as_str().unwrap_or("")),
xml_escape(entry["Id"].as_str().unwrap_or("")),
xml_escape(entry["StackId"].as_str().unwrap_or("")),
xml_escape(entry["StackName"].as_str().unwrap_or("")),
xml_escape(entry["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
xml_escape(entry["ExecutionStatus"].as_str().unwrap_or("AVAILABLE")),
changes_xml,
);
Ok(xml_response("DescribeChangeSet", inner, &rid))
}
"DescribeChangeSetHooks" => {
require_scalar(¶ms, "ChangeSetName")?;
Ok(xml_response(
"DescribeChangeSetHooks",
" <Hooks/>".to_string(),
&rid,
))
}
"DeleteChangeSet" => {
let cs = params
.get("ChangeSetName")
.ok_or_else(|| missing("ChangeSetName"))?
.clone();
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
if let Some(m) = state.extras.get_mut("change_sets") {
m.retain(|_, v| {
v["Id"].as_str() != Some(&cs) && v["ChangeSetName"].as_str() != Some(&cs)
});
}
Ok(xml_response("DeleteChangeSet", String::new(), &rid))
}
"ExecuteChangeSet" => {
let cs = params
.get("ChangeSetName")
.cloned()
.ok_or_else(|| missing("ChangeSetName"))?;
let stack_filter = params.get("StackName").cloned();
let entry = {
let accounts = self.state.read();
accounts
.get(&aid)
.and_then(|s| s.extras.get("change_sets"))
.and_then(|m| {
m.values()
.find(|v| {
let id_match = v["Id"].as_str() == Some(&cs)
|| v["ChangeSetName"].as_str() == Some(&cs);
let stack_match = stack_filter.as_deref().is_none_or(|sf| {
v["StackName"].as_str() == Some(sf)
|| v["StackId"].as_str() == Some(sf)
});
id_match && stack_match
})
.cloned()
})
};
let Some(entry) = entry else {
return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
};
if entry["ExecutionStatus"].as_str() != Some("AVAILABLE") {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"InvalidChangeSetStatus",
format!(
"ChangeSet [{cs}] cannot be executed in its current status of [{}]",
entry["ExecutionStatus"].as_str().unwrap_or("")
),
));
}
let cs_id = entry["Id"].as_str().unwrap_or("").to_string();
let stack_name = entry["StackName"].as_str().unwrap_or("").to_string();
let template_body = entry["TemplateBody"].as_str().unwrap_or("").to_string();
if template_body.trim().is_empty() {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
if let Some(m) = state.extras.get_mut("change_sets") {
if let Some(e) = m.get_mut(&cs_id) {
e["ExecutionStatus"] = json!("EXECUTE_COMPLETE");
}
}
return Ok(xml_response("ExecuteChangeSet", String::new(), &rid));
}
let cs_tags: BTreeMap<String, String> = entry["Tags"]
.as_object()
.map(|m| {
m.iter()
.filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
.collect()
})
.unwrap_or_default();
let cs_notif: Vec<String> = entry["NotificationArns"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let mut cs_params: BTreeMap<String, String> = entry["Parameters"]
.as_object()
.map(|m| {
m.iter()
.filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string())))
.collect()
})
.unwrap_or_default();
let found_stack_id = {
let accounts = self.state.read();
accounts.get(&aid).and_then(|s| {
s.stacks
.values()
.find(|st| {
(st.name == stack_name || st.stack_id == stack_name)
&& st.status != "DELETE_COMPLETE"
})
.map(|st| st.stack_id.clone())
})
}
.ok_or_else(|| {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
format!("Stack [{stack_name}] does not exist"),
)
})?;
cs_params
.entry("AWS::Region".to_string())
.or_insert_with(|| req.region.clone());
cs_params
.entry("AWS::AccountId".to_string())
.or_insert_with(|| aid.clone());
cs_params
.entry("AWS::StackId".to_string())
.or_insert_with(|| found_stack_id.clone());
cs_params
.entry("AWS::StackName".to_string())
.or_insert_with(|| stack_name.clone());
cs_params
.entry("AWS::Partition".to_string())
.or_insert_with(|| "aws".to_string());
cs_params
.entry("AWS::URLSuffix".to_string())
.or_insert_with(|| "amazonaws.com".to_string());
let parsed = template::parse_template(&template_body, &cs_params).map_err(|e| {
AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationError", e)
})?;
let provisioner = self.provisioner(&found_stack_id, &aid, &req.region);
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
let (update_result, sid, stack_name_owned) = {
let stack = state
.stacks
.values_mut()
.find(|st| st.stack_id == found_stack_id && st.status != "DELETE_COMPLETE")
.ok_or_else(|| {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
format!("Stack [{stack_name}] does not exist"),
)
})?;
stack.status = "UPDATE_IN_PROGRESS".to_string();
let result = crate::service::apply_resource_updates(
stack,
&parsed.resources,
&template_body,
&cs_params,
&provisioner,
);
let sid = stack.stack_id.clone();
let sname = stack.name.clone();
stack.template = template_body.clone();
stack.status = if result.is_err() {
"UPDATE_ROLLBACK_COMPLETE".to_string()
} else {
"UPDATE_COMPLETE".to_string()
};
stack.parameters = cs_params.clone();
if !cs_tags.is_empty() {
stack.tags = cs_tags;
}
if !cs_notif.is_empty() {
stack.notification_arns = cs_notif;
}
stack.updated_at = Some(Utc::now());
if result.is_ok() {
stack.outputs.clear();
}
(result, sid, sname)
};
crate::service::record_stack_status_event(
state,
&sid,
&stack_name_owned,
"AWS::CloudFormation::Stack",
"UPDATE_IN_PROGRESS",
);
let final_status = match &update_result {
Ok(changes) => {
crate::service::record_stack_events(
state,
&sid,
&stack_name_owned,
changes,
);
"UPDATE_COMPLETE"
}
Err(_) => "UPDATE_ROLLBACK_COMPLETE",
};
crate::service::record_stack_status_event(
state,
&sid,
&stack_name_owned,
"AWS::CloudFormation::Stack",
final_status,
);
if let Some(m) = state.extras.get_mut("change_sets") {
if let Some(e) = m.get_mut(&cs_id) {
e["ExecutionStatus"] = json!(if update_result.is_err() {
"EXECUTE_FAILED"
} else {
"EXECUTE_COMPLETE"
});
}
}
drop(accounts);
if let Err(msg) = update_result {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationError",
msg,
));
}
Ok(xml_response("ExecuteChangeSet", String::new(), &rid))
}
"ListChangeSets" => {
require_scalar(¶ms, "StackName")?;
let accounts = self.state.read();
let items: Vec<Value> = accounts
.get(&aid)
.and_then(|s| s.extras.get("change_sets"))
.map(|m| m.values().cloned().collect())
.unwrap_or_default();
let inner = format!(
" <Summaries>\n{}\n </Summaries>",
members_xml(&items, |v| {
format!(
" <ChangeSetId>{}</ChangeSetId>\n <ChangeSetName>{}</ChangeSetName>\n <Status>{}</Status>",
xml_escape(v["Id"].as_str().unwrap_or("")),
xml_escape(v["ChangeSetName"].as_str().unwrap_or("")),
xml_escape(v["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
)
}),
);
Ok(xml_response("ListChangeSets", inner, &rid))
}
"CreateStackSet" => {
let name = params
.get("StackSetName")
.ok_or_else(|| missing("StackSetName"))?
.clone();
let id = format!("{name}:{}", rand_id());
let entry = json!({
"StackSetId": id,
"StackSetName": name,
"Status": "ACTIVE",
"TemplateBody": params.get("TemplateBody").cloned().unwrap_or_default(),
});
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
store(&mut state.extras, "stack_sets").insert(name.clone(), entry);
Ok(xml_response(
"CreateStackSet",
format!(" <StackSetId>{}</StackSetId>", xml_escape(&id)),
&rid,
))
}
"DescribeStackSet" => {
let name = params
.get("StackSetName")
.ok_or_else(|| missing("StackSetName"))?
.clone();
let accounts = self.state.read();
let entry = accounts
.get(&aid)
.and_then(|s| s.extras.get("stack_sets"))
.and_then(|m| m.get(&name))
.cloned()
.unwrap_or_else(|| json!({"StackSetName": name.clone(), "Status": "ACTIVE"}));
let inner = format!(
" <StackSet>\n <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>\n </StackSet>",
xml_escape(entry["StackSetName"].as_str().unwrap_or(&name)),
xml_escape(entry["StackSetId"].as_str().unwrap_or("")),
xml_escape(entry["Status"].as_str().unwrap_or("ACTIVE")),
);
Ok(xml_response("DescribeStackSet", inner, &rid))
}
"ListStackSets" => {
let accounts = self.state.read();
let items: Vec<Value> = accounts
.get(&aid)
.and_then(|s| s.extras.get("stack_sets"))
.map(|m| m.values().cloned().collect())
.unwrap_or_default();
let inner = format!(
" <Summaries>\n{}\n </Summaries>",
members_xml(&items, |v| {
format!(
" <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>",
xml_escape(v["StackSetName"].as_str().unwrap_or("")),
xml_escape(v["StackSetId"].as_str().unwrap_or("")),
xml_escape(v["Status"].as_str().unwrap_or("ACTIVE")),
)
}),
);
Ok(xml_response("ListStackSets", inner, &rid))
}
"UpdateStackSet" => {
require_scalar(¶ms, "StackSetName")?;
let op_id = rand_id();
Ok(xml_response(
"UpdateStackSet",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"DeleteStackSet" => {
let name = params
.get("StackSetName")
.ok_or_else(|| missing("StackSetName"))?
.clone();
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
if let Some(m) = state.extras.get_mut("stack_sets") {
m.remove(&name);
}
Ok(xml_response("DeleteStackSet", String::new(), &rid))
}
"DescribeStackSetOperation" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "OperationId")?;
let op_id = params.get("OperationId").cloned().unwrap_or_else(rand_id);
let inner = format!(
" <StackSetOperation>\n <OperationId>{}</OperationId>\n <Status>SUCCEEDED</Status>\n </StackSetOperation>",
xml_escape(&op_id),
);
Ok(xml_response("DescribeStackSetOperation", inner, &rid))
}
"ListStackSetOperations" => {
require_scalar(¶ms, "StackSetName")?;
Ok(xml_response(
"ListStackSetOperations",
" <Summaries/>".to_string(),
&rid,
))
}
"ListStackSetOperationResults" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "OperationId")?;
Ok(xml_response(
"ListStackSetOperationResults",
" <Summaries/>".to_string(),
&rid,
))
}
"ListStackSetAutoDeploymentTargets" => {
require_scalar(¶ms, "StackSetName")?;
Ok(xml_response(
"ListStackSetAutoDeploymentTargets",
" <Summaries/>".to_string(),
&rid,
))
}
"StopStackSetOperation" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "OperationId")?;
Ok(xml_response("StopStackSetOperation", String::new(), &rid))
}
"ImportStacksToStackSet" => {
require_scalar(¶ms, "StackSetName")?;
let op_id = rand_id();
Ok(xml_response(
"ImportStacksToStackSet",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"CreateStackInstances" => {
require_scalar(¶ms, "StackSetName")?;
let op_id = rand_id();
Ok(xml_response(
"CreateStackInstances",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"UpdateStackInstances" => {
require_scalar(¶ms, "StackSetName")?;
let op_id = rand_id();
Ok(xml_response(
"UpdateStackInstances",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"DeleteStackInstances" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "RetainStacks")?;
let op_id = rand_id();
Ok(xml_response(
"DeleteStackInstances",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"DescribeStackInstance" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "StackInstanceAccount")?;
require_scalar(¶ms, "StackInstanceRegion")?;
let inner =
" <StackInstance>\n <Status>CURRENT</Status>\n </StackInstance>"
.to_string();
Ok(xml_response("DescribeStackInstance", inner, &rid))
}
"ListStackInstances" => {
require_scalar(¶ms, "StackSetName")?;
Ok(xml_response(
"ListStackInstances",
" <Summaries/>".to_string(),
&rid,
))
}
"ListStackInstanceResourceDrifts" => {
require_scalar(¶ms, "StackSetName")?;
require_scalar(¶ms, "StackInstanceAccount")?;
require_scalar(¶ms, "StackInstanceRegion")?;
require_scalar(¶ms, "OperationId")?;
Ok(xml_response(
"ListStackInstanceResourceDrifts",
" <Summaries/>".to_string(),
&rid,
))
}
"CreateStackRefactor" => {
require_collection(¶ms, "StackDefinitions")?;
let id = rand_id();
let entry = json!({"StackRefactorId": id.clone(), "Status": "CREATE_COMPLETE"});
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
store(&mut state.extras, "refactors").insert(id.clone(), entry);
Ok(xml_response(
"CreateStackRefactor",
format!(" <StackRefactorId>{}</StackRefactorId>", xml_escape(&id)),
&rid,
))
}
"DescribeStackRefactor" => {
let id = params
.get("StackRefactorId")
.ok_or_else(|| missing("StackRefactorId"))?
.clone();
let inner = format!(
" <StackRefactorId>{}</StackRefactorId>\n <Status>CREATE_COMPLETE</Status>",
xml_escape(&id),
);
Ok(xml_response("DescribeStackRefactor", inner, &rid))
}
"ExecuteStackRefactor" => {
require_scalar(¶ms, "StackRefactorId")?;
Ok(xml_response("ExecuteStackRefactor", String::new(), &rid))
}
"ListStackRefactors" => Ok(xml_response(
"ListStackRefactors",
" <StackRefactorSummaries/>".to_string(),
&rid,
)),
"ListStackRefactorActions" => {
require_scalar(¶ms, "StackRefactorId")?;
Ok(xml_response(
"ListStackRefactorActions",
" <StackRefactorActions/>".to_string(),
&rid,
))
}
"ActivateType" => {
let arn = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("type/resource/{}", rand_id()),
)
.to_string();
Ok(xml_response(
"ActivateType",
format!(" <Arn>{}</Arn>", xml_escape(&arn)),
&rid,
))
}
"DeactivateType" => Ok(xml_response("DeactivateType", String::new(), &rid)),
"DescribeType" => {
let arn = params.get("Arn").cloned().unwrap_or_else(|| {
Arn::new("cloudformation", "us-east-1", &aid, "type/resource/Default")
.to_string()
});
let inner = format!(
" <Arn>{}</Arn>\n <Type>RESOURCE</Type>\n <TypeName>AWS::Custom::Type</TypeName>",
xml_escape(&arn),
);
Ok(xml_response("DescribeType", inner, &rid))
}
"DescribeTypeRegistration" => {
let token = params
.get("RegistrationToken")
.cloned()
.ok_or_else(|| missing("RegistrationToken"))?;
let inner = format!(
" <ProgressStatus>COMPLETE</ProgressStatus>\n <Description>{}</Description>",
xml_escape(&token),
);
Ok(xml_response("DescribeTypeRegistration", inner, &rid))
}
"RegisterType" => {
require_scalar(¶ms, "TypeName")?;
require_scalar(¶ms, "SchemaHandlerPackage")?;
let token = rand_id();
Ok(xml_response(
"RegisterType",
format!(
" <RegistrationToken>{}</RegistrationToken>",
xml_escape(&token)
),
&rid,
))
}
"DeregisterType" => Ok(xml_response("DeregisterType", String::new(), &rid)),
"ListTypes" => Ok(xml_response(
"ListTypes",
" <TypeSummaries/>".to_string(),
&rid,
)),
"ListTypeRegistrations" => Ok(xml_response(
"ListTypeRegistrations",
" <RegistrationTokenList/>".to_string(),
&rid,
)),
"ListTypeVersions" => Ok(xml_response(
"ListTypeVersions",
" <TypeVersionSummaries/>".to_string(),
&rid,
)),
"BatchDescribeTypeConfigurations" => {
Ok(xml_response(
"BatchDescribeTypeConfigurations",
" <Errors/>\n <TypeConfigurations/>".to_string(),
&rid,
))
}
"SetTypeConfiguration" => {
require_scalar(¶ms, "Configuration")?;
let arn = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("type-config/{}", rand_id()),
)
.to_string();
Ok(xml_response(
"SetTypeConfiguration",
format!(
" <ConfigurationArn>{}</ConfigurationArn>",
xml_escape(&arn)
),
&rid,
))
}
"SetTypeDefaultVersion" => {
Ok(xml_response("SetTypeDefaultVersion", String::new(), &rid))
}
"TestType" => {
let arn = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("type/resource/{}", rand_id()),
)
.to_string();
Ok(xml_response(
"TestType",
format!(" <TypeVersionArn>{}</TypeVersionArn>", xml_escape(&arn)),
&rid,
))
}
"PublishType" => {
let arn = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("type/resource/{}", rand_id()),
)
.to_string();
Ok(xml_response(
"PublishType",
format!(" <PublicTypeArn>{}</PublicTypeArn>", xml_escape(&arn)),
&rid,
))
}
"RegisterPublisher" => {
let id = rand_id();
Ok(xml_response(
"RegisterPublisher",
format!(" <PublisherId>{}</PublisherId>", xml_escape(&id)),
&rid,
))
}
"DescribePublisher" => {
let id = params
.get("PublisherId")
.cloned()
.unwrap_or_else(|| "default-publisher".to_string());
let inner = format!(
" <PublisherId>{}</PublisherId>\n <PublisherStatus>VERIFIED</PublisherStatus>\n <IdentityProvider>AWS_Marketplace</IdentityProvider>",
xml_escape(&id),
);
Ok(xml_response("DescribePublisher", inner, &rid))
}
"CreateGeneratedTemplate" => {
let name = params
.get("GeneratedTemplateName")
.ok_or_else(|| missing("GeneratedTemplateName"))?
.clone();
let id = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("generatedtemplate/{}", rand_id()),
)
.to_string();
let entry = json!({"GeneratedTemplateId": id.clone(), "Name": name.clone(), "Status": "COMPLETE"});
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
store(&mut state.extras, "generated_templates").insert(name.clone(), entry);
Ok(xml_response(
"CreateGeneratedTemplate",
format!(
" <GeneratedTemplateId>{}</GeneratedTemplateId>",
xml_escape(&id)
),
&rid,
))
}
"UpdateGeneratedTemplate" => {
let name = params
.get("GeneratedTemplateName")
.ok_or_else(|| missing("GeneratedTemplateName"))?
.clone();
let id = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("generatedtemplate/{name}"),
)
.to_string();
Ok(xml_response(
"UpdateGeneratedTemplate",
format!(
" <GeneratedTemplateId>{}</GeneratedTemplateId>",
xml_escape(&id)
),
&rid,
))
}
"DescribeGeneratedTemplate" => {
let name = params
.get("GeneratedTemplateName")
.ok_or_else(|| missing("GeneratedTemplateName"))?
.clone();
let inner = format!(
" <GeneratedTemplateId>arn:aws:cloudformation:us-east-1:{}:generatedtemplate/{}</GeneratedTemplateId>\n <GeneratedTemplateName>{}</GeneratedTemplateName>\n <Status>COMPLETE</Status>",
xml_escape(&aid),
xml_escape(&name),
xml_escape(&name),
);
Ok(xml_response("DescribeGeneratedTemplate", inner, &rid))
}
"GetGeneratedTemplate" => {
require_scalar(¶ms, "GeneratedTemplateName")?;
Ok(xml_response(
"GetGeneratedTemplate",
" <Status>COMPLETE</Status>\n <TemplateBody>{}</TemplateBody>"
.to_string(),
&rid,
))
}
"DeleteGeneratedTemplate" => {
let name = params
.get("GeneratedTemplateName")
.ok_or_else(|| missing("GeneratedTemplateName"))?
.clone();
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
if let Some(m) = state.extras.get_mut("generated_templates") {
m.remove(&name);
}
Ok(xml_response("DeleteGeneratedTemplate", String::new(), &rid))
}
"ListGeneratedTemplates" => Ok(xml_response(
"ListGeneratedTemplates",
" <Summaries/>".to_string(),
&rid,
)),
"StartResourceScan" => {
let id = Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("resourceScan/{}", rand_id()),
)
.to_string();
Ok(xml_response(
"StartResourceScan",
format!(" <ResourceScanId>{}</ResourceScanId>", xml_escape(&id)),
&rid,
))
}
"DescribeResourceScan" => {
let id = params
.get("ResourceScanId")
.cloned()
.ok_or_else(|| missing("ResourceScanId"))?;
let inner = format!(
" <ResourceScanId>{}</ResourceScanId>\n <Status>COMPLETE</Status>",
xml_escape(&id),
);
Ok(xml_response("DescribeResourceScan", inner, &rid))
}
"ListResourceScans" => Ok(xml_response(
"ListResourceScans",
" <ResourceScanSummaries/>".to_string(),
&rid,
)),
"ListResourceScanResources" => {
require_scalar(¶ms, "ResourceScanId")?;
Ok(xml_response(
"ListResourceScanResources",
" <Resources/>".to_string(),
&rid,
))
}
"ListResourceScanRelatedResources" => {
require_scalar(¶ms, "ResourceScanId")?;
Ok(xml_response(
"ListResourceScanRelatedResources",
" <RelatedResources/>".to_string(),
&rid,
))
}
"DetectStackDrift" => {
let stack_name = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let id = rand_id();
let resources: Vec<StackResource> = {
let accounts = self.state.read();
let stack = accounts.get(&aid).and_then(|s| {
s.stacks.values().find(|st| {
(st.name == stack_name || st.stack_id == stack_name)
&& st.status != "DELETE_COMPLETE"
})
});
stack.map(|s| s.resources.clone()).unwrap_or_default()
};
let mut drifted_resources: Vec<Value> = Vec::new();
for resource in &resources {
let exists = match resource.resource_type.as_str() {
"AWS::SQS::Queue" => self
.deps
.sqs
.read()
.get(&aid)
.map(|s| s.queues.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::SNS::Topic" => self
.deps
.sns
.read()
.get(&aid)
.map(|s| s.topics.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::S3::Bucket" => self
.deps
.s3
.read()
.get(&aid)
.map(|s| s.buckets.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::Lambda::Function" => self
.deps
.lambda
.read()
.get(&aid)
.map(|s| s.functions.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::IAM::Role" => self
.deps
.iam
.read()
.get(&aid)
.map(|s| s.roles.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::DynamoDB::Table" => self
.deps
.dynamodb
.read()
.get(&aid)
.map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
.unwrap_or(false),
"AWS::KMS::Key" => self
.deps
.kms
.read()
.get(&aid)
.map(|s| s.keys.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::SecretsManager::Secret" => self
.deps
.secretsmanager
.read()
.get(&aid)
.map(|s| s.secrets.contains_key(&resource.physical_id))
.unwrap_or(false),
_ => true, };
if !exists {
drifted_resources.push(json!({
"LogicalResourceId": resource.logical_id,
"PhysicalResourceId": resource.physical_id,
"ResourceType": resource.resource_type,
"StackResourceDriftStatus": "DELETED",
"PropertyDifferences": [],
}));
}
}
let stack_drift_status = if drifted_resources.is_empty() {
"IN_SYNC"
} else {
"DRIFTED"
};
let record = json!({
"StackDriftDetectionId": id,
"StackName": stack_name,
"StackDriftStatus": stack_drift_status,
"DetectionStatus": "DETECTION_COMPLETE",
"DriftedResources": drifted_resources,
});
{
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
store(&mut state.extras, "drift_detection").insert(id.clone(), record);
}
Ok(xml_response(
"DetectStackDrift",
format!(
" <StackDriftDetectionId>{}</StackDriftDetectionId>",
xml_escape(&id)
),
&rid,
))
}
"DetectStackResourceDrift" => {
let stack_name = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let logical = params
.get("LogicalResourceId")
.ok_or_else(|| missing("LogicalResourceId"))?
.clone();
let accounts = self.state.read();
let resource_drift = accounts
.get(&aid)
.and_then(|s| {
s.stacks.values().find(|st| {
(st.name == stack_name || st.stack_id == stack_name)
&& st.status != "DELETE_COMPLETE"
})
})
.and_then(|stack| stack.resources.iter().find(|r| r.logical_id == logical))
.map(|resource| {
let exists = match resource.resource_type.as_str() {
"AWS::SQS::Queue" => self
.deps
.sqs
.read()
.get(&aid)
.map(|s| s.queues.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::SNS::Topic" => self
.deps
.sns
.read()
.get(&aid)
.map(|s| s.topics.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::S3::Bucket" => self
.deps
.s3
.read()
.get(&aid)
.map(|s| s.buckets.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::Lambda::Function" => self
.deps
.lambda
.read()
.get(&aid)
.map(|s| s.functions.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::IAM::Role" => self
.deps
.iam
.read()
.get(&aid)
.map(|s| s.roles.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::DynamoDB::Table" => self
.deps
.dynamodb
.read()
.get(&aid)
.map(|s| s.tables.values().any(|t| t.arn == resource.physical_id))
.unwrap_or(false),
"AWS::KMS::Key" => self
.deps
.kms
.read()
.get(&aid)
.map(|s| s.keys.contains_key(&resource.physical_id))
.unwrap_or(false),
"AWS::SecretsManager::Secret" => self
.deps
.secretsmanager
.read()
.get(&aid)
.map(|s| s.secrets.contains_key(&resource.physical_id))
.unwrap_or(false),
_ => true,
};
if exists {
"IN_SYNC"
} else {
"DELETED"
}
})
.unwrap_or("NOT_CHECKED");
let inner = format!(
" <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
xml_escape(&logical),
xml_escape(resource_drift),
);
Ok(xml_response("DetectStackResourceDrift", inner, &rid))
}
"DetectStackSetDrift" => {
require_scalar(¶ms, "StackSetName")?;
let op_id = rand_id();
Ok(xml_response(
"DetectStackSetDrift",
format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)),
&rid,
))
}
"DescribeStackDriftDetectionStatus" => {
let id = params
.get("StackDriftDetectionId")
.ok_or_else(|| missing("StackDriftDetectionId"))?
.clone();
let accounts = self.state.read();
let record = accounts
.get(&aid)
.and_then(|s| s.extras.get("drift_detection"))
.and_then(|m| m.get(&id))
.cloned()
.unwrap_or_else(|| {
json!({
"StackDriftDetectionId": id,
"StackDriftStatus": "IN_SYNC",
"DetectionStatus": "DETECTION_COMPLETE",
})
});
let stack_id = record["StackId"]
.as_str()
.map(str::to_owned)
.unwrap_or_else(|| {
Arn::new(
"cloudformation",
"us-east-1",
&aid,
&format!("stack/drift-{id}/{}", rand_id()),
)
.to_string()
});
let timestamp = record["Timestamp"]
.as_str()
.map(str::to_owned)
.unwrap_or_else(|| "2024-01-01T00:00:00Z".to_string());
let inner = format!(
" <StackId>{}</StackId>\n <StackDriftDetectionId>{}</StackDriftDetectionId>\n <DetectionStatus>{}</DetectionStatus>\n <StackDriftStatus>{}</StackDriftStatus>\n <Timestamp>{}</Timestamp>",
xml_escape(&stack_id),
xml_escape(record["StackDriftDetectionId"].as_str().unwrap_or("")),
xml_escape(record["DetectionStatus"].as_str().unwrap_or("DETECTION_COMPLETE")),
xml_escape(record["StackDriftStatus"].as_str().unwrap_or("IN_SYNC")),
xml_escape(×tamp),
);
Ok(xml_response(
"DescribeStackDriftDetectionStatus",
inner,
&rid,
))
}
"DescribeStackResourceDrifts" => {
let stack_name = params
.get("StackName")
.cloned()
.ok_or_else(|| missing("StackName"))?;
let accounts = self.state.read();
let drifted: Vec<Value> = accounts
.get(&aid)
.and_then(|s| {
let found = s
.stacks
.values()
.find(|st| {
(st.name == stack_name || st.stack_id == stack_name)
&& st.status != "DELETE_COMPLETE"
})
.is_some();
if !found {
return None;
}
s.extras
.get("drift_detection")
.and_then(|m| {
m.values()
.find(|v| v["StackName"].as_str() == Some(stack_name.as_str()))
})
.and_then(|v| v["DriftedResources"].as_array().cloned())
})
.unwrap_or_default();
let inner = if drifted.is_empty() {
" <StackResourceDrifts/>".to_string()
} else {
format!(
" <StackResourceDrifts>\n{}\n </StackResourceDrifts>",
members_xml(&drifted, |v| {
format!(
" <StackResourceDrift>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <StackResourceDriftStatus>{}</StackResourceDriftStatus>\n </StackResourceDrift>",
xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
xml_escape(v["ResourceType"].as_str().unwrap_or("")),
xml_escape(v["StackResourceDriftStatus"].as_str().unwrap_or("IN_SYNC")),
)
}),
)
};
Ok(xml_response("DescribeStackResourceDrifts", inner, &rid))
}
"DescribeStackResource" => {
let stack_name = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let logical = params
.get("LogicalResourceId")
.ok_or_else(|| missing("LogicalResourceId"))?
.clone();
let accounts = self.state.read();
let detail = accounts
.get(&aid)
.and_then(|s| s.stacks.get(&stack_name))
.and_then(|s| s.resources.iter().find(|r| r.logical_id == logical))
.map(|r| {
(
r.physical_id.clone(),
r.resource_type.clone(),
r.status.clone(),
)
})
.unwrap_or_else(|| {
(
"pid".to_string(),
"AWS::Custom".to_string(),
"CREATE_COMPLETE".to_string(),
)
});
let inner = format!(
" <StackResourceDetail>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <LastUpdatedTimestamp>{}</LastUpdatedTimestamp>\n </StackResourceDetail>",
xml_escape(&stack_name),
xml_escape(&logical),
xml_escape(&detail.0),
xml_escape(&detail.1),
xml_escape(&detail.2),
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"),
);
Ok(xml_response("DescribeStackResource", inner, &rid))
}
"DescribeStackEvents" => {
require_scalar(¶ms, "StackName")?;
let stack_filter = params.get("StackName").cloned();
let accounts = self.state.read();
let events: Vec<Value> = accounts
.get(&aid)
.map(|s| {
let mut all: Vec<Value> = Vec::new();
for (sid, evs) in &s.events {
let matches = match &stack_filter {
None => true,
Some(filter) => {
sid == filter
|| s.stacks.values().any(|st| {
st.stack_id == *sid
&& (st.name == *filter || st.stack_id == *filter)
})
}
};
if matches {
all.extend(evs.iter().cloned());
}
}
all.reverse();
all
})
.unwrap_or_default();
let inner = if events.is_empty() {
" <StackEvents/>".to_string()
} else {
format!(
" <StackEvents>\n{}\n </StackEvents>",
members_xml(&events, |v| {
format!(
" <EventId>{}</EventId>\n <StackId>{}</StackId>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <Timestamp>{}</Timestamp>",
xml_escape(v["EventId"].as_str().unwrap_or("")),
xml_escape(v["StackId"].as_str().unwrap_or("")),
xml_escape(v["StackName"].as_str().unwrap_or("")),
xml_escape(v["LogicalResourceId"].as_str().unwrap_or("")),
xml_escape(v["PhysicalResourceId"].as_str().unwrap_or("")),
xml_escape(v["ResourceType"].as_str().unwrap_or("")),
xml_escape(v["ResourceStatus"].as_str().unwrap_or("")),
xml_escape(v["Timestamp"].as_str().unwrap_or("")),
)
}),
)
};
Ok(xml_response("DescribeStackEvents", inner, &rid))
}
"DescribeEvents" => Ok(xml_response(
"DescribeEvents",
" <Events/>".to_string(),
&rid,
)),
"GetHookResult" => Ok(xml_response(
"GetHookResult",
" <Status>HOOK_COMPLETE_SUCCEEDED</Status>".to_string(),
&rid,
)),
"ListHookResults" => Ok(xml_response(
"ListHookResults",
" <HookResults/>".to_string(),
&rid,
)),
"RecordHandlerProgress" => {
require_scalar(¶ms, "BearerToken")?;
require_scalar(¶ms, "OperationStatus")?;
Ok(xml_response_no_result("RecordHandlerProgress", &rid))
}
"ListExports" => {
let accounts = self.state.read();
let mut entries = String::new();
if let Some(state) = accounts.get(&aid) {
for (name, export) in &state.exports {
entries.push_str(&format!(
" <member>\n <ExportingStackId>{}</ExportingStackId>\n <Name>{}</Name>\n <Value>{}</Value>\n </member>\n",
xml_escape(&export.exporting_stack_id),
xml_escape(name),
xml_escape(&export.value),
));
}
}
let inner = if entries.is_empty() {
" <Exports/>".to_string()
} else {
format!(" <Exports>\n{entries} </Exports>")
};
Ok(xml_response("ListExports", inner, &rid))
}
"ListImports" => {
let export_name = params
.get("ExportName")
.cloned()
.ok_or_else(|| missing("ExportName"))?;
let accounts = self.state.read();
let mut entries = String::new();
if let Some(state) = accounts.get(&aid) {
if let Some(consumers) = state.imports.get(&export_name) {
for stack_name in consumers {
entries.push_str(&format!(
" <member>{}</member>\n",
xml_escape(stack_name)
));
}
}
}
let inner = if entries.is_empty() {
" <Imports/>".to_string()
} else {
format!(" <Imports>\n{entries} </Imports>")
};
Ok(xml_response("ListImports", inner, &rid))
}
"GetStackPolicy" => {
let stack = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let accounts = self.state.read();
let body = accounts.get(&aid)
.and_then(|s| s.stack_policies.get(&stack))
.cloned()
.unwrap_or_else(|| r#"{"Statement":[{"Effect":"Allow","Action":"Update:*","Principal":"*","Resource":"*"}]}"#.to_string());
let inner = format!(
" <StackPolicyBody>{}</StackPolicyBody>",
xml_escape(&body)
);
Ok(xml_response("GetStackPolicy", inner, &rid))
}
"SetStackPolicy" => {
let stack = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let body = params.get("StackPolicyBody").cloned().unwrap_or_default();
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
state.stack_policies.insert(stack, body);
Ok(xml_response_no_result("SetStackPolicy", &rid))
}
"UpdateTerminationProtection" => {
let stack = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let enabled_raw = params
.get("EnableTerminationProtection")
.ok_or_else(|| missing("EnableTerminationProtection"))?;
let enabled = enabled_raw.eq_ignore_ascii_case("true");
let stack_id = {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
state.termination_protection.insert(stack.clone(), enabled);
state
.stacks
.get(&stack)
.map(|s| s.stack_id.clone())
.unwrap_or_else(|| stack.clone())
};
Ok(xml_response(
"UpdateTerminationProtection",
format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
&rid,
))
}
"DescribeAccountLimits" => Ok(xml_response(
"DescribeAccountLimits",
r#" <AccountLimits>
<member>
<Name>StackLimit</Name>
<Value>2000</Value>
</member>
</AccountLimits>"#
.to_string(),
&rid,
)),
"ActivateOrganizationsAccess" => {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
state.orgs_access_enabled = true;
Ok(xml_response(
"ActivateOrganizationsAccess",
String::new(),
&rid,
))
}
"DeactivateOrganizationsAccess" => {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&aid);
state.orgs_access_enabled = false;
Ok(xml_response(
"DeactivateOrganizationsAccess",
String::new(),
&rid,
))
}
"DescribeOrganizationsAccess" => {
let accounts = self.state.read();
let status = if accounts
.get(&aid)
.map(|s| s.orgs_access_enabled)
.unwrap_or(false)
{
"ENABLED"
} else {
"DISABLED"
};
Ok(xml_response(
"DescribeOrganizationsAccess",
format!(" <Status>{}</Status>", status),
&rid,
))
}
"ValidateTemplate" => Ok(xml_response(
"ValidateTemplate",
" <Description>Validated</Description>\n <Capabilities/>\n <Parameters/>"
.to_string(),
&rid,
)),
"EstimateTemplateCost" => Ok(xml_response(
"EstimateTemplateCost",
" <Url>https://calculator.aws/#/estimate</Url>".to_string(),
&rid,
)),
"GetTemplateSummary" => Ok(xml_response(
"GetTemplateSummary",
" <Parameters/>\n <ResourceTypes/>\n <Capabilities/>".to_string(),
&rid,
)),
"CancelUpdateStack" => {
params
.get("StackName")
.ok_or_else(|| missing("StackName"))?;
Ok(xml_response_no_result("CancelUpdateStack", &rid))
}
"ContinueUpdateRollback" => {
params
.get("StackName")
.ok_or_else(|| missing("StackName"))?;
Ok(xml_response("ContinueUpdateRollback", String::new(), &rid))
}
"RollbackStack" => {
let stack = params
.get("StackName")
.ok_or_else(|| missing("StackName"))?
.clone();
let stack_id = {
let accounts = self.state.read();
accounts
.get(&aid)
.and_then(|s| s.stacks.get(&stack))
.map(|s| s.stack_id.clone())
.unwrap_or_else(|| stack.clone())
};
Ok(xml_response(
"RollbackStack",
format!(" <StackId>{}</StackId>", xml_escape(&stack_id)),
&rid,
))
}
"SignalResource" => {
require_scalar(¶ms, "StackName")?;
require_scalar(¶ms, "LogicalResourceId")?;
require_scalar(¶ms, "UniqueId")?;
require_scalar(¶ms, "Status")?;
Ok(xml_response_no_result("SignalResource", &rid))
}
_ => Err(AwsServiceError::action_not_implemented(
"cloudformation",
&action,
)),
}
}
}
#[cfg(test)]
mod tests {
use crate::service::{CloudFormationDeps, CloudFormationService};
use crate::state::{CloudFormationState, SharedCloudFormationState};
use fakecloud_core::delivery::DeliveryBus;
use fakecloud_core::multi_account::MultiAccountState;
use fakecloud_core::service::AwsRequest;
use http::Method;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
fn deps() -> CloudFormationDeps {
use fakecloud_dynamodb::DynamoDbState;
use fakecloud_ecr::EcrState;
use fakecloud_eventbridge::EventBridgeState;
use fakecloud_iam::IamState;
use fakecloud_kinesis::KinesisState;
use fakecloud_kms::KmsState;
use fakecloud_lambda::LambdaState;
use fakecloud_logs::LogsState;
use fakecloud_s3::S3State;
use fakecloud_secretsmanager::SecretsManagerState;
use fakecloud_sns::SnsState;
use fakecloud_sqs::SqsState;
use fakecloud_ssm::SsmState;
fn shared<T: fakecloud_core::multi_account::AccountState>(
) -> Arc<RwLock<MultiAccountState<T>>> {
Arc::new(RwLock::new(MultiAccountState::<T>::new(
"000000000000",
"us-east-1",
"",
)))
}
CloudFormationDeps {
sqs: shared::<SqsState>(),
sns: shared::<SnsState>(),
ssm: shared::<SsmState>(),
iam: shared::<IamState>(),
s3: shared::<S3State>(),
eventbridge: shared::<EventBridgeState>(),
dynamodb: shared::<DynamoDbState>(),
logs: shared::<LogsState>(),
lambda: shared::<LambdaState>(),
secretsmanager: shared::<SecretsManagerState>(),
kinesis: shared::<KinesisState>(),
kms: shared::<KmsState>(),
ecr: shared::<EcrState>(),
cloudwatch: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
elbv2: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
organizations: Arc::new(RwLock::new(None)),
cognito: shared::<fakecloud_cognito::CognitoState>(),
rds: shared::<fakecloud_rds::RdsState>(),
ecs: shared::<fakecloud_ecs::EcsState>(),
acm: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
elasticache: shared::<fakecloud_elasticache::ElastiCacheState>(),
route53: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
cloudfront: Arc::new(RwLock::new(fakecloud_cloudfront::CloudFrontAccounts::new())),
stepfunctions: shared::<fakecloud_stepfunctions::StepFunctionsState>(),
wafv2: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
apigateway: shared::<fakecloud_apigateway::ApiGatewayState>(),
apigatewayv2: shared::<fakecloud_apigatewayv2::ApiGatewayV2State>(),
ses: shared::<fakecloud_ses::SesState>(),
application_autoscaling: Arc::new(parking_lot::RwLock::new(
fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
)),
athena: Arc::new(parking_lot::RwLock::new(
fakecloud_athena::AthenaAccounts::new(),
)),
firehose: Arc::new(parking_lot::RwLock::new(
fakecloud_firehose::FirehoseAccounts::new(),
)),
glue: Arc::new(parking_lot::RwLock::new(fakecloud_glue::GlueAccounts::new())),
delivery: Arc::new(DeliveryBus::new()),
}
}
fn svc() -> CloudFormationService {
let state: SharedCloudFormationState =
Arc::new(RwLock::new(MultiAccountState::<CloudFormationState>::new(
"000000000000",
"us-east-1",
"",
)));
CloudFormationService::new(state, deps())
}
fn req(action: &str, params: &[(&str, &str)]) -> AwsRequest {
let mut q = HashMap::new();
q.insert("Action".to_string(), action.to_string());
for (k, v) in params {
q.insert(k.to_string(), v.to_string());
}
AwsRequest {
service: "cloudformation".to_string(),
method: Method::POST,
raw_path: "/".to_string(),
raw_query: String::new(),
path_segments: vec![],
query_params: q,
headers: http::HeaderMap::new(),
body: bytes::Bytes::new(),
body_stream: parking_lot::Mutex::new(None),
account_id: "000000000000".to_string(),
region: "us-east-1".to_string(),
request_id: "rid".to_string(),
action: action.to_string(),
is_query_protocol: true,
access_key_id: None,
principal: None,
}
}
fn ok(action: &str, params: &[(&str, &str)]) {
let r = svc().handle_extra_action(&req(action, params));
match r {
Ok(resp) => assert!(resp.status.is_success(), "{action} status: {}", resp.status),
Err(e) => panic!("{action} failed: {e:?}"),
}
}
#[test]
fn change_sets() {
ok(
"CreateChangeSet",
&[("StackName", "s"), ("ChangeSetName", "cs")],
);
ok("DescribeChangeSet", &[("ChangeSetName", "cs")]);
ok("DescribeChangeSetHooks", &[("ChangeSetName", "cs")]);
ok("ListChangeSets", &[("StackName", "s")]);
ok("ExecuteChangeSet", &[("ChangeSetName", "cs")]);
ok("DeleteChangeSet", &[("ChangeSetName", "cs")]);
}
#[test]
fn stack_sets_instances_refactors() {
ok("CreateStackSet", &[("StackSetName", "ss")]);
ok("DescribeStackSet", &[("StackSetName", "ss")]);
ok("ListStackSets", &[]);
ok("UpdateStackSet", &[("StackSetName", "ss")]);
ok(
"DescribeStackSetOperation",
&[("StackSetName", "ss"), ("OperationId", "op")],
);
ok("ListStackSetOperations", &[("StackSetName", "ss")]);
ok(
"ListStackSetOperationResults",
&[("StackSetName", "ss"), ("OperationId", "op")],
);
ok(
"ListStackSetAutoDeploymentTargets",
&[("StackSetName", "ss")],
);
ok(
"StopStackSetOperation",
&[("StackSetName", "ss"), ("OperationId", "op")],
);
ok("ImportStacksToStackSet", &[("StackSetName", "ss")]);
ok("DeleteStackSet", &[("StackSetName", "ss")]);
ok(
"CreateStackInstances",
&[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
);
ok(
"UpdateStackInstances",
&[("StackSetName", "ss"), ("Regions.member.1", "us-east-1")],
);
ok(
"DeleteStackInstances",
&[
("StackSetName", "ss"),
("Regions.member.1", "us-east-1"),
("RetainStacks", "false"),
],
);
ok(
"DescribeStackInstance",
&[
("StackSetName", "ss"),
("StackInstanceAccount", "000000000000"),
("StackInstanceRegion", "us-east-1"),
],
);
ok("ListStackInstances", &[("StackSetName", "ss")]);
ok(
"ListStackInstanceResourceDrifts",
&[
("StackSetName", "ss"),
("StackInstanceAccount", "000000000000"),
("StackInstanceRegion", "us-east-1"),
("OperationId", "op"),
],
);
ok(
"CreateStackRefactor",
&[("StackDefinitions.member.1.StackName", "s")],
);
ok("DescribeStackRefactor", &[("StackRefactorId", "r")]);
ok("ExecuteStackRefactor", &[("StackRefactorId", "r")]);
ok("ListStackRefactors", &[]);
ok("ListStackRefactorActions", &[("StackRefactorId", "r")]);
}
#[test]
fn types_and_publishers() {
ok("ActivateType", &[]);
ok("DeactivateType", &[]);
ok("DescribeType", &[]);
ok("DescribeTypeRegistration", &[("RegistrationToken", "tok")]);
ok(
"RegisterType",
&[("TypeName", "T"), ("SchemaHandlerPackage", "pkg")],
);
ok("DeregisterType", &[]);
ok("ListTypes", &[]);
ok("ListTypeRegistrations", &[]);
ok("ListTypeVersions", &[]);
ok(
"BatchDescribeTypeConfigurations",
&[("TypeConfigurationIdentifiers.member.1.Type", "RESOURCE")],
);
ok("SetTypeConfiguration", &[("Configuration", "{}")]);
ok("SetTypeDefaultVersion", &[]);
ok("TestType", &[]);
ok("PublishType", &[]);
ok("RegisterPublisher", &[]);
ok("DescribePublisher", &[]);
}
#[test]
fn templates_resource_scans_drift() {
ok(
"CreateGeneratedTemplate",
&[("GeneratedTemplateName", "gt")],
);
ok(
"UpdateGeneratedTemplate",
&[("GeneratedTemplateName", "gt")],
);
ok(
"DescribeGeneratedTemplate",
&[("GeneratedTemplateName", "gt")],
);
ok("GetGeneratedTemplate", &[("GeneratedTemplateName", "gt")]);
ok("ListGeneratedTemplates", &[]);
ok(
"DeleteGeneratedTemplate",
&[("GeneratedTemplateName", "gt")],
);
ok("StartResourceScan", &[]);
ok("DescribeResourceScan", &[("ResourceScanId", "rs")]);
ok("ListResourceScans", &[]);
ok("ListResourceScanResources", &[("ResourceScanId", "rs")]);
ok(
"ListResourceScanRelatedResources",
&[
("ResourceScanId", "rs"),
("Resources.member.1.ResourceType", "AWS::SQS::Queue"),
],
);
ok("DetectStackDrift", &[("StackName", "s")]);
ok(
"DetectStackResourceDrift",
&[("StackName", "s"), ("LogicalResourceId", "L")],
);
ok("DetectStackSetDrift", &[("StackSetName", "ss")]);
ok(
"DescribeStackDriftDetectionStatus",
&[("StackDriftDetectionId", "id")],
);
ok("DescribeStackResourceDrifts", &[("StackName", "s")]);
ok(
"DescribeStackResource",
&[("StackName", "s"), ("LogicalResourceId", "L")],
);
}
#[test]
fn events_hooks_imports_policies_org() {
ok("DescribeStackEvents", &[("StackName", "s")]);
ok("DescribeEvents", &[]);
ok("GetHookResult", &[]);
ok("ListHookResults", &[]);
ok(
"RecordHandlerProgress",
&[("BearerToken", "tok"), ("OperationStatus", "SUCCESS")],
);
ok("ListExports", &[]);
ok("ListImports", &[("ExportName", "SomeExport")]);
ok("GetStackPolicy", &[("StackName", "s")]);
ok("SetStackPolicy", &[("StackName", "s")]);
ok(
"UpdateTerminationProtection",
&[("StackName", "s"), ("EnableTerminationProtection", "false")],
);
ok("DescribeAccountLimits", &[]);
ok("ActivateOrganizationsAccess", &[]);
ok("DescribeOrganizationsAccess", &[]);
ok("DeactivateOrganizationsAccess", &[]);
ok("ValidateTemplate", &[]);
ok("EstimateTemplateCost", &[]);
ok("GetTemplateSummary", &[]);
ok("CancelUpdateStack", &[("StackName", "s")]);
ok("ContinueUpdateRollback", &[("StackName", "s")]);
ok("RollbackStack", &[("StackName", "s")]);
ok(
"SignalResource",
&[
("StackName", "s"),
("LogicalResourceId", "L"),
("UniqueId", "U"),
("Status", "SUCCESS"),
],
);
}
}