1use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
2use crate::aws::credential_provider::AwsCredentialProvider;
3use alien_client_core::{ErrorData, Result};
4
5use alien_error::ContextError;
6use bon::Builder;
7use form_urlencoded;
8use quick_xml;
9use reqwest::{Client, StatusCode};
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "test-utils")]
14use mockall::automock;
15
16#[cfg_attr(feature = "test-utils", automock)]
17#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
18#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
19pub trait CloudFormationApi: Send + Sync + std::fmt::Debug {
20 async fn create_stack(&self, request: CreateStackRequest) -> Result<CreateStackResponse>;
21 async fn describe_stacks(
22 &self,
23 request: DescribeStacksRequest,
24 ) -> Result<DescribeStacksResponse>;
25 async fn delete_stack(&self, request: DeleteStackRequest) -> Result<DeleteStackResponse>;
26 async fn describe_stack_resources(
27 &self,
28 request: DescribeStackResourcesRequest,
29 ) -> Result<DescribeStackResourcesResponse>;
30 async fn describe_stack_resource(
31 &self,
32 request: DescribeStackResourceRequest,
33 ) -> Result<DescribeStackResourceResponse>;
34 async fn describe_stack_events(
35 &self,
36 request: DescribeStackEventsRequest,
37 ) -> Result<DescribeStackEventsResponse>;
38}
39
40#[derive(Debug, Clone)]
42pub struct CloudFormationClient {
43 client: Client,
44 credentials: AwsCredentialProvider,
45}
46
47impl CloudFormationClient {
48 pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
49 Self {
50 client,
51 credentials,
52 }
53 }
54
55 fn sign_config(&self) -> AwsSignConfig {
56 AwsSignConfig {
57 service_name: "cloudformation".into(),
58 region: self.credentials.region().to_string(),
59 credentials: self.credentials.get_credentials(),
60 signing_region: None,
61 }
62 }
63
64 fn get_base_url(&self) -> String {
65 if let Some(override_url) = self
66 .credentials
67 .get_service_endpoint_option("cloudformation")
68 {
69 override_url.to_string()
70 } else {
71 format!(
72 "https://cloudformation.{}.amazonaws.com",
73 self.credentials.region()
74 )
75 }
76 }
77
78 fn build_form_body(action: &str, version: &str, params: Vec<(String, String)>) -> String {
79 let mut all = vec![
80 ("Action".to_string(), action.to_string()),
81 ("Version".to_string(), version.to_string()),
82 ];
83 all.extend(params);
84 all.into_iter()
85 .map(|(k, v)| {
86 format!(
87 "{}={}",
88 k,
89 form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
90 )
91 })
92 .collect::<Vec<String>>()
93 .join("&")
94 }
95
96 async fn post_xml<T: DeserializeOwned + Send + 'static>(
97 &self,
98 body: String,
99 operation: &str,
100 resource_name: &str,
101 ) -> Result<T> {
102 self.credentials.ensure_fresh().await?;
103 let base_url = self.get_base_url();
104 let url = format!("{}/", base_url.trim_end_matches('/'));
105 let body_for_error = body.clone();
106 let builder = self
107 .client
108 .post(&url)
109 .host(&format!(
110 "cloudformation.{}.amazonaws.com",
111 self.credentials.region()
112 ))
113 .content_type_form()
114 .body(body);
115
116 let result =
117 crate::aws::aws_request_utils::sign_send_xml(builder, &self.sign_config()).await;
118
119 match result {
120 Ok(v) => Ok(v),
121 Err(e) => {
122 if let Some(ErrorData::HttpResponseError {
123 http_status,
124 http_response_text: Some(ref text),
125 ..
126 }) = &e.error
127 {
128 let status = StatusCode::from_u16(*http_status)
129 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
130 if let Some(mapped) = Self::map_cfn_error(
131 status,
132 text,
133 operation,
134 resource_name,
135 Some(&body_for_error),
136 ) {
137 Err(e.context(mapped))
138 } else {
139 Err(e)
141 }
142 } else {
143 Err(e)
144 }
145 }
146 }
147 }
148
149 fn map_cfn_error(
150 status: StatusCode,
151 error_body: &str,
152 operation: &str,
153 resource_name: &str,
154 request_body: Option<&str>,
155 ) -> Option<ErrorData> {
156 let parsed: std::result::Result<CloudFormationErrorResponse, _> =
158 quick_xml::de::from_str(error_body);
159 let (code, message) = match parsed {
160 Ok(e) => (
161 e.error.code.unwrap_or_else(|| "UnknownErrorCode".into()),
162 e.error.message.unwrap_or_else(|| "Unknown error".into()),
163 ),
164 Err(_) => {
165 return None;
167 }
168 };
169
170 Some(match code.as_str() {
171 "AccessDenied"
172 | "AccessDeniedException"
173 | "UnauthorizedOperation"
174 | "AuthFailure"
175 | "SignatureDoesNotMatch" => ErrorData::RemoteAccessDenied {
176 resource_type: "CloudFormation Stack".into(),
177 resource_name: resource_name.into(),
178 },
179 "Throttling" | "ThrottlingException" | "RequestLimitExceeded" => {
180 ErrorData::RateLimitExceeded { message }
181 }
182 "ServiceUnavailable" | "InternalFailure" => {
183 ErrorData::RemoteServiceUnavailable { message }
184 }
185 "AlreadyExists" | "AlreadyExistsException" => ErrorData::RemoteResourceConflict {
186 message,
187 resource_type: "CloudFormation Stack".into(),
188 resource_name: resource_name.into(),
189 },
190 "LimitExceeded" | "LimitExceededException" => ErrorData::QuotaExceeded { message },
191 "ValidationError" if message.contains("does not exist") => {
192 ErrorData::RemoteResourceNotFound {
193 resource_type: "CloudFormation Stack".into(),
194 resource_name: resource_name.into(),
195 }
196 }
197 _ => match status {
198 StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
199 message,
200 resource_type: "CloudFormation Stack".into(),
201 resource_name: resource_name.into(),
202 },
203 StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
204 resource_type: "CloudFormation Stack".into(),
205 resource_name: resource_name.into(),
206 },
207 StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
208 resource_type: "CloudFormation Stack".into(),
209 resource_name: resource_name.into(),
210 },
211 StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
212 StatusCode::SERVICE_UNAVAILABLE
213 | StatusCode::BAD_GATEWAY
214 | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
215 _ => ErrorData::HttpResponseError {
216 message: format!("CloudFormation {operation} failed: {message}"),
217 url: format!("cloudformation.amazonaws.com"),
218 http_status: status.as_u16(),
219 http_request_text: request_body.map(|s| s.to_string()),
220 http_response_text: Some(error_body.into()),
221 },
222 },
223 })
224 }
225}
226
227#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
228#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
229impl CloudFormationApi for CloudFormationClient {
230 async fn create_stack(&self, request: CreateStackRequest) -> Result<CreateStackResponse> {
231 let mut params = vec![
232 ("StackName".to_string(), request.stack_name.clone()),
233 ("TemplateBody".to_string(), request.template_body),
234 ];
235
236 if let Some(description) = request.description {
237 params.push(("Description".to_string(), description));
238 }
239
240 if let Some(timeout) = request.timeout_in_minutes {
241 params.push(("TimeoutInMinutes".to_string(), timeout.to_string()));
242 }
243
244 if let Some(ref capabilities) = request.capabilities {
245 for (i, capability) in capabilities.iter().enumerate() {
246 params.push((format!("Capabilities.member.{}", i + 1), capability.clone()));
247 }
248 }
249
250 let body = Self::build_form_body("CreateStack", "2010-05-15", params);
251 let stack_name = &request.stack_name;
252 self.post_xml(body, "CreateStack", stack_name).await
253 }
254
255 async fn describe_stacks(
256 &self,
257 request: DescribeStacksRequest,
258 ) -> Result<DescribeStacksResponse> {
259 let mut params = Vec::new();
260 if let Some(ref stack_name) = request.stack_name {
261 params.push(("StackName".to_string(), stack_name.clone()));
262 }
263
264 let body = Self::build_form_body("DescribeStacks", "2010-05-15", params);
265 let resource_name = request.stack_name.as_deref().unwrap_or("(unknown)");
266 self.post_xml(body, "DescribeStacks", resource_name).await
267 }
268
269 async fn delete_stack(&self, request: DeleteStackRequest) -> Result<DeleteStackResponse> {
270 let params = vec![("StackName".to_string(), request.stack_name.clone())];
271
272 let body = Self::build_form_body("DeleteStack", "2010-05-15", params);
273 self.post_xml(body, "DeleteStack", &request.stack_name)
274 .await
275 }
276
277 async fn describe_stack_resources(
278 &self,
279 request: DescribeStackResourcesRequest,
280 ) -> Result<DescribeStackResourcesResponse> {
281 let mut params = Vec::new();
282 if let Some(ref stack_name) = request.stack_name {
283 params.push(("StackName".to_string(), stack_name.clone()));
284 }
285 if let Some(ref logical_id) = request.logical_resource_id {
286 params.push(("LogicalResourceId".to_string(), logical_id.clone()));
287 }
288 if let Some(ref physical_id) = request.physical_resource_id {
289 params.push(("PhysicalResourceId".to_string(), physical_id.clone()));
290 }
291
292 let body = Self::build_form_body("DescribeStackResources", "2010-05-15", params);
293 self.post_xml(
294 body,
295 "DescribeStackResources",
296 request.stack_name.as_deref().unwrap_or("(unknown)"),
297 )
298 .await
299 }
300
301 async fn describe_stack_resource(
302 &self,
303 request: DescribeStackResourceRequest,
304 ) -> Result<DescribeStackResourceResponse> {
305 let params = vec![
306 ("StackName".to_string(), request.stack_name.clone()),
307 (
308 "LogicalResourceId".to_string(),
309 request.logical_resource_id.clone(),
310 ),
311 ];
312
313 let body = Self::build_form_body("DescribeStackResource", "2010-05-15", params);
314 self.post_xml(body, "DescribeStackResource", &request.stack_name)
315 .await
316 }
317
318 async fn describe_stack_events(
319 &self,
320 request: DescribeStackEventsRequest,
321 ) -> Result<DescribeStackEventsResponse> {
322 let params = vec![("StackName".to_string(), request.stack_name.clone())];
323
324 let body = Self::build_form_body("DescribeStackEvents", "2010-05-15", params);
325 self.post_xml(body, "DescribeStackEvents", &request.stack_name)
326 .await
327 }
328}
329
330#[derive(Deserialize, Debug)]
335#[serde(rename_all = "PascalCase")]
336struct CloudFormationErrorResponse {
337 pub error: CloudFormationErrorDetails,
338}
339
340#[derive(Deserialize, Debug)]
341#[serde(rename_all = "PascalCase")]
342struct CloudFormationErrorDetails {
343 pub code: Option<String>,
344 pub message: Option<String>,
345}
346
347#[derive(Serialize, Debug, Clone, Builder)]
352#[serde(rename_all = "PascalCase")]
353pub struct CreateStackRequest {
354 pub stack_name: String,
355 pub template_body: String,
356 pub description: Option<String>,
357 pub timeout_in_minutes: Option<i32>,
358 pub capabilities: Option<Vec<String>>,
359}
360
361#[derive(Deserialize, Debug)]
362#[serde(rename_all = "PascalCase")]
363pub struct CreateStackResponse {
364 pub create_stack_result: CreateStackResult,
365}
366
367#[derive(Deserialize, Debug)]
368#[serde(rename_all = "PascalCase")]
369pub struct CreateStackResult {
370 pub stack_id: String,
371}
372
373#[derive(Serialize, Debug, Clone, Builder)]
374#[serde(rename_all = "PascalCase")]
375pub struct DescribeStacksRequest {
376 pub stack_name: Option<String>,
377}
378
379#[derive(Deserialize, Debug)]
380#[serde(rename_all = "PascalCase")]
381pub struct DescribeStacksResponse {
382 pub describe_stacks_result: DescribeStacksResult,
383}
384
385#[derive(Deserialize, Debug)]
386#[serde(rename_all = "PascalCase")]
387pub struct DescribeStacksResult {
388 pub stacks: Stacks,
389}
390
391#[derive(Deserialize, Debug)]
392#[serde(rename_all = "PascalCase")]
393pub struct Stacks {
394 #[serde(rename = "member", default)]
395 pub member: Vec<Stack>,
396}
397
398#[derive(Deserialize, Debug)]
399#[serde(rename_all = "PascalCase")]
400pub struct Stack {
401 pub stack_id: String,
402 pub stack_name: String,
403 pub stack_status: String,
404 pub creation_time: String,
405 pub description: Option<String>,
406 pub capabilities: Option<Capabilities>,
407 pub outputs: Option<Outputs>,
408 pub parameters: Option<Parameters>,
409}
410
411#[derive(Deserialize, Debug)]
412#[serde(rename_all = "PascalCase")]
413pub struct Capabilities {
414 #[serde(rename = "member", default)]
415 pub member: Vec<String>,
416}
417
418#[derive(Deserialize, Debug)]
419#[serde(rename_all = "PascalCase")]
420pub struct Outputs {
421 #[serde(rename = "member", default)]
422 pub member: Vec<Output>,
423}
424
425#[derive(Deserialize, Debug)]
426#[serde(rename_all = "PascalCase")]
427pub struct Output {
428 pub output_key: String,
429 pub output_value: String,
430 pub description: Option<String>,
431}
432
433#[derive(Deserialize, Debug)]
434#[serde(rename_all = "PascalCase")]
435pub struct Parameters {
436 #[serde(rename = "member", default)]
437 pub member: Vec<Parameter>,
438}
439
440#[derive(Deserialize, Debug)]
441#[serde(rename_all = "PascalCase")]
442pub struct Parameter {
443 pub parameter_key: String,
444 pub parameter_value: Option<String>,
445}
446
447#[derive(Serialize, Debug, Clone, Builder)]
448#[serde(rename_all = "PascalCase")]
449pub struct DeleteStackRequest {
450 pub stack_name: String,
451}
452
453#[derive(Deserialize, Debug)]
454#[serde(rename_all = "PascalCase")]
455pub struct DeleteStackResponse {
456 }
458
459#[derive(Serialize, Debug, Clone, Builder)]
460#[serde(rename_all = "PascalCase")]
461pub struct DescribeStackResourcesRequest {
462 pub stack_name: Option<String>,
463 pub logical_resource_id: Option<String>,
464 pub physical_resource_id: Option<String>,
465}
466
467#[derive(Deserialize, Debug)]
468#[serde(rename_all = "PascalCase")]
469pub struct DescribeStackResourcesResponse {
470 pub describe_stack_resources_result: DescribeStackResourcesResult,
471}
472
473#[derive(Deserialize, Debug)]
474#[serde(rename_all = "PascalCase")]
475pub struct DescribeStackResourcesResult {
476 pub stack_resources: StackResources,
477}
478
479#[derive(Deserialize, Debug)]
480#[serde(rename_all = "PascalCase")]
481pub struct StackResources {
482 #[serde(rename = "member", default)]
483 pub member: Vec<StackResource>,
484}
485
486#[derive(Deserialize, Debug)]
487#[serde(rename_all = "PascalCase")]
488pub struct StackResource {
489 pub stack_name: Option<String>,
490 pub stack_id: Option<String>,
491 pub logical_resource_id: String,
492 pub physical_resource_id: Option<String>,
493 pub resource_type: String,
494 pub timestamp: String,
495 pub resource_status: String,
496 pub resource_status_reason: Option<String>,
497 pub description: Option<String>,
498 pub drift_information: Option<StackResourceDriftInformation>,
499 pub module_info: Option<ModuleInfo>,
500}
501
502#[derive(Deserialize, Debug)]
503#[serde(rename_all = "PascalCase")]
504pub struct StackResourceDriftInformation {
505 pub stack_resource_drift_status: String,
506 pub last_check_timestamp: Option<String>,
507}
508
509#[derive(Deserialize, Debug)]
510#[serde(rename_all = "PascalCase")]
511pub struct ModuleInfo {
512 pub type_hierarchy: Option<String>,
513 pub logical_id_hierarchy: Option<String>,
514}
515
516#[derive(Serialize, Debug, Clone, Builder)]
517#[serde(rename_all = "PascalCase")]
518pub struct DescribeStackResourceRequest {
519 pub stack_name: String,
520 pub logical_resource_id: String,
521}
522
523#[derive(Deserialize, Debug)]
524#[serde(rename_all = "PascalCase")]
525pub struct DescribeStackResourceResponse {
526 pub describe_stack_resource_result: DescribeStackResourceResult,
527}
528
529#[derive(Deserialize, Debug)]
530#[serde(rename_all = "PascalCase")]
531pub struct DescribeStackResourceResult {
532 pub stack_resource_detail: StackResourceDetail,
533}
534
535#[derive(Deserialize, Debug)]
536#[serde(rename_all = "PascalCase")]
537pub struct StackResourceDetail {
538 pub stack_name: Option<String>,
539 pub stack_id: Option<String>,
540 pub logical_resource_id: String,
541 pub physical_resource_id: Option<String>,
542 pub resource_type: String,
543 pub last_updated_timestamp: String,
544 pub resource_status: String,
545 pub resource_status_reason: Option<String>,
546 pub description: Option<String>,
547 pub metadata: Option<String>,
548 pub drift_information: Option<StackResourceDriftInformation>,
549 pub module_info: Option<ModuleInfo>,
550}
551
552#[derive(Serialize, Debug, Clone, Builder)]
553#[serde(rename_all = "PascalCase")]
554pub struct DescribeStackEventsRequest {
555 pub stack_name: String,
556}
557
558#[derive(Deserialize, Debug)]
559#[serde(rename_all = "PascalCase")]
560pub struct DescribeStackEventsResponse {
561 pub describe_stack_events_result: DescribeStackEventsResult,
562}
563
564#[derive(Deserialize, Debug)]
565#[serde(rename_all = "PascalCase")]
566pub struct DescribeStackEventsResult {
567 pub stack_events: StackEvents,
568}
569
570#[derive(Deserialize, Debug)]
571#[serde(rename_all = "PascalCase")]
572pub struct StackEvents {
573 #[serde(rename = "member", default)]
574 pub member: Vec<StackEvent>,
575}
576
577#[derive(Deserialize, Debug)]
578#[serde(rename_all = "PascalCase")]
579pub struct StackEvent {
580 pub stack_id: String,
581 pub event_id: String,
582 pub stack_name: String,
583 pub logical_resource_id: Option<String>,
584 pub physical_resource_id: Option<String>,
585 pub resource_type: Option<String>,
586 pub timestamp: String,
587 pub resource_status: Option<String>,
588 pub resource_status_reason: Option<String>,
589 pub resource_properties: Option<String>,
590 pub client_request_token: Option<String>,
591}