1use crate::util::{get_existing_template, get_stack_status, load_config};
2use aws_sdk_cloudformation::Client;
3use aws_sdk_cloudformation::types::StackStatus;
4use aws_sdk_s3::types::{Delete, ObjectIdentifier};
5use rusty_cdk_core::stack::{Cleanable, Stack};
6use rusty_cdk_core::wrappers::StringWithOnlyAlphaNumericsAndHyphens;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::time::Duration;
10use tokio::time::sleep;
11
12#[derive(Debug)]
13pub enum DestroyError {
14 EmptyError(String),
15 StackDeleteError(String),
16 UnknownStack(String),
17 UnknownError(String),
18}
19
20impl Error for DestroyError {}
21
22impl Display for DestroyError {
23 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 match self {
25 DestroyError::EmptyError(_) => f.write_str("could not empty bucket"),
26 DestroyError::StackDeleteError(_) => f.write_str("unable to delete stack"),
27 DestroyError::UnknownStack(_) => f.write_str("stack could not be found"),
28 DestroyError::UnknownError(_) => f.write_str("unknown error"),
29 }
30 }
31}
32
33pub async fn destroy(name: StringWithOnlyAlphaNumericsAndHyphens, print_progress: bool) -> Result<(), DestroyError> {
43 let name = name.0;
44 let config = load_config(false).await;
45 let cloudformation_client = Client::new(&config);
46
47 destroy_stack(&name, &cloudformation_client).await?;
48
49 loop {
50 let status = get_stack_status(&name, &cloudformation_client).await;
51
52 if let Some(status) = status {
53 match status {
54 StackStatus::DeleteComplete => return Ok(()),
55 StackStatus::DeleteInProgress => {
56 if print_progress {
57 println!("destroying...");
58 }
59 }
60 StackStatus::DeleteFailed => {
61 return Err(DestroyError::StackDeleteError(format!("{status}")));
62 }
63 _ => {
64 return Err(DestroyError::UnknownError(format!("{status}")));
65 }
66 }
67 } else {
68 return Ok(());
70 }
71
72 sleep(Duration::from_secs(10)).await;
73 }
74}
75
76async fn destroy_stack(name: &String, cloudformation_client: &Client) -> Result<(), DestroyError> {
77 let delete_result = cloudformation_client.delete_stack().stack_name(name).send().await;
78 match delete_result {
79 Ok(_) => Ok(()),
80 Err(e) => Err(DestroyError::StackDeleteError(e.to_string())),
81 }
82}
83
84pub async fn clean(name: StringWithOnlyAlphaNumericsAndHyphens, print_progress: bool) -> Result<(), DestroyError> {
85 let config = load_config(false).await;
86 let cloudformation_client = Client::new(&config);
87
88 let stack = get_existing_template(&cloudformation_client, &name.0)
89 .await
90 .ok_or_else(|| DestroyError::UnknownStack(format!("could not retrieve stack with name {}", &name.0)))?;
91
92 let stack: Stack = serde_json::from_str(&stack).expect("to transform template into stack");
93
94 for resource in stack.get_cleanable_resources() {
95 match resource {
96 Cleanable::Bucket(id) => {
97 let bucket_info = cloudformation_client
98 .describe_stack_resource()
99 .stack_name(&name.0)
100 .logical_resource_id(id)
101 .send()
102 .await
103 .expect("to find resource that's mentioned in the template");
104 let physical_id = bucket_info
105 .stack_resource_detail
106 .expect("stack resource output to have detail")
107 .physical_resource_id
108 .expect("physical id to be present");
109
110 if print_progress {
111 println!("found bucket {id} (name {physical_id}) that will be deleted - emptying")
112 }
113 empty_bucket(physical_id).await?
114 }
115 Cleanable::Topic(id) => {
116 let bucket_info = cloudformation_client
117 .describe_stack_resource()
118 .stack_name(&name.0)
119 .logical_resource_id(id)
120 .send()
121 .await
122 .expect("to find resource that's mentioned in the template");
123 let physical_id = bucket_info
124 .stack_resource_detail
125 .expect("stack resource output to have detail")
126 .physical_resource_id
127 .expect("physical id to be present");
128
129 if print_progress {
130 println!("found topic {id} (arn {physical_id}) that will be deleted - remove archival policy")
131 }
132 remove_archive_policy(physical_id).await?;
133 }
134 }
135 }
136
137 Ok(())
138}
139
140async fn remove_archive_policy(arn: String) -> Result<(), DestroyError> {
141 let config = load_config(false).await;
142 let client = aws_sdk_sns::Client::new(&config);
143
144 client
145 .set_topic_attributes()
146 .topic_arn(arn)
147 .attribute_name("ArchivePolicy")
148 .attribute_value("{}")
149 .send()
150 .await
151 .map_err(|e| DestroyError::EmptyError(format!("could not remove archive policy from topic: {:?}", e)))?;
152
153 Ok(())
154}
155
156async fn empty_bucket(name: String) -> Result<(), DestroyError> {
157 let config = load_config(false).await;
158 let client = aws_sdk_s3::Client::new(&config);
159
160 let mut marker_response = delete_objects(&client, &name, None).await?;
161
162 while let Some(marker) = marker_response {
163 println!("more cleanup required for {name}...");
164 marker_response = delete_objects(&client, &name, Some(marker)).await?;
165 }
166
167 Ok(())
168}
169
170async fn delete_objects(client: &aws_sdk_s3::Client, name: &str, marker: Option<String>) -> Result<Option<String>, DestroyError> {
171 let mut builder = client.list_objects().bucket(name);
172
173 if let Some(marker) = marker {
174 builder = builder.marker(marker);
175 }
176
177 let objects = builder
178 .send()
179 .await
180 .map_err(|e| DestroyError::EmptyError(format!("could not list objects to delete: {:?}", e)))?;
181
182 let marker = objects.marker;
183
184 if let Some(content) = objects.contents {
185 let objects_to_delete = content
186 .into_iter()
187 .map(|v| {
188 ObjectIdentifier::builder()
189 .key(v.key.expect("object to have a key"))
190 .build()
191 .expect("building object identifier to succeed")
192 })
193 .collect();
194
195 let to_delete = Delete::builder()
196 .set_objects(Some(objects_to_delete))
197 .build()
198 .expect("building delete object to succeed");
199 client
200 .delete_objects()
201 .bucket(name)
202 .delete(to_delete)
203 .send()
204 .await
205 .map_err(|e| DestroyError::EmptyError(format!("could not delete objects: {:?}", e)))?;
206
207 Ok(marker)
208 } else {
209 Ok(None)
210 }
211}