use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
use crate::aws::credential_provider::AwsCredentialProvider;
use alien_client_core::{ErrorData, Result};
use alien_error::ContextError;
use bon::Builder;
use form_urlencoded;
use quick_xml;
use reqwest::{Client, StatusCode};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[cfg(feature = "test-utils")]
use mockall::automock;
#[cfg_attr(feature = "test-utils", automock)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
pub trait CloudFormationApi: Send + Sync + std::fmt::Debug {
async fn create_stack(&self, request: CreateStackRequest) -> Result<CreateStackResponse>;
async fn describe_stacks(
&self,
request: DescribeStacksRequest,
) -> Result<DescribeStacksResponse>;
async fn delete_stack(&self, request: DeleteStackRequest) -> Result<DeleteStackResponse>;
async fn describe_stack_resources(
&self,
request: DescribeStackResourcesRequest,
) -> Result<DescribeStackResourcesResponse>;
async fn describe_stack_resource(
&self,
request: DescribeStackResourceRequest,
) -> Result<DescribeStackResourceResponse>;
async fn describe_stack_events(
&self,
request: DescribeStackEventsRequest,
) -> Result<DescribeStackEventsResponse>;
}
#[derive(Debug, Clone)]
pub struct CloudFormationClient {
client: Client,
credentials: AwsCredentialProvider,
}
impl CloudFormationClient {
pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
Self {
client,
credentials,
}
}
fn sign_config(&self) -> AwsSignConfig {
AwsSignConfig {
service_name: "cloudformation".into(),
region: self.credentials.region().to_string(),
credentials: self.credentials.get_credentials(),
signing_region: None,
}
}
fn get_base_url(&self) -> String {
if let Some(override_url) = self
.credentials
.get_service_endpoint_option("cloudformation")
{
override_url.to_string()
} else {
format!(
"https://cloudformation.{}.amazonaws.com",
self.credentials.region()
)
}
}
fn build_form_body(action: &str, version: &str, params: Vec<(String, String)>) -> String {
let mut all = vec![
("Action".to_string(), action.to_string()),
("Version".to_string(), version.to_string()),
];
all.extend(params);
all.into_iter()
.map(|(k, v)| {
format!(
"{}={}",
k,
form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
)
})
.collect::<Vec<String>>()
.join("&")
}
async fn post_xml<T: DeserializeOwned + Send + 'static>(
&self,
body: String,
operation: &str,
resource_name: &str,
) -> Result<T> {
self.credentials.ensure_fresh().await?;
let base_url = self.get_base_url();
let url = format!("{}/", base_url.trim_end_matches('/'));
let body_for_error = body.clone();
let builder = self
.client
.post(&url)
.host(&format!(
"cloudformation.{}.amazonaws.com",
self.credentials.region()
))
.content_type_form()
.body(body);
let result =
crate::aws::aws_request_utils::sign_send_xml(builder, &self.sign_config()).await;
match result {
Ok(v) => Ok(v),
Err(e) => {
if let Some(ErrorData::HttpResponseError {
http_status,
http_response_text: Some(ref text),
..
}) = &e.error
{
let status = StatusCode::from_u16(*http_status)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
if let Some(mapped) = Self::map_cfn_error(
status,
text,
operation,
resource_name,
Some(&body_for_error),
) {
Err(e.context(mapped))
} else {
Err(e)
}
} else {
Err(e)
}
}
}
}
fn map_cfn_error(
status: StatusCode,
error_body: &str,
operation: &str,
resource_name: &str,
request_body: Option<&str>,
) -> Option<ErrorData> {
let parsed: std::result::Result<CloudFormationErrorResponse, _> =
quick_xml::de::from_str(error_body);
let (code, message) = match parsed {
Ok(e) => (
e.error.code.unwrap_or_else(|| "UnknownErrorCode".into()),
e.error.message.unwrap_or_else(|| "Unknown error".into()),
),
Err(_) => {
return None;
}
};
Some(match code.as_str() {
"AccessDenied"
| "AccessDeniedException"
| "UnauthorizedOperation"
| "AuthFailure"
| "SignatureDoesNotMatch" => ErrorData::RemoteAccessDenied {
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
},
"Throttling" | "ThrottlingException" | "RequestLimitExceeded" => {
ErrorData::RateLimitExceeded { message }
}
"ServiceUnavailable" | "InternalFailure" => {
ErrorData::RemoteServiceUnavailable { message }
}
"AlreadyExists" | "AlreadyExistsException" => ErrorData::RemoteResourceConflict {
message,
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
},
"LimitExceeded" | "LimitExceededException" => ErrorData::QuotaExceeded { message },
"ValidationError" if message.contains("does not exist") => {
ErrorData::RemoteResourceNotFound {
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
}
}
_ => match status {
StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
message,
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
},
StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
},
StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
resource_type: "CloudFormation Stack".into(),
resource_name: resource_name.into(),
},
StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
StatusCode::SERVICE_UNAVAILABLE
| StatusCode::BAD_GATEWAY
| StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
_ => ErrorData::HttpResponseError {
message: format!("CloudFormation {operation} failed: {message}"),
url: format!("cloudformation.amazonaws.com"),
http_status: status.as_u16(),
http_request_text: request_body.map(|s| s.to_string()),
http_response_text: Some(error_body.into()),
},
},
})
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CloudFormationApi for CloudFormationClient {
async fn create_stack(&self, request: CreateStackRequest) -> Result<CreateStackResponse> {
let mut params = vec![
("StackName".to_string(), request.stack_name.clone()),
("TemplateBody".to_string(), request.template_body),
];
if let Some(description) = request.description {
params.push(("Description".to_string(), description));
}
if let Some(timeout) = request.timeout_in_minutes {
params.push(("TimeoutInMinutes".to_string(), timeout.to_string()));
}
if let Some(ref capabilities) = request.capabilities {
for (i, capability) in capabilities.iter().enumerate() {
params.push((format!("Capabilities.member.{}", i + 1), capability.clone()));
}
}
let body = Self::build_form_body("CreateStack", "2010-05-15", params);
let stack_name = &request.stack_name;
self.post_xml(body, "CreateStack", stack_name).await
}
async fn describe_stacks(
&self,
request: DescribeStacksRequest,
) -> Result<DescribeStacksResponse> {
let mut params = Vec::new();
if let Some(ref stack_name) = request.stack_name {
params.push(("StackName".to_string(), stack_name.clone()));
}
let body = Self::build_form_body("DescribeStacks", "2010-05-15", params);
let resource_name = request.stack_name.as_deref().unwrap_or("(unknown)");
self.post_xml(body, "DescribeStacks", resource_name).await
}
async fn delete_stack(&self, request: DeleteStackRequest) -> Result<DeleteStackResponse> {
let params = vec![("StackName".to_string(), request.stack_name.clone())];
let body = Self::build_form_body("DeleteStack", "2010-05-15", params);
self.post_xml(body, "DeleteStack", &request.stack_name)
.await
}
async fn describe_stack_resources(
&self,
request: DescribeStackResourcesRequest,
) -> Result<DescribeStackResourcesResponse> {
let mut params = Vec::new();
if let Some(ref stack_name) = request.stack_name {
params.push(("StackName".to_string(), stack_name.clone()));
}
if let Some(ref logical_id) = request.logical_resource_id {
params.push(("LogicalResourceId".to_string(), logical_id.clone()));
}
if let Some(ref physical_id) = request.physical_resource_id {
params.push(("PhysicalResourceId".to_string(), physical_id.clone()));
}
let body = Self::build_form_body("DescribeStackResources", "2010-05-15", params);
self.post_xml(
body,
"DescribeStackResources",
request.stack_name.as_deref().unwrap_or("(unknown)"),
)
.await
}
async fn describe_stack_resource(
&self,
request: DescribeStackResourceRequest,
) -> Result<DescribeStackResourceResponse> {
let params = vec![
("StackName".to_string(), request.stack_name.clone()),
(
"LogicalResourceId".to_string(),
request.logical_resource_id.clone(),
),
];
let body = Self::build_form_body("DescribeStackResource", "2010-05-15", params);
self.post_xml(body, "DescribeStackResource", &request.stack_name)
.await
}
async fn describe_stack_events(
&self,
request: DescribeStackEventsRequest,
) -> Result<DescribeStackEventsResponse> {
let params = vec![("StackName".to_string(), request.stack_name.clone())];
let body = Self::build_form_body("DescribeStackEvents", "2010-05-15", params);
self.post_xml(body, "DescribeStackEvents", &request.stack_name)
.await
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct CloudFormationErrorResponse {
pub error: CloudFormationErrorDetails,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
struct CloudFormationErrorDetails {
pub code: Option<String>,
pub message: Option<String>,
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct CreateStackRequest {
pub stack_name: String,
pub template_body: String,
pub description: Option<String>,
pub timeout_in_minutes: Option<i32>,
pub capabilities: Option<Vec<String>>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct CreateStackResponse {
pub create_stack_result: CreateStackResult,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct CreateStackResult {
pub stack_id: String,
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStacksRequest {
pub stack_name: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStacksResponse {
pub describe_stacks_result: DescribeStacksResult,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStacksResult {
pub stacks: Stacks,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Stacks {
#[serde(rename = "member", default)]
pub member: Vec<Stack>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Stack {
pub stack_id: String,
pub stack_name: String,
pub stack_status: String,
pub creation_time: String,
pub description: Option<String>,
pub capabilities: Option<Capabilities>,
pub outputs: Option<Outputs>,
pub parameters: Option<Parameters>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Capabilities {
#[serde(rename = "member", default)]
pub member: Vec<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Outputs {
#[serde(rename = "member", default)]
pub member: Vec<Output>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Output {
pub output_key: String,
pub output_value: String,
pub description: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Parameters {
#[serde(rename = "member", default)]
pub member: Vec<Parameter>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Parameter {
pub parameter_key: String,
pub parameter_value: Option<String>,
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct DeleteStackRequest {
pub stack_name: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DeleteStackResponse {
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourcesRequest {
pub stack_name: Option<String>,
pub logical_resource_id: Option<String>,
pub physical_resource_id: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourcesResponse {
pub describe_stack_resources_result: DescribeStackResourcesResult,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourcesResult {
pub stack_resources: StackResources,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackResources {
#[serde(rename = "member", default)]
pub member: Vec<StackResource>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackResource {
pub stack_name: Option<String>,
pub stack_id: Option<String>,
pub logical_resource_id: String,
pub physical_resource_id: Option<String>,
pub resource_type: String,
pub timestamp: String,
pub resource_status: String,
pub resource_status_reason: Option<String>,
pub description: Option<String>,
pub drift_information: Option<StackResourceDriftInformation>,
pub module_info: Option<ModuleInfo>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackResourceDriftInformation {
pub stack_resource_drift_status: String,
pub last_check_timestamp: Option<String>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct ModuleInfo {
pub type_hierarchy: Option<String>,
pub logical_id_hierarchy: Option<String>,
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourceRequest {
pub stack_name: String,
pub logical_resource_id: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourceResponse {
pub describe_stack_resource_result: DescribeStackResourceResult,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackResourceResult {
pub stack_resource_detail: StackResourceDetail,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackResourceDetail {
pub stack_name: Option<String>,
pub stack_id: Option<String>,
pub logical_resource_id: String,
pub physical_resource_id: Option<String>,
pub resource_type: String,
pub last_updated_timestamp: String,
pub resource_status: String,
pub resource_status_reason: Option<String>,
pub description: Option<String>,
pub metadata: Option<String>,
pub drift_information: Option<StackResourceDriftInformation>,
pub module_info: Option<ModuleInfo>,
}
#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackEventsRequest {
pub stack_name: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackEventsResponse {
pub describe_stack_events_result: DescribeStackEventsResult,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct DescribeStackEventsResult {
pub stack_events: StackEvents,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackEvents {
#[serde(rename = "member", default)]
pub member: Vec<StackEvent>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct StackEvent {
pub stack_id: String,
pub event_id: String,
pub stack_name: String,
pub logical_resource_id: Option<String>,
pub physical_resource_id: Option<String>,
pub resource_type: Option<String>,
pub timestamp: String,
pub resource_status: Option<String>,
pub resource_status_reason: Option<String>,
pub resource_properties: Option<String>,
pub client_request_token: Option<String>,
}