Skip to main content

rusty_cdk/
destroy.rs

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
33/// Destroy a deployed stack
34/// It returns a `Result`. In case of error, a `DestroyError` is returned.
35///
36/// # Parameters
37///
38/// * `name` - The CloudFormation stack name (alphanumeric characters and hyphens only)
39/// * `stack` - The stack to deploy, created using `StackBuilder`
40/// * `print_progress` - Print progress updates to standard out
41/// 
42pub 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            // no status, so stack should be gone
69            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}