1use aws_config::stalled_stream_protection::StalledStreamProtectionConfig;
2use aws_config::SdkConfig;
3use aws_sdk_cloudformation::types::{Capability, StackStatus, Tag};
4use aws_sdk_cloudformation::Client;
5use rusty_cdk_core::stack::{Asset, Stack};
6use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::process::exit;
10use std::sync::Arc;
11use std::time::Duration;
12use aws_sdk_cloudformation::error::{ProvideErrorMetadata, SdkError};
13use tokio::time::sleep;
14use crate::util::get_existing_template;
15
16#[derive(Debug)]
17pub enum DeployError {
18 SynthError(String),
19 StackCreateError(String),
20 StackUpdateError(String),
21 AssetError(String),
22 UnknownError(String),
23}
24
25impl Error for DeployError {}
26
27impl Display for DeployError {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 match self {
30 DeployError::SynthError(_) => f.write_str("unable to synth"),
31 DeployError::StackCreateError(_) => f.write_str("unable to create stack"),
32 DeployError::StackUpdateError(_) => f.write_str("unable to update stack"),
33 DeployError::AssetError(_) => f.write_str("unable to handle asset"),
34 DeployError::UnknownError(_) => f.write_str("unknown error"),
35 }
36 }
37}
38
39pub async fn deploy(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack) {
97 let name = name.0;
98 let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
99 .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
101 .load()
102 .await;
103
104 let assets = stack.get_assets();
105 assets.iter().for_each(|a| {
106 println!("uploading {}", a);
107 });
108
109 match upload_assets(assets, &config).await {
110 Ok(_) => {}
111 Err(e) => {
112 eprintln!("{e:#?}");
113 exit(1);
114 }
115 }
116
117 let cloudformation_client = Client::new(&config);
118
119 match create_or_update_stack(&name, &mut stack, &cloudformation_client).await {
120 Ok(_) => {}
121 Err(e) => {
122 eprintln!("{e:#?}");
123 exit(1);
124 }
125 }
126
127 loop {
128 let status = cloudformation_client.describe_stacks().stack_name(&name).send().await;
129 let mut stacks = status
130 .expect("to get a describe stacks result")
131 .stacks
132 .expect("to have a list of stacks");
133 let first_stack = stacks.get_mut(0).expect("to find our stack");
134 let status = first_stack.stack_status.take().expect("stack to have status");
135
136 match status {
137 StackStatus::CreateComplete => {
138 println!("creation completed successfully!");
139 exit(0);
140 }
141 StackStatus::CreateFailed => {
142 println!("creation failed");
143 exit(1);
144 }
145 StackStatus::CreateInProgress => {
146 println!("creating...");
147 }
148 StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
149 println!("update completed successfully!");
150 exit(0);
151 }
152 StackStatus::UpdateRollbackComplete
153 | StackStatus::UpdateRollbackCompleteCleanupInProgress
154 | StackStatus::UpdateRollbackFailed
155 | StackStatus::UpdateRollbackInProgress
156 | StackStatus::UpdateFailed => {
157 println!("update failed");
158 exit(1);
159 }
160 StackStatus::UpdateInProgress => {
161 println!("updating...");
162 }
163 _ => {
164 println!("encountered unexpected cloudformation status: {status}");
165 exit(1);
166 }
167 }
168
169 sleep(Duration::from_secs(10)).await;
170 }
171}
172
173pub async fn deploy_with_result(name: StringWithOnlyAlphaNumericsAndHyphens, mut stack: Stack) -> Result<(), DeployError> {
233 let name = name.0;
234 let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
235 .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
237 .load()
238 .await;
239
240 upload_assets(stack.get_assets(), &config).await?;
241
242 let cloudformation_client = Client::new(&config);
243
244 create_or_update_stack(&name, &mut stack, &cloudformation_client).await?;
245
246 loop {
247 let status = cloudformation_client.describe_stacks().stack_name(&name).send().await;
248 let mut stacks = status
249 .expect("to get a describe stacks result")
250 .stacks
251 .expect("to have a list of stacks");
252 let first_stack = stacks.get_mut(0).expect("to find our stack");
253 let status = first_stack.stack_status.take().expect("stack to have status");
254
255 match status {
256 StackStatus::CreateComplete => {
257 return Ok(());
258 }
259 StackStatus::UpdateComplete | StackStatus::UpdateCompleteCleanupInProgress => {
260 return Ok(());
261 }
262 StackStatus::CreateInProgress => {}
263 StackStatus::UpdateInProgress => {}
264 StackStatus::CreateFailed => {
265 return Err(DeployError::StackCreateError(format!("{status}")));
266 }
267 StackStatus::UpdateRollbackComplete
268 | StackStatus::UpdateRollbackCompleteCleanupInProgress
269 | StackStatus::UpdateRollbackFailed
270 | StackStatus::UpdateRollbackInProgress
271 | StackStatus::UpdateFailed => {
272 return Err(DeployError::StackUpdateError(format!("{status}")));
273 }
274 _ => {
275 return Err(DeployError::UnknownError(format!("{status}")));
276 }
277 }
278
279 sleep(Duration::from_secs(10)).await;
280 }
281}
282
283async fn create_or_update_stack(name: &String, stack: &mut Stack, cloudformation_client: &Client) -> Result<(), DeployError> {
284 let existing_template = get_existing_template(cloudformation_client, name).await;
285 let tags = stack.get_tags();
286 let tags = if tags.is_empty() {
287 None
288 } else {
289 Some(tags.into_iter().map(|v| Tag::builder().key(v.0).value(v.1).build()).collect())
290 };
291
292 match existing_template {
293 Some(existing) => {
294 let body = stack
295 .synth_for_existing(&existing)
296 .map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
297
298 return match cloudformation_client
299 .update_stack()
300 .stack_name(name)
301 .template_body(body)
302 .capabilities(Capability::CapabilityNamedIam)
303 .set_tags(tags)
304 .send()
305 .await {
306 Ok(_) => Ok(()),
307 Err(e) => {
308 match e {
309 SdkError::ServiceError(ref s) => {
310 let update_stack_error = s.err();
311 if update_stack_error.message().map(|v| v.contains("No updates are to be performed")).unwrap_or(false) {
312 Ok(())
313 } else {
314 Err(DeployError::StackUpdateError(format!("{e:?}")))
315 }
316 }
317 _ => {
318 Err(DeployError::StackUpdateError(format!("{e:?}")))
319 }
320 }
321 }
322 }
323 }
324 None => {
325 let body = stack.synth().map_err(|e| DeployError::SynthError(format!("{e:?}")))?;
326
327 cloudformation_client
328 .create_stack()
329 .stack_name(name)
330 .template_body(body)
331 .capabilities(Capability::CapabilityNamedIam)
332 .set_tags(tags)
333 .send()
334 .await
335 .map_err(|e| DeployError::StackCreateError(format!("{e:?}")))?;
336 }
337 }
338 Ok(())
339}
340
341async fn upload_assets(assets: Vec<Asset>, config: &SdkConfig) -> Result<(), DeployError> {
342 let s3_client = Arc::new(aws_sdk_s3::Client::new(config));
343
344 let tasks: Vec<_> = assets
345 .into_iter()
346 .map(|a| {
347 let s3_client = s3_client.clone();
348 tokio::spawn(async move {
349 let body = aws_sdk_s3::primitives::ByteStream::from_path(a.path).await;
350 s3_client
351 .put_object()
352 .bucket(a.s3_bucket)
353 .key(a.s3_key)
354 .body(body.unwrap())
355 .send()
356 .await
357 .unwrap();
358 })
359 })
360 .collect();
361
362 for task in tasks {
363 task.await.map_err(|e| DeployError::AssetError(format!("{e:?}")))?;
364 }
365 Ok(())
366}