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