1use http::StatusCode;
14use serde_json::{json, Value};
15use std::collections::HashMap;
16
17use fakecloud_aws::xml::xml_escape;
18use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
19
20use crate::service::CloudFormationService;
21
22const NS: &str = "http://cloudformation.amazonaws.com/doc/2010-05-15/";
23
24fn rand_id() -> String {
25 use std::sync::atomic::{AtomicU64, Ordering};
26 static COUNTER: AtomicU64 = AtomicU64::new(0);
27 let nanos = std::time::SystemTime::now()
28 .duration_since(std::time::UNIX_EPOCH)
29 .map(|d| d.as_nanos())
30 .unwrap_or(0);
31 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
32 format!("{nanos:x}-{seq:x}")
33}
34
35fn xml_response(action: &str, inner: String, request_id: &str) -> AwsResponse {
36 let body = format!(
37 r#"<{action}Response xmlns="{NS}">
38 <{action}Result>
39{inner}
40 </{action}Result>
41 <ResponseMetadata>
42 <RequestId>{rid}</RequestId>
43 </ResponseMetadata>
44</{action}Response>"#,
45 action = action,
46 NS = NS,
47 inner = inner,
48 rid = xml_escape(request_id),
49 );
50 AwsResponse::xml(StatusCode::OK, body)
51}
52
53fn xml_response_no_result(action: &str, request_id: &str) -> AwsResponse {
54 let body = format!(
55 r#"<{action}Response xmlns="{NS}">
56 <ResponseMetadata>
57 <RequestId>{rid}</RequestId>
58 </ResponseMetadata>
59</{action}Response>"#,
60 action = action,
61 NS = NS,
62 rid = xml_escape(request_id),
63 );
64 AwsResponse::xml(StatusCode::OK, body)
65}
66
67fn members_xml<F>(items: &[Value], render: F) -> String
68where
69 F: Fn(&Value) -> String,
70{
71 items
72 .iter()
73 .map(|v| format!(" <member>\n{}\n </member>", render(v)))
74 .collect::<Vec<_>>()
75 .join("\n")
76}
77
78fn store<'a>(
79 extras: &'a mut HashMap<String, HashMap<String, Value>>,
80 category: &str,
81) -> &'a mut HashMap<String, Value> {
82 extras.entry(category.to_string()).or_default()
83}
84
85fn missing(name: &str) -> AwsServiceError {
86 AwsServiceError::aws_error(
87 StatusCode::BAD_REQUEST,
88 "ValidationError",
89 format!("{name} is required"),
90 )
91}
92
93impl CloudFormationService {
94 pub(crate) fn handle_extra_action(
95 &self,
96 req: &AwsRequest,
97 ) -> Result<AwsResponse, AwsServiceError> {
98 let action = req.action.clone();
99 let params = Self::get_all_params(req);
100 let aid = req.account_id.clone();
101 let rid = req.request_id.clone();
102
103 match action.as_str() {
104 "CreateChangeSet" => {
106 let stack_name = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
107 let cs_name = params.get("ChangeSetName").ok_or_else(|| missing("ChangeSetName"))?.clone();
108 let id = format!("arn:aws:cloudformation:us-east-1:{aid}:changeSet/{cs_name}/{}", rand_id());
109 let stack_id = format!("arn:aws:cloudformation:us-east-1:{aid}:stack/{stack_name}/{}", rand_id());
110 let entry = json!({
111 "Id": id,
112 "ChangeSetName": cs_name,
113 "StackId": stack_id,
114 "StackName": stack_name,
115 "Status": "CREATE_COMPLETE",
116 "ExecutionStatus": "AVAILABLE",
117 "Changes": [],
118 });
119 let mut accounts = self.state.write();
120 let state = accounts.get_or_create(&aid);
121 store(&mut state.extras, "change_sets").insert(id.clone(), entry);
122 Ok(xml_response(
123 "CreateChangeSet",
124 format!(" <Id>{}</Id>\n <StackId>{}</StackId>", xml_escape(&id), xml_escape(&stack_id)),
125 &rid,
126 ))
127 }
128 "DescribeChangeSet" => {
129 let cs = params.get("ChangeSetName").ok_or_else(|| missing("ChangeSetName"))?.clone();
130 let accounts = self.state.read();
131 let entry = accounts.get(&aid)
132 .and_then(|s| s.extras.get("change_sets"))
133 .and_then(|m| m.values().find(|v| v["Id"].as_str() == Some(&cs) || v["ChangeSetName"].as_str() == Some(&cs)))
134 .cloned()
135 .unwrap_or_else(|| json!({"ChangeSetName": cs.clone(), "Status": "CREATE_COMPLETE", "ExecutionStatus": "AVAILABLE"}));
136 let inner = format!(
137 " <ChangeSetName>{}</ChangeSetName>\n <Status>{}</Status>\n <ExecutionStatus>{}</ExecutionStatus>\n <Changes/>",
138 xml_escape(entry["ChangeSetName"].as_str().unwrap_or("")),
139 xml_escape(entry["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
140 xml_escape(entry["ExecutionStatus"].as_str().unwrap_or("AVAILABLE")),
141 );
142 Ok(xml_response("DescribeChangeSet", inner, &rid))
143 }
144 "DescribeChangeSetHooks" => Ok(xml_response(
145 "DescribeChangeSetHooks",
146 " <Hooks/>".to_string(),
147 &rid,
148 )),
149 "DeleteChangeSet" => {
150 let cs = params.get("ChangeSetName").ok_or_else(|| missing("ChangeSetName"))?.clone();
151 let mut accounts = self.state.write();
152 let state = accounts.get_or_create(&aid);
153 if let Some(m) = state.extras.get_mut("change_sets") {
154 m.retain(|_, v| v["Id"].as_str() != Some(&cs) && v["ChangeSetName"].as_str() != Some(&cs));
155 }
156 Ok(xml_response("DeleteChangeSet", String::new(), &rid))
157 }
158 "ExecuteChangeSet" => Ok(xml_response("ExecuteChangeSet", String::new(), &rid)),
159 "ListChangeSets" => {
160 let accounts = self.state.read();
161 let items: Vec<Value> = accounts.get(&aid)
162 .and_then(|s| s.extras.get("change_sets"))
163 .map(|m| m.values().cloned().collect())
164 .unwrap_or_default();
165 let inner = format!(
166 " <Summaries>\n{}\n </Summaries>",
167 members_xml(&items, |v| format!(
168 " <ChangeSetId>{}</ChangeSetId>\n <ChangeSetName>{}</ChangeSetName>\n <Status>{}</Status>",
169 xml_escape(v["Id"].as_str().unwrap_or("")),
170 xml_escape(v["ChangeSetName"].as_str().unwrap_or("")),
171 xml_escape(v["Status"].as_str().unwrap_or("CREATE_COMPLETE")),
172 )),
173 );
174 Ok(xml_response("ListChangeSets", inner, &rid))
175 }
176
177 "CreateStackSet" => {
179 let name = params.get("StackSetName").ok_or_else(|| missing("StackSetName"))?.clone();
180 let id = format!("{name}:{}", rand_id());
181 let entry = json!({
182 "StackSetId": id,
183 "StackSetName": name,
184 "Status": "ACTIVE",
185 "TemplateBody": params.get("TemplateBody").cloned().unwrap_or_default(),
186 });
187 let mut accounts = self.state.write();
188 let state = accounts.get_or_create(&aid);
189 store(&mut state.extras, "stack_sets").insert(name.clone(), entry);
190 Ok(xml_response("CreateStackSet", format!(" <StackSetId>{}</StackSetId>", xml_escape(&id)), &rid))
191 }
192 "DescribeStackSet" => {
193 let name = params.get("StackSetName").ok_or_else(|| missing("StackSetName"))?.clone();
194 let accounts = self.state.read();
195 let entry = accounts.get(&aid)
196 .and_then(|s| s.extras.get("stack_sets"))
197 .and_then(|m| m.get(&name))
198 .cloned()
199 .unwrap_or_else(|| json!({"StackSetName": name.clone(), "Status": "ACTIVE"}));
200 let inner = format!(
201 " <StackSet>\n <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>\n </StackSet>",
202 xml_escape(entry["StackSetName"].as_str().unwrap_or(&name)),
203 xml_escape(entry["StackSetId"].as_str().unwrap_or("")),
204 xml_escape(entry["Status"].as_str().unwrap_or("ACTIVE")),
205 );
206 Ok(xml_response("DescribeStackSet", inner, &rid))
207 }
208 "ListStackSets" => {
209 let accounts = self.state.read();
210 let items: Vec<Value> = accounts.get(&aid)
211 .and_then(|s| s.extras.get("stack_sets"))
212 .map(|m| m.values().cloned().collect())
213 .unwrap_or_default();
214 let inner = format!(
215 " <Summaries>\n{}\n </Summaries>",
216 members_xml(&items, |v| format!(
217 " <StackSetName>{}</StackSetName>\n <StackSetId>{}</StackSetId>\n <Status>{}</Status>",
218 xml_escape(v["StackSetName"].as_str().unwrap_or("")),
219 xml_escape(v["StackSetId"].as_str().unwrap_or("")),
220 xml_escape(v["Status"].as_str().unwrap_or("ACTIVE")),
221 )),
222 );
223 Ok(xml_response("ListStackSets", inner, &rid))
224 }
225 "UpdateStackSet" => {
226 let op_id = rand_id();
227 Ok(xml_response("UpdateStackSet", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
228 }
229 "DeleteStackSet" => {
230 let name = params.get("StackSetName").ok_or_else(|| missing("StackSetName"))?.clone();
231 let mut accounts = self.state.write();
232 let state = accounts.get_or_create(&aid);
233 if let Some(m) = state.extras.get_mut("stack_sets") {
234 m.remove(&name);
235 }
236 Ok(xml_response("DeleteStackSet", String::new(), &rid))
237 }
238 "DescribeStackSetOperation" => {
239 let op_id = params.get("OperationId").cloned().unwrap_or_else(rand_id);
240 let inner = format!(
241 " <StackSetOperation>\n <OperationId>{}</OperationId>\n <Status>SUCCEEDED</Status>\n </StackSetOperation>",
242 xml_escape(&op_id),
243 );
244 Ok(xml_response("DescribeStackSetOperation", inner, &rid))
245 }
246 "ListStackSetOperations" => Ok(xml_response("ListStackSetOperations", " <Summaries/>".to_string(), &rid)),
247 "ListStackSetOperationResults" => Ok(xml_response("ListStackSetOperationResults", " <Summaries/>".to_string(), &rid)),
248 "ListStackSetAutoDeploymentTargets" => Ok(xml_response("ListStackSetAutoDeploymentTargets", " <Summaries/>".to_string(), &rid)),
249 "StopStackSetOperation" => Ok(xml_response("StopStackSetOperation", String::new(), &rid)),
250 "ImportStacksToStackSet" => {
251 let op_id = rand_id();
252 Ok(xml_response("ImportStacksToStackSet", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
253 }
254
255 "CreateStackInstances" => {
257 let op_id = rand_id();
258 Ok(xml_response("CreateStackInstances", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
259 }
260 "UpdateStackInstances" => {
261 let op_id = rand_id();
262 Ok(xml_response("UpdateStackInstances", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
263 }
264 "DeleteStackInstances" => {
265 let op_id = rand_id();
266 Ok(xml_response("DeleteStackInstances", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
267 }
268 "DescribeStackInstance" => {
269 let inner = " <StackInstance>\n <Status>CURRENT</Status>\n </StackInstance>".to_string();
270 Ok(xml_response("DescribeStackInstance", inner, &rid))
271 }
272 "ListStackInstances" => Ok(xml_response("ListStackInstances", " <Summaries/>".to_string(), &rid)),
273 "ListStackInstanceResourceDrifts" => Ok(xml_response("ListStackInstanceResourceDrifts", " <Summaries/>".to_string(), &rid)),
274
275 "CreateStackRefactor" => {
277 let id = rand_id();
278 let entry = json!({"StackRefactorId": id.clone(), "Status": "CREATE_COMPLETE"});
279 let mut accounts = self.state.write();
280 let state = accounts.get_or_create(&aid);
281 store(&mut state.extras, "refactors").insert(id.clone(), entry);
282 Ok(xml_response("CreateStackRefactor", format!(" <StackRefactorId>{}</StackRefactorId>", xml_escape(&id)), &rid))
283 }
284 "DescribeStackRefactor" => {
285 let id = params.get("StackRefactorId").ok_or_else(|| missing("StackRefactorId"))?.clone();
286 let inner = format!(
287 " <StackRefactorId>{}</StackRefactorId>\n <Status>CREATE_COMPLETE</Status>",
288 xml_escape(&id),
289 );
290 Ok(xml_response("DescribeStackRefactor", inner, &rid))
291 }
292 "ExecuteStackRefactor" => Ok(xml_response("ExecuteStackRefactor", String::new(), &rid)),
293 "ListStackRefactors" => Ok(xml_response("ListStackRefactors", " <StackRefactorSummaries/>".to_string(), &rid)),
294 "ListStackRefactorActions" => Ok(xml_response("ListStackRefactorActions", " <StackRefactorActions/>".to_string(), &rid)),
295
296 "ActivateType" => {
298 let arn = format!("arn:aws:cloudformation:us-east-1:{aid}:type/resource/{}", rand_id());
299 Ok(xml_response("ActivateType", format!(" <Arn>{}</Arn>", xml_escape(&arn)), &rid))
300 }
301 "DeactivateType" => Ok(xml_response("DeactivateType", String::new(), &rid)),
302 "DescribeType" => {
303 let arn = params.get("Arn").cloned().unwrap_or_else(|| format!("arn:aws:cloudformation:us-east-1:{aid}:type/resource/Default"));
304 let inner = format!(
305 " <Arn>{}</Arn>\n <Type>RESOURCE</Type>\n <TypeName>AWS::Custom::Type</TypeName>",
306 xml_escape(&arn),
307 );
308 Ok(xml_response("DescribeType", inner, &rid))
309 }
310 "DescribeTypeRegistration" => {
311 let token = params.get("RegistrationToken").cloned().unwrap_or_default();
312 let inner = format!(
313 " <ProgressStatus>COMPLETE</ProgressStatus>\n <Description>{}</Description>",
314 xml_escape(&token),
315 );
316 Ok(xml_response("DescribeTypeRegistration", inner, &rid))
317 }
318 "RegisterType" => {
319 let token = rand_id();
320 Ok(xml_response("RegisterType", format!(" <RegistrationToken>{}</RegistrationToken>", xml_escape(&token)), &rid))
321 }
322 "DeregisterType" => Ok(xml_response("DeregisterType", String::new(), &rid)),
323 "ListTypes" => Ok(xml_response("ListTypes", " <TypeSummaries/>".to_string(), &rid)),
324 "ListTypeRegistrations" => Ok(xml_response("ListTypeRegistrations", " <RegistrationTokenList/>".to_string(), &rid)),
325 "ListTypeVersions" => Ok(xml_response("ListTypeVersions", " <TypeVersionSummaries/>".to_string(), &rid)),
326 "BatchDescribeTypeConfigurations" => Ok(xml_response(
327 "BatchDescribeTypeConfigurations",
328 " <Errors/>\n <TypeConfigurations/>".to_string(),
329 &rid,
330 )),
331 "SetTypeConfiguration" => {
332 let arn = format!("arn:aws:cloudformation:us-east-1:{aid}:type-config/{}", rand_id());
333 Ok(xml_response("SetTypeConfiguration", format!(" <ConfigurationArn>{}</ConfigurationArn>", xml_escape(&arn)), &rid))
334 }
335 "SetTypeDefaultVersion" => Ok(xml_response("SetTypeDefaultVersion", String::new(), &rid)),
336 "TestType" => {
337 let arn = format!("arn:aws:cloudformation:us-east-1:{aid}:type/resource/{}", rand_id());
338 Ok(xml_response("TestType", format!(" <TypeVersionArn>{}</TypeVersionArn>", xml_escape(&arn)), &rid))
339 }
340 "PublishType" => {
341 let arn = format!("arn:aws:cloudformation:us-east-1:{aid}:type/resource/{}", rand_id());
342 Ok(xml_response("PublishType", format!(" <PublicTypeArn>{}</PublicTypeArn>", xml_escape(&arn)), &rid))
343 }
344 "RegisterPublisher" => {
345 let id = rand_id();
346 Ok(xml_response("RegisterPublisher", format!(" <PublisherId>{}</PublisherId>", xml_escape(&id)), &rid))
347 }
348 "DescribePublisher" => {
349 let id = params.get("PublisherId").cloned().unwrap_or_else(|| "default-publisher".to_string());
350 let inner = format!(
351 " <PublisherId>{}</PublisherId>\n <PublisherStatus>VERIFIED</PublisherStatus>\n <IdentityProvider>AWS_Marketplace</IdentityProvider>",
352 xml_escape(&id),
353 );
354 Ok(xml_response("DescribePublisher", inner, &rid))
355 }
356
357 "CreateGeneratedTemplate" => {
359 let name = params.get("GeneratedTemplateName").ok_or_else(|| missing("GeneratedTemplateName"))?.clone();
360 let id = format!("arn:aws:cloudformation:us-east-1:{aid}:generatedtemplate/{}", rand_id());
361 let entry = json!({"GeneratedTemplateId": id.clone(), "Name": name.clone(), "Status": "COMPLETE"});
362 let mut accounts = self.state.write();
363 let state = accounts.get_or_create(&aid);
364 store(&mut state.extras, "generated_templates").insert(name.clone(), entry);
365 Ok(xml_response("CreateGeneratedTemplate", format!(" <GeneratedTemplateId>{}</GeneratedTemplateId>", xml_escape(&id)), &rid))
366 }
367 "UpdateGeneratedTemplate" => {
368 let name = params.get("GeneratedTemplateName").ok_or_else(|| missing("GeneratedTemplateName"))?.clone();
369 let id = format!("arn:aws:cloudformation:us-east-1:{aid}:generatedtemplate/{name}");
370 Ok(xml_response("UpdateGeneratedTemplate", format!(" <GeneratedTemplateId>{}</GeneratedTemplateId>", xml_escape(&id)), &rid))
371 }
372 "DescribeGeneratedTemplate" => {
373 let name = params.get("GeneratedTemplateName").ok_or_else(|| missing("GeneratedTemplateName"))?.clone();
374 let inner = format!(
375 " <GeneratedTemplateId>arn:aws:cloudformation:us-east-1:{}:generatedtemplate/{}</GeneratedTemplateId>\n <GeneratedTemplateName>{}</GeneratedTemplateName>\n <Status>COMPLETE</Status>",
376 xml_escape(&aid),
377 xml_escape(&name),
378 xml_escape(&name),
379 );
380 Ok(xml_response("DescribeGeneratedTemplate", inner, &rid))
381 }
382 "GetGeneratedTemplate" => Ok(xml_response("GetGeneratedTemplate", " <Status>COMPLETE</Status>\n <TemplateBody>{}</TemplateBody>".to_string(), &rid)),
383 "DeleteGeneratedTemplate" => {
384 let name = params.get("GeneratedTemplateName").ok_or_else(|| missing("GeneratedTemplateName"))?.clone();
385 let mut accounts = self.state.write();
386 let state = accounts.get_or_create(&aid);
387 if let Some(m) = state.extras.get_mut("generated_templates") {
388 m.remove(&name);
389 }
390 Ok(xml_response("DeleteGeneratedTemplate", String::new(), &rid))
391 }
392 "ListGeneratedTemplates" => Ok(xml_response("ListGeneratedTemplates", " <Summaries/>".to_string(), &rid)),
393
394 "StartResourceScan" => {
396 let id = format!("arn:aws:cloudformation:us-east-1:{aid}:resourceScan/{}", rand_id());
397 Ok(xml_response("StartResourceScan", format!(" <ResourceScanId>{}</ResourceScanId>", xml_escape(&id)), &rid))
398 }
399 "DescribeResourceScan" => {
400 let id = params.get("ResourceScanId").cloned().unwrap_or_default();
401 let inner = format!(
402 " <ResourceScanId>{}</ResourceScanId>\n <Status>COMPLETE</Status>",
403 xml_escape(&id),
404 );
405 Ok(xml_response("DescribeResourceScan", inner, &rid))
406 }
407 "ListResourceScans" => Ok(xml_response("ListResourceScans", " <ResourceScanSummaries/>".to_string(), &rid)),
408 "ListResourceScanResources" => Ok(xml_response("ListResourceScanResources", " <Resources/>".to_string(), &rid)),
409 "ListResourceScanRelatedResources" => Ok(xml_response("ListResourceScanRelatedResources", " <RelatedResources/>".to_string(), &rid)),
410
411 "DetectStackDrift" => {
413 let id = rand_id();
414 Ok(xml_response("DetectStackDrift", format!(" <StackDriftDetectionId>{}</StackDriftDetectionId>", xml_escape(&id)), &rid))
415 }
416 "DetectStackResourceDrift" => Ok(xml_response(
417 "DetectStackResourceDrift",
418 " <StackResourceDrift>\n <StackResourceDriftStatus>IN_SYNC</StackResourceDriftStatus>\n </StackResourceDrift>".to_string(),
419 &rid,
420 )),
421 "DetectStackSetDrift" => {
422 let op_id = rand_id();
423 Ok(xml_response("DetectStackSetDrift", format!(" <OperationId>{}</OperationId>", xml_escape(&op_id)), &rid))
424 }
425 "DescribeStackDriftDetectionStatus" => {
426 let id = params.get("StackDriftDetectionId").cloned().unwrap_or_default();
427 let inner = format!(
428 " <StackDriftDetectionId>{}</StackDriftDetectionId>\n <DetectionStatus>DETECTION_COMPLETE</DetectionStatus>\n <StackDriftStatus>IN_SYNC</StackDriftStatus>",
429 xml_escape(&id),
430 );
431 Ok(xml_response("DescribeStackDriftDetectionStatus", inner, &rid))
432 }
433 "DescribeStackResourceDrifts" => Ok(xml_response("DescribeStackResourceDrifts", " <StackResourceDrifts/>".to_string(), &rid)),
434 "DescribeStackResource" => {
435 let stack_name = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
436 let logical = params.get("LogicalResourceId").ok_or_else(|| missing("LogicalResourceId"))?.clone();
437 let accounts = self.state.read();
438 let detail = accounts.get(&aid)
439 .and_then(|s| s.stacks.get(&stack_name))
440 .and_then(|s| s.resources.iter().find(|r| r.logical_id == logical))
441 .map(|r| (r.physical_id.clone(), r.resource_type.clone(), r.status.clone()))
442 .unwrap_or_else(|| ("pid".to_string(), "AWS::Custom".to_string(), "CREATE_COMPLETE".to_string()));
443 let inner = format!(
444 " <StackResourceDetail>\n <StackName>{}</StackName>\n <LogicalResourceId>{}</LogicalResourceId>\n <PhysicalResourceId>{}</PhysicalResourceId>\n <ResourceType>{}</ResourceType>\n <ResourceStatus>{}</ResourceStatus>\n <LastUpdatedTimestamp>{}</LastUpdatedTimestamp>\n </StackResourceDetail>",
445 xml_escape(&stack_name),
446 xml_escape(&logical),
447 xml_escape(&detail.0),
448 xml_escape(&detail.1),
449 xml_escape(&detail.2),
450 chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ"),
451 );
452 Ok(xml_response("DescribeStackResource", inner, &rid))
453 }
454
455 "DescribeStackEvents" => Ok(xml_response("DescribeStackEvents", " <StackEvents/>".to_string(), &rid)),
457 "DescribeEvents" => Ok(xml_response("DescribeEvents", " <Events/>".to_string(), &rid)),
458
459 "GetHookResult" => Ok(xml_response(
461 "GetHookResult",
462 " <Status>HOOK_COMPLETE_SUCCEEDED</Status>".to_string(),
463 &rid,
464 )),
465 "ListHookResults" => Ok(xml_response("ListHookResults", " <HookResults/>".to_string(), &rid)),
466 "RecordHandlerProgress" => Ok(xml_response_no_result("RecordHandlerProgress", &rid)),
467
468 "ListExports" => Ok(xml_response("ListExports", " <Exports/>".to_string(), &rid)),
470 "ListImports" => Ok(xml_response("ListImports", " <Imports/>".to_string(), &rid)),
471
472 "GetStackPolicy" => {
474 let stack = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
475 let accounts = self.state.read();
476 let body = accounts.get(&aid)
477 .and_then(|s| s.stack_policies.get(&stack))
478 .cloned()
479 .unwrap_or_else(|| r#"{"Statement":[{"Effect":"Allow","Action":"Update:*","Principal":"*","Resource":"*"}]}"#.to_string());
480 let inner = format!(" <StackPolicyBody>{}</StackPolicyBody>", xml_escape(&body));
481 Ok(xml_response("GetStackPolicy", inner, &rid))
482 }
483 "SetStackPolicy" => {
484 let stack = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
485 let body = params.get("StackPolicyBody").cloned().unwrap_or_default();
486 let mut accounts = self.state.write();
487 let state = accounts.get_or_create(&aid);
488 state.stack_policies.insert(stack, body);
489 Ok(xml_response_no_result("SetStackPolicy", &rid))
490 }
491
492 "UpdateTerminationProtection" => {
494 let stack = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
495 let enabled = params.get("EnableTerminationProtection")
496 .map(|v| v.eq_ignore_ascii_case("true"))
497 .unwrap_or(false);
498 let stack_id = {
499 let mut accounts = self.state.write();
500 let state = accounts.get_or_create(&aid);
501 state.termination_protection.insert(stack.clone(), enabled);
502 state.stacks.get(&stack).map(|s| s.stack_id.clone()).unwrap_or_else(|| stack.clone())
503 };
504 Ok(xml_response("UpdateTerminationProtection", format!(" <StackId>{}</StackId>", xml_escape(&stack_id)), &rid))
505 }
506
507 "DescribeAccountLimits" => Ok(xml_response(
509 "DescribeAccountLimits",
510 r#" <AccountLimits>
511 <member>
512 <Name>StackLimit</Name>
513 <Value>2000</Value>
514 </member>
515 </AccountLimits>"#.to_string(),
516 &rid,
517 )),
518 "ActivateOrganizationsAccess" => {
519 let mut accounts = self.state.write();
520 let state = accounts.get_or_create(&aid);
521 state.orgs_access_enabled = true;
522 Ok(xml_response("ActivateOrganizationsAccess", String::new(), &rid))
523 }
524 "DeactivateOrganizationsAccess" => {
525 let mut accounts = self.state.write();
526 let state = accounts.get_or_create(&aid);
527 state.orgs_access_enabled = false;
528 Ok(xml_response("DeactivateOrganizationsAccess", String::new(), &rid))
529 }
530 "DescribeOrganizationsAccess" => {
531 let accounts = self.state.read();
532 let status = if accounts.get(&aid).map(|s| s.orgs_access_enabled).unwrap_or(false) {
533 "ENABLED"
534 } else {
535 "DISABLED"
536 };
537 Ok(xml_response("DescribeOrganizationsAccess", format!(" <Status>{}</Status>", status), &rid))
538 }
539 "ValidateTemplate" => Ok(xml_response(
540 "ValidateTemplate",
541 " <Description>Validated</Description>\n <Capabilities/>\n <Parameters/>".to_string(),
542 &rid,
543 )),
544 "EstimateTemplateCost" => Ok(xml_response(
545 "EstimateTemplateCost",
546 " <Url>https://calculator.aws/#/estimate</Url>".to_string(),
547 &rid,
548 )),
549 "GetTemplateSummary" => Ok(xml_response(
550 "GetTemplateSummary",
551 " <Parameters/>\n <ResourceTypes/>\n <Capabilities/>".to_string(),
552 &rid,
553 )),
554 "CancelUpdateStack" => Ok(xml_response_no_result("CancelUpdateStack", &rid)),
555 "ContinueUpdateRollback" => Ok(xml_response("ContinueUpdateRollback", String::new(), &rid)),
556 "RollbackStack" => {
557 let stack = params.get("StackName").ok_or_else(|| missing("StackName"))?.clone();
558 let stack_id = {
559 let accounts = self.state.read();
560 accounts.get(&aid).and_then(|s| s.stacks.get(&stack)).map(|s| s.stack_id.clone()).unwrap_or_else(|| stack.clone())
561 };
562 Ok(xml_response("RollbackStack", format!(" <StackId>{}</StackId>", xml_escape(&stack_id)), &rid))
563 }
564 "SignalResource" => Ok(xml_response_no_result("SignalResource", &rid)),
565
566 _ => Err(AwsServiceError::action_not_implemented("cloudformation", &action)),
567 }
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use crate::service::{CloudFormationDeps, CloudFormationService};
574 use crate::state::{CloudFormationState, SharedCloudFormationState};
575 use fakecloud_core::delivery::DeliveryBus;
576 use fakecloud_core::multi_account::MultiAccountState;
577 use fakecloud_core::service::AwsRequest;
578 use http::Method;
579 use parking_lot::RwLock;
580 use std::collections::HashMap;
581 use std::sync::Arc;
582
583 fn deps() -> CloudFormationDeps {
584 use fakecloud_dynamodb::state::DynamoDbState;
585 use fakecloud_eventbridge::state::EventBridgeState;
586 use fakecloud_iam::state::IamState;
587 use fakecloud_logs::state::LogsState;
588 use fakecloud_s3::state::S3State;
589 use fakecloud_sns::state::SnsState;
590 use fakecloud_sqs::state::SqsState;
591 use fakecloud_ssm::state::SsmState;
592
593 fn shared<T: fakecloud_core::multi_account::AccountState>(
594 ) -> Arc<RwLock<MultiAccountState<T>>> {
595 Arc::new(RwLock::new(MultiAccountState::<T>::new(
596 "000000000000",
597 "us-east-1",
598 "",
599 )))
600 }
601 CloudFormationDeps {
602 sqs: shared::<SqsState>(),
603 sns: shared::<SnsState>(),
604 ssm: shared::<SsmState>(),
605 iam: shared::<IamState>(),
606 s3: shared::<S3State>(),
607 eventbridge: shared::<EventBridgeState>(),
608 dynamodb: shared::<DynamoDbState>(),
609 logs: shared::<LogsState>(),
610 delivery: Arc::new(DeliveryBus::new()),
611 }
612 }
613
614 fn svc() -> CloudFormationService {
615 let state: SharedCloudFormationState =
616 Arc::new(RwLock::new(MultiAccountState::<CloudFormationState>::new(
617 "000000000000",
618 "us-east-1",
619 "",
620 )));
621 CloudFormationService::new(state, deps())
622 }
623
624 fn req(action: &str, params: &[(&str, &str)]) -> AwsRequest {
625 let mut q = HashMap::new();
626 q.insert("Action".to_string(), action.to_string());
627 for (k, v) in params {
628 q.insert(k.to_string(), v.to_string());
629 }
630 AwsRequest {
631 service: "cloudformation".to_string(),
632 method: Method::POST,
633 raw_path: "/".to_string(),
634 raw_query: String::new(),
635 path_segments: vec![],
636 query_params: q,
637 headers: http::HeaderMap::new(),
638 body: bytes::Bytes::new(),
639 account_id: "000000000000".to_string(),
640 region: "us-east-1".to_string(),
641 request_id: "rid".to_string(),
642 action: action.to_string(),
643 is_query_protocol: true,
644 access_key_id: None,
645 principal: None,
646 }
647 }
648
649 fn ok(action: &str, params: &[(&str, &str)]) {
650 let r = svc().handle_extra_action(&req(action, params));
651 match r {
652 Ok(resp) => assert!(resp.status.is_success(), "{action} status: {}", resp.status),
653 Err(e) => panic!("{action} failed: {e:?}"),
654 }
655 }
656
657 #[test]
658 fn change_sets() {
659 ok(
660 "CreateChangeSet",
661 &[("StackName", "s"), ("ChangeSetName", "cs")],
662 );
663 ok("DescribeChangeSet", &[("ChangeSetName", "cs")]);
664 ok("DescribeChangeSetHooks", &[]);
665 ok("ListChangeSets", &[]);
666 ok("ExecuteChangeSet", &[]);
667 ok("DeleteChangeSet", &[("ChangeSetName", "cs")]);
668 }
669
670 #[test]
671 fn stack_sets_instances_refactors() {
672 ok("CreateStackSet", &[("StackSetName", "ss")]);
673 ok("DescribeStackSet", &[("StackSetName", "ss")]);
674 ok("ListStackSets", &[]);
675 ok("UpdateStackSet", &[]);
676 ok("DescribeStackSetOperation", &[]);
677 ok("ListStackSetOperations", &[]);
678 ok("ListStackSetOperationResults", &[]);
679 ok("ListStackSetAutoDeploymentTargets", &[]);
680 ok("StopStackSetOperation", &[]);
681 ok("ImportStacksToStackSet", &[]);
682 ok("DeleteStackSet", &[("StackSetName", "ss")]);
683 ok("CreateStackInstances", &[]);
684 ok("UpdateStackInstances", &[]);
685 ok("DeleteStackInstances", &[]);
686 ok("DescribeStackInstance", &[]);
687 ok("ListStackInstances", &[]);
688 ok("ListStackInstanceResourceDrifts", &[]);
689 ok("CreateStackRefactor", &[]);
690 ok("DescribeStackRefactor", &[("StackRefactorId", "r")]);
691 ok("ExecuteStackRefactor", &[]);
692 ok("ListStackRefactors", &[]);
693 ok("ListStackRefactorActions", &[]);
694 }
695
696 #[test]
697 fn types_and_publishers() {
698 ok("ActivateType", &[]);
699 ok("DeactivateType", &[]);
700 ok("DescribeType", &[]);
701 ok("DescribeTypeRegistration", &[]);
702 ok("RegisterType", &[]);
703 ok("DeregisterType", &[]);
704 ok("ListTypes", &[]);
705 ok("ListTypeRegistrations", &[]);
706 ok("ListTypeVersions", &[]);
707 ok("BatchDescribeTypeConfigurations", &[]);
708 ok("SetTypeConfiguration", &[]);
709 ok("SetTypeDefaultVersion", &[]);
710 ok("TestType", &[]);
711 ok("PublishType", &[]);
712 ok("RegisterPublisher", &[]);
713 ok("DescribePublisher", &[]);
714 }
715
716 #[test]
717 fn templates_resource_scans_drift() {
718 ok(
719 "CreateGeneratedTemplate",
720 &[("GeneratedTemplateName", "gt")],
721 );
722 ok(
723 "UpdateGeneratedTemplate",
724 &[("GeneratedTemplateName", "gt")],
725 );
726 ok(
727 "DescribeGeneratedTemplate",
728 &[("GeneratedTemplateName", "gt")],
729 );
730 ok("GetGeneratedTemplate", &[]);
731 ok("ListGeneratedTemplates", &[]);
732 ok(
733 "DeleteGeneratedTemplate",
734 &[("GeneratedTemplateName", "gt")],
735 );
736 ok("StartResourceScan", &[]);
737 ok("DescribeResourceScan", &[]);
738 ok("ListResourceScans", &[]);
739 ok("ListResourceScanResources", &[]);
740 ok("ListResourceScanRelatedResources", &[]);
741 ok("DetectStackDrift", &[]);
742 ok("DetectStackResourceDrift", &[]);
743 ok("DetectStackSetDrift", &[]);
744 ok("DescribeStackDriftDetectionStatus", &[]);
745 ok("DescribeStackResourceDrifts", &[]);
746 ok(
747 "DescribeStackResource",
748 &[("StackName", "s"), ("LogicalResourceId", "L")],
749 );
750 }
751
752 #[test]
753 fn events_hooks_imports_policies_org() {
754 ok("DescribeStackEvents", &[]);
755 ok("DescribeEvents", &[]);
756 ok("GetHookResult", &[]);
757 ok("ListHookResults", &[]);
758 ok("RecordHandlerProgress", &[]);
759 ok("ListExports", &[]);
760 ok("ListImports", &[]);
761 ok("GetStackPolicy", &[("StackName", "s")]);
762 ok("SetStackPolicy", &[("StackName", "s")]);
763 ok("UpdateTerminationProtection", &[("StackName", "s")]);
764 ok("DescribeAccountLimits", &[]);
765 ok("ActivateOrganizationsAccess", &[]);
766 ok("DescribeOrganizationsAccess", &[]);
767 ok("DeactivateOrganizationsAccess", &[]);
768 ok("ValidateTemplate", &[]);
769 ok("EstimateTemplateCost", &[]);
770 ok("GetTemplateSummary", &[]);
771 ok("CancelUpdateStack", &[]);
772 ok("ContinueUpdateRollback", &[]);
773 ok("RollbackStack", &[("StackName", "s")]);
774 ok("SignalResource", &[]);
775 }
776}