1use crate::util::{get_existing_template, get_stack_status, load_config};
2use aws_config::SdkConfig;
3use aws_sdk_cloudformation::Client;
4use aws_sdk_cloudformation::error::{ProvideErrorMetadata, SdkError};
5use aws_sdk_cloudformation::types::{Capability, StackStatus, Tag};
6use rusty_cdk_core::stack::{Asset, Stack};
7use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
8use std::error::Error;
9use std::fmt::{Display, Formatter};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::time::sleep;
13
14#[derive(Debug)]
15pub enum DeployError {
16 SynthError(String),
17 StackCreateError(String),
18 StackUpdateError(String),
19 AssetError(String),
20 UnknownError(String),
21}
22
23impl Error for DeployError {}
24
25impl Display for DeployError {
26 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
27 match self {
28 DeployError::SynthError(_) => f.write_str("unable to synth"),
29 DeployError::StackCreateError(_) => f.write_str("unable to create stack"),
30 DeployError::StackUpdateError(_) => f.write_str("unable to update stack"),
31 DeployError::AssetError(_) => f.write_str("unable to handle asset"),
32 DeployError::UnknownError(_) => f.write_str("unknown error"),
33 }
34 }
35}
36
37pub async fn deploy(
99 name: StringWithOnlyAlphaNumericsAndHyphens,
100 mut stack: Stack,
101 print_progress: bool,
102) -> Result<String, DeployError> {
103 let name = name.0;
104 let config = load_config(true).await;
105
106 upload_assets(stack.get_assets(), &config).await?;
107
108 let cloudformation_client = Client::new(&config);
109
110 create_or_update_stack(&name, &mut stack, &cloudformation_client).await?;
111
112 loop {
113 let status = get_stack_status(&name, &cloudformation_client)
114 .await
115 .expect("status to be available for stack");
116
117 match status {
118 StackStatus::CreateComplete => {
119 return Ok("created successfully".to_string());
120 }
121 StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
122 return Ok("updated successfully".to_string());
123 }
124 StackStatus::CreateInProgress => {
125 if print_progress {
126 println!("creating...");
127 }
128 }
129 StackStatus::UpdateInProgress => {
130 if print_progress {
131 println!("updating...");
132 }
133 }
134 StackStatus::CreateFailed => {
135 return Err(DeployError::StackCreateError(format!("{status}")));
136 }
137 StackStatus::UpdateRollbackComplete
138 | StackStatus::UpdateRollbackCompleteCleanupInProgress
139 | StackStatus::UpdateRollbackFailed
140 | StackStatus::UpdateRollbackInProgress
141 | StackStatus::UpdateFailed => {
142 return Err(DeployError::StackUpdateError(format!("{status}")));
143 }
144 _ => {
145 return Err(DeployError::UnknownError(format!("{status}")));
146 }
147 }
148
149 sleep(Duration::from_secs(10)).await;
150 }
151}
152
153async fn create_or_update_stack(name: &String, stack: &mut Stack, cloudformation_client: &Client) -> Result<(), DeployError> {
154 let existing_template = get_existing_template(cloudformation_client, name).await;
155 let tags = stack.get_tags();
156 let tags = if tags.is_empty() {
157 None
158 } else {
159 Some(tags.into_iter().map(|v| Tag::builder().key(v.0).value(v.1).build()).collect())
160 };
161
162 match existing_template {
163 Some(existing) => {
164 let body = stack
165 .synth_for_existing(&existing)
166 .map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
167
168 return match cloudformation_client
169 .update_stack()
170 .stack_name(name)
171 .template_body(body)
172 .capabilities(Capability::CapabilityNamedIam)
173 .set_tags(tags)
174 .send()
175 .await
176 {
177 Ok(_) => Ok(()),
178 Err(e) => match e {
179 SdkError::ServiceError(ref s) => {
180 let update_stack_error = s.err();
181 if update_stack_error
182 .message()
183 .map(|v| v.contains("No updates are to be performed"))
184 .unwrap_or(false)
185 {
186 Ok(())
187 } else {
188 Err(DeployError::StackUpdateError(format!("{e:?}")))
189 }
190 }
191 _ => Err(DeployError::StackUpdateError(format!("{e:?}"))),
192 },
193 };
194 }
195 None => {
196 let body = stack.synth().map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
197
198 cloudformation_client
199 .create_stack()
200 .stack_name(name)
201 .template_body(body)
202 .capabilities(Capability::CapabilityNamedIam)
203 .set_tags(tags)
204 .send()
205 .await
206 .map_err(|e| DeployError::StackCreateError(format!("{e:?}")))?;
207 }
208 }
209 Ok(())
210}
211
212async fn upload_assets(assets: Vec<Asset>, config: &SdkConfig) -> Result<(), DeployError> {
213 let s3_client = Arc::new(aws_sdk_s3::Client::new(config));
214
215 let tasks: Vec<_> = assets
216 .into_iter()
217 .map(|a| {
218 let s3_client = s3_client.clone();
219 tokio::spawn(async move {
220 let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
221 s3_client
222 .put_object()
223 .bucket(a.s3_bucket)
224 .key(a.s3_key)
225 .body(body.unwrap())
226 .send()
227 .await
228 .unwrap();
229 })
230 })
231 .collect();
232
233 for task in tasks {
234 task.await.map_err(|e| DeployError::AssetError(format!("{e:?}")))?;
235 }
236 Ok(())
237}