1use crate::stack::{load_stack, try_load_stack};
2use crate::types::*;
3use std::collections::BTreeSet;
4
5pub(crate) struct RemoteOperation {
6 pub(crate) client_request_token: ClientRequestToken,
7 pub(crate) stack_id: StackId,
8}
9
10#[derive(Clone, Debug, Eq, PartialEq, clap::ValueEnum)]
11pub enum ReviewChangeSet {
12 Interactive,
13 NoReview,
14}
15
16pub type Capabilities = std::collections::BTreeSet<aws_sdk_cloudformation::types::Capability>;
17
18pub struct TemplateUploader<'a> {
19 pub cloudformation: &'a aws_sdk_cloudformation::Client,
20 pub s3: &'a aws_sdk_s3::Client,
21 pub s3_bucket_name_output_key: &'a OutputKey,
22 pub stack_name: &'a StackName,
23}
24
25impl TemplateUploader<'_> {
26 async fn upload(&self, template_rendered: &TemplateRendered) -> TemplateUrl {
27 let hex =
28 hex::encode(<sha2::Sha256 as sha2::Digest>::digest(&template_rendered.body).as_slice());
29
30 let s3_bucket_name = crate::stack::read_stack_output(
31 self.cloudformation,
32 self.stack_name,
33 self.s3_bucket_name_output_key,
34 )
35 .await;
36
37 let s3_object_key = format!("{hex}.{}", template_rendered.format.file_ext());
38
39 log::info!("Uploading template to: {s3_bucket_name}/{s3_object_key}");
40
41 self.s3
42 .put_object()
43 .bucket(&s3_bucket_name)
44 .key(&s3_object_key)
45 .body((&template_rendered.body).into())
46 .send()
47 .await
48 .unwrap();
49
50 format!("https://s3.amazonaws.com/{s3_bucket_name}/{s3_object_key}").into()
51 }
52}
53
54#[derive(Debug)]
55pub struct InstanceSpec {
56 pub capabilities: Capabilities,
57 pub parameter_map: ParameterMap,
58 pub stack_name: StackName,
59 pub tag_map: TagMap,
60 pub template: Template,
61}
62
63impl InstanceSpec {
64 #[must_use]
65 pub fn context<'a>(
66 &'a self,
67 cloudformation: &'a aws_sdk_cloudformation::Client,
68 template_uploader: Option<&'a TemplateUploader<'a>>,
69 ) -> Context<'a> {
70 Context {
71 cloudformation,
72 instance_spec: self,
73 template_uploader,
74 }
75 }
76
77 pub(crate) async fn watch(cloudformation: &aws_sdk_cloudformation::Client, stack_id: StackId) {
78 crate::events::Poll::default(stack_id)
79 .run(cloudformation, |stack_event| {
80 crate::events::print_event(stack_event)
81 })
82 .await;
83 }
84}
85
86pub struct Context<'a> {
87 cloudformation: &'a aws_sdk_cloudformation::Client,
88 instance_spec: &'a InstanceSpec,
89 template_uploader: Option<&'a TemplateUploader<'a>>,
90}
91
92impl Context<'_> {
93 pub async fn create_change_set(
94 &self,
95 change_set_name: &ChangeSetName,
96 user_parameter_map: &ParameterMap,
97 ) {
98 let existing_stack = load_stack(self.cloudformation, &self.instance_spec.stack_name).await;
99
100 let change_set_arn = self
101 .start_create_change_set(&existing_stack, change_set_name, user_parameter_map)
102 .await;
103
104 println!("ChangeSetArn: {}", change_set_arn.as_str());
105 }
106
107 pub async fn delete_change_set(&self, change_set_name: &ChangeSetName) {
108 self.cloudformation
109 .delete_change_set()
110 .change_set_name(change_set_name)
111 .stack_name(self.instance_spec.stack_name.as_str())
112 .send()
113 .await
114 .unwrap();
115 }
116
117 pub async fn describe_change_set(&self, change_set_name: &ChangeSetName) {
118 let output = self
119 .cloudformation
120 .describe_change_set()
121 .change_set_name(change_set_name)
122 .stack_name(self.instance_spec.stack_name.as_str())
123 .send()
124 .await
125 .unwrap();
126
127 crate::change_set::print_change_set_output(&output);
128 }
129
130 pub async fn list_change_sets(&self) {
131 let mut paginator = self
132 .cloudformation
133 .list_change_sets()
134 .stack_name(self.instance_spec.stack_name.as_str())
135 .into_paginator()
136 .send();
137
138 while let Some(result) = paginator.next().await {
139 for summary in result.unwrap().summaries.unwrap_or_default() {
140 eprintln!("{summary:#?}")
141 }
142 }
143 }
144
145 pub async fn delete(&self) {
146 let client_request_token = ClientRequestToken::generate();
147 let stack_id =
148 crate::stack::load_stack_id(self.cloudformation, &self.instance_spec.stack_name).await;
149
150 self.cloudformation
151 .delete_stack()
152 .client_request_token(&client_request_token)
153 .stack_name(self.instance_spec.stack_name.as_str())
154 .send()
155 .await
156 .unwrap();
157
158 Self::wait(
159 self.cloudformation,
160 RemoteOperation {
161 client_request_token,
162 stack_id,
163 },
164 )
165 .await
166 }
167
168 pub async fn create(&self, user_parameter_map: &ParameterMap) {
169 self.wait_for_final(self.start_create(user_parameter_map).await)
170 .await
171 }
172
173 pub async fn sync(
174 &self,
175 review_change_set: &ReviewChangeSet,
176 user_parameter_map: &ParameterMap,
177 ) {
178 match try_load_stack(self.cloudformation, &self.instance_spec.stack_name).await {
179 Some(existing_stack) => {
180 self.update_existing_stack(review_change_set, &existing_stack, user_parameter_map)
181 .await
182 }
183 None => self.create(user_parameter_map).await,
184 }
185 }
186
187 pub async fn update(
188 &self,
189 review_change_set: &ReviewChangeSet,
190 user_parameter_map: &ParameterMap,
191 ) {
192 let existing_stack = load_stack(self.cloudformation, &self.instance_spec.stack_name).await;
193 self.update_existing_stack(review_change_set, &existing_stack, user_parameter_map)
194 .await
195 }
196
197 async fn update_existing_stack(
198 &self,
199 review_change_set: &ReviewChangeSet,
200 existing_stack: &aws_sdk_cloudformation::types::Stack,
201 user_parameter_map: &ParameterMap,
202 ) {
203 match review_change_set {
204 ReviewChangeSet::Interactive => {
205 self.update_interactive(existing_stack, user_parameter_map)
206 .await
207 }
208 ReviewChangeSet::NoReview => {
209 self.wait_for_final_update(
210 self.start_template_update(existing_stack, user_parameter_map)
211 .await,
212 )
213 .await
214 }
215 }
216 }
217
218 async fn update_interactive(
219 &self,
220 existing_stack: &aws_sdk_cloudformation::types::Stack,
221 user_parameter_map: &ParameterMap,
222 ) {
223 use std::io::BufRead;
224
225 let change_set_name =
226 format!("interactive-{}", chrono::Utc::now().format("%Y%m%d%H%M%S")).into();
227
228 log::info!("Creating change set: {change_set_name}");
229
230 let change_set_arn = self
231 .start_create_change_set(existing_stack, &change_set_name, user_parameter_map)
232 .await;
233
234 log::info!("Created change set: {change_set_arn}");
235
236 self.wait_for_final_change_set_status(&change_set_arn).await;
237
238 let output = self
239 .cloudformation
240 .describe_change_set()
241 .change_set_name(&change_set_arn)
242 .include_property_values(true)
243 .send()
244 .await
245 .unwrap();
246
247 let stack_id: StackId = StackId(output.stack_id.clone().unwrap());
248
249 crate::change_set::print_change_set_output(&output);
250
251 println!("Apply? Type YES to proceed or send SIGERM");
252
253 for line in std::io::stdin().lock().lines() {
254 if line.unwrap() == "YES" {
255 break;
256 } else {
257 println!("Only YES please ;)")
258 }
259 }
260
261 let client_request_token = ClientRequestToken::generate();
262
263 self.cloudformation
264 .execute_change_set()
265 .change_set_name(change_set_arn)
266 .client_request_token(&client_request_token)
267 .send()
268 .await
269 .unwrap();
270
271 self.wait_for_final(RemoteOperation {
272 stack_id,
273 client_request_token,
274 })
275 .await
276 }
277
278 async fn wait_for_final_update(&self, result: Option<RemoteOperation>) {
279 match result {
280 None => log::info!("Stack is already up to date"),
281 Some(remote_operation) => self.wait_for_final(remote_operation).await,
282 }
283 }
284
285 async fn wait_for_final(&self, remote_operation: RemoteOperation) {
286 Self::wait(self.cloudformation, remote_operation).await
287 }
288
289 pub async fn parameter_update(&self, user_parameter_map: &ParameterMap) {
290 let result = self
291 .start_parameter_update(
292 self.cloudformation,
293 &load_stack(self.cloudformation, &self.instance_spec.stack_name).await,
294 user_parameter_map,
295 )
296 .await;
297
298 match result {
299 None => log::info!("Stack is already up to date"),
300 Some(remote_operation) => Self::wait(self.cloudformation, remote_operation).await,
301 }
302 }
303
304 async fn wait(
305 cloudformation: &aws_sdk_cloudformation::Client,
306 remote_operation: RemoteOperation,
307 ) {
308 crate::events::Poll::wait_for_remote_operation(remote_operation)
309 .run(cloudformation, crate::events::print_event)
310 .await
311 }
312
313 async fn wait_for_final_change_set_status(&self, change_set_arn: &ChangeSetArn) {
314 use aws_sdk_cloudformation::types::ChangeSetStatus;
315
316 enum Iteration {
317 Continue,
318 Stop,
319 }
320
321 loop {
322 let output = self
323 .cloudformation
324 .describe_change_set()
325 .change_set_name(change_set_arn)
326 .include_property_values(true)
327 .send()
328 .await
329 .unwrap();
330
331 let status = output.status.unwrap();
332
333 let (log_level, panic_message, iteration) = match status {
334 ChangeSetStatus::CreateComplete | ChangeSetStatus::DeleteComplete => {
335 (log::Level::Info, None, Iteration::Stop)
336 }
337 ChangeSetStatus::CreateInProgress
338 | ChangeSetStatus::CreatePending
339 | ChangeSetStatus::DeleteInProgress
340 | ChangeSetStatus::DeletePending => (log::Level::Info, None, Iteration::Continue),
341 ChangeSetStatus::DeleteFailed => (
342 log::Level::Error,
343 Some("Failed to delete change set"),
344 Iteration::Stop,
345 ),
346 ChangeSetStatus::Failed => (
347 log::Level::Error,
348 Some("Failed to create change set"),
349 Iteration::Stop,
350 ),
351 unknown => panic!("Unknown change set status: {unknown:#?}"),
352 };
353
354 log::log!(
355 log_level,
356 "{change_set_arn}: {status}, {}",
357 match output.status_reason.as_ref() {
358 Some(reason) => reason.as_str(),
359 None => "",
360 }
361 );
362
363 if let Some(panic_message) = panic_message {
364 panic!("{panic_message}");
365 }
366
367 match iteration {
368 Iteration::Continue => {}
369 Iteration::Stop => break,
370 }
371
372 tokio::time::sleep(std::time::Duration::new(1, 0)).await
373 }
374 }
375
376 async fn start_create(&self, user_parameter_map: &ParameterMap) -> RemoteOperation {
377 let client_request_token = ClientRequestToken::generate();
378
379 let request = self
380 .cloudformation
381 .create_stack()
382 .stack_name(self.instance_spec.stack_name.as_str())
383 .set_parameters(Some(
384 self.instance_spec
385 .parameter_map
386 .merge(user_parameter_map)
387 .to_create_parameters(),
388 ))
389 .set_capabilities(Some(self.capabilities()))
390 .set_tags(Some(self.instance_spec.tag_map.to_sdk_tags()))
391 .client_request_token(&client_request_token);
392
393 let stack_id = StackId(
394 self.set_create_template(request)
395 .await
396 .send()
397 .await
398 .unwrap()
399 .stack_id
400 .unwrap(),
401 );
402
403 RemoteOperation {
404 client_request_token,
405 stack_id,
406 }
407 }
408
409 pub async fn start_create_change_set(
410 &self,
411 existing_stack: &aws_sdk_cloudformation::types::Stack,
412 change_set_name: &ChangeSetName,
413 user_parameter_map: &ParameterMap,
414 ) -> ChangeSetArn {
415 let client_request_token = ClientRequestToken::generate();
416
417 let existing_stack_parameter_keys = existing_stack_parameter_keys(existing_stack);
418
419 let request = self
420 .cloudformation
421 .create_change_set()
422 .change_set_name(change_set_name)
423 .stack_name(existing_stack.stack_id.as_ref().unwrap())
424 .set_parameters(Some(
425 self.instance_spec
426 .parameter_map
427 .merge(user_parameter_map)
428 .to_template_update_parameters(
429 &self.instance_spec.template.parameter_keys(),
430 &existing_stack_parameter_keys,
431 ),
432 ))
433 .set_capabilities(Some(self.capabilities()))
434 .set_tags(Some(self.instance_spec.tag_map.to_sdk_tags()))
435 .client_token(&client_request_token);
436
437 let output = self
438 .set_create_change_set_template(request)
439 .await
440 .send()
441 .await
442 .unwrap();
443
444 ChangeSetArn(output.id.unwrap())
445 }
446
447 async fn start_template_update(
448 &self,
449 existing_stack: &aws_sdk_cloudformation::types::Stack,
450 user_parameter_map: &ParameterMap,
451 ) -> Option<RemoteOperation> {
452 let client_request_token = ClientRequestToken::generate();
453 let existing_stack_parameter_keys = existing_stack_parameter_keys(existing_stack);
454
455 let request = self
456 .cloudformation
457 .update_stack()
458 .stack_name(existing_stack.stack_id.as_ref().unwrap())
459 .set_parameters(Some(
460 self.instance_spec
461 .parameter_map
462 .merge(user_parameter_map)
463 .to_template_update_parameters(
464 &self.instance_spec.template.parameter_keys(),
465 &existing_stack_parameter_keys,
466 ),
467 ))
468 .set_capabilities(Some(self.capabilities()))
469 .set_tags(Some(self.instance_spec.tag_map.to_sdk_tags()))
470 .client_request_token(&client_request_token);
471
472 let response = self.set_update_template(request).await.send().await;
473
474 Self::process_update_response(client_request_token, response)
475 }
476
477 async fn start_parameter_update(
478 &self,
479 cloudformation: &aws_sdk_cloudformation::Client,
480 existing_stack: &aws_sdk_cloudformation::types::Stack,
481 user_parameter_map: &ParameterMap,
482 ) -> Option<RemoteOperation> {
483 let client_request_token = ClientRequestToken::generate();
484 let response = cloudformation
485 .update_stack()
486 .stack_name(existing_stack.stack_id.as_ref().unwrap())
487 .set_parameters(Some(
488 user_parameter_map.to_parameter_update_parameters(existing_stack),
489 ))
490 .set_capabilities(Some(self.capabilities()))
491 .set_tags(Some(self.instance_spec.tag_map.to_sdk_tags()))
492 .client_request_token(&client_request_token)
493 .use_previous_template(true)
494 .send()
495 .await;
496
497 Self::process_update_response(client_request_token, response)
498 }
499
500 fn process_update_response(
501 client_request_token: ClientRequestToken,
502 result: Result<
503 aws_sdk_cloudformation::operation::update_stack::UpdateStackOutput,
504 aws_sdk_cloudformation::error::SdkError<
505 aws_sdk_cloudformation::operation::update_stack::UpdateStackError,
506 >,
507 >,
508 ) -> Option<RemoteOperation> {
509 match result {
510 Ok(output) => Some(RemoteOperation {
511 client_request_token,
512 stack_id: StackId(output.stack_id.unwrap()),
513 }),
514 Err(error) => {
515 let service_error = error.into_service_error();
516 let meta = service_error.meta();
517
518 match meta.code() {
519 Some("ValidationError")
521 if meta.message() == Some("No updates are to be performed.") =>
522 {
523 None
524 }
525 _ => panic!("unexpected service error: {service_error:#?}"),
526 }
527 }
528 }
529 }
530
531 fn capabilities(&self) -> Vec<aws_sdk_cloudformation::types::Capability> {
532 self.instance_spec.capabilities.iter().cloned().collect()
533 }
534
535 fn template_rendered(&self) -> TemplateRendered {
536 self.instance_spec.template.rendered()
537 }
538
539 async fn set_create_template(
540 &self,
541 request: aws_sdk_cloudformation::operation::create_stack::builders::CreateStackFluentBuilder,
542 ) -> aws_sdk_cloudformation::operation::create_stack::builders::CreateStackFluentBuilder {
543 match self.upload_template().await {
544 Ok(template_url) => request.template_url(template_url),
545 Err(template_body) => request.template_body(template_body),
546 }
547 }
548
549 async fn set_create_change_set_template(
550 &self,
551 request: aws_sdk_cloudformation::operation::create_change_set::builders::CreateChangeSetFluentBuilder,
552 ) -> aws_sdk_cloudformation::operation::create_change_set::builders::CreateChangeSetFluentBuilder
553 {
554 match self.upload_template().await {
555 Ok(template_url) => request.template_url(template_url),
556 Err(template_body) => request.template_body(template_body),
557 }
558 }
559
560 async fn set_update_template(
561 &self,
562 request: aws_sdk_cloudformation::operation::update_stack::builders::UpdateStackFluentBuilder,
563 ) -> aws_sdk_cloudformation::operation::update_stack::builders::UpdateStackFluentBuilder {
564 match self.upload_template().await {
565 Ok(template_url) => request.template_url(template_url),
566 Err(template_body) => request.template_body(template_body),
567 }
568 }
569
570 async fn upload_template(&self) -> Result<TemplateUrl, TemplateBody> {
571 let template_rendered = self.template_rendered();
572
573 if template_rendered.body.needs_upload() {
574 log::debug!("Template is to big for inline, uploading it");
575
576 let template_uploader = self
577 .template_uploader
578 .as_ref()
579 .expect("Template needs upload but template uploader not configured!");
580
581 Ok(template_uploader.upload(&template_rendered).await)
582 } else {
583 Err(template_rendered.body)
584 }
585 }
586}
587
588pub struct Registry(pub Vec<InstanceSpec>);
589
590impl Registry {
591 #[must_use]
592 pub fn find(&self, instance_name: &StackName) -> Option<&InstanceSpec> {
593 self.0
594 .iter()
595 .find(|&instance_spec| instance_spec.stack_name == *instance_name)
596 }
597
598 #[must_use]
599 pub fn templates(&self) -> std::collections::BTreeMap<TemplateName, &Template> {
600 let mut map = std::collections::BTreeMap::new();
601
602 for instance_spec in &self.0 {
603 let template = &instance_spec.template;
604 let name = template.name();
605
606 if let Some(other) = map.insert(name.clone(), template)
607 && other != template
608 {
609 panic!("Template name clash for unequal templates: {name:#?}")
610 }
611 }
612
613 map
614 }
615
616 pub fn golden_tests(&self, path: &std::path::Path) {
617 let mut base: std::path::PathBuf = path.into();
618 base.push("templates");
619 std::fs::create_dir_all(&base).unwrap();
620
621 for (_name, template) in self.templates() {
622 verify_template(&base, template)
623 }
624 }
625}
626
627fn verify_template(base: &std::path::Path, template: &Template) {
628 let new = template.rendered_pretty();
629
630 let mut template_path: std::path::PathBuf = base.into();
631
632 template_path.push(format!(
633 "{}.{}",
634 template.name().as_str(),
635 new.format.file_ext()
636 ));
637
638 if std::env::var("UPDATE_GOLDEN_TESTS").is_ok() {
639 if let Ok(existing) = std::fs::read_to_string(&template_path)
640 && new.body == existing.into()
641 {
642 return;
643 }
644 eprintln!("Updating: {}", template_path.display());
645 std::fs::write(template_path, new.body).unwrap();
646 } else {
647 let expected = std::fs::read_to_string(&template_path)
648 .unwrap_or_else(|error| panic!("Could not open: {}: {error}", template_path.display()));
649
650 assert_eq!(&expected, new.body.as_str())
651 }
652}
653
654fn existing_stack_parameter_keys(
655 existing_stack: &aws_sdk_cloudformation::types::Stack,
656) -> BTreeSet<ParameterKey> {
657 BTreeSet::from_iter(match &existing_stack.parameters {
658 None => vec![],
659 Some(parameters) => parameters
660 .iter()
661 .map(|parameter| ParameterKey(parameter.parameter_key.clone().unwrap()))
662 .collect(),
663 })
664}