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