cloudformatious/
delete_stack.rs

1use std::{fmt, future::Future, pin::Pin, task};
2
3use async_stream::try_stream;
4use aws_sdk_cloudformation::{
5    error::{ProvideErrorMetadata, SdkError},
6    operation::{
7        delete_stack::builders::DeleteStackFluentBuilder, describe_stacks::DescribeStacksError,
8    },
9};
10use chrono::Utc;
11use futures_util::{Stream, TryStreamExt};
12
13use crate::{
14    stack::{StackOperation, StackOperationError, StackOperationStatus},
15    StackEvent, StackFailure, StackStatus, StackWarning,
16};
17
18/// The input for the `delete_stack` operation.
19///
20/// You can create a delete stack input via the [`new`](Self::new) associated function. Setters are
21/// also available to make constructing sparse inputs more ergonomic.
22///
23/// ```no_run
24/// use cloudformatious::DeleteStackInput;
25///
26/// # #[tokio::main]
27/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28/// let config = aws_config::load_from_env().await;
29/// let client = cloudformatious::Client::new(&config);
30/// let input = DeleteStackInput::new("my-stack")
31///     .set_client_request_token("hello")
32///     .set_retain_resources(["MyResource"])
33///     .set_role_arn("arn:foo");
34/// client.delete_stack(input).await?;
35/// // ...
36/// # Ok(())
37/// # }
38/// ```
39#[allow(clippy::module_name_repetitions)]
40pub struct DeleteStackInput {
41    /// A unique identifier for this `DeleteStack` request. Specify this token if you plan to retry
42    /// requests so that AWS CloudFormation knows that you're not attempting to delete a stack with
43    /// the same name. You might retry `DeleteStack` requests to ensure that AWS CloudFormation
44    /// successfully received them.
45    ///
46    /// All events triggered by a given stack operation are assigned the same client request token,
47    /// which you can use to track operations. For example, if you execute a `CreateStack` operation
48    /// with the token `token1`, then all the `StackEvent`s generated by that operation will have
49    /// `ClientRequestToken` set as `token1`.
50    ///
51    /// In the console, stack operations display the client request token on the Events tab. Stack
52    /// operations that are initiated from the console use the token format
53    /// `Console-StackOperation-ID`, which helps you easily identify the stack operation . For
54    /// example, if you create a stack using the console, each stack event would be assigned the
55    /// same token in the following format:
56    /// `Console-CreateStack-7f59c3cf-00d2-40c7-b2ff-e75db0987002`.
57    pub client_request_token: Option<String>,
58
59    /// For stacks in the `DELETE_FAILED` state, a list of resource logical IDs that are associated
60    /// with the resources you want to retain. During deletion, AWS CloudFormation deletes the stack
61    /// but does not delete the retained resources.
62    ///
63    /// Retaining resources is useful when you cannot delete a resource, such as a non-empty S3
64    /// bucket, but you want to delete the stack.
65    pub retain_resources: Option<Vec<String>>,
66
67    /// The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that AWS
68    /// CloudFormation assumes to delete the stack. AWS CloudFormation uses the role's credentials
69    /// to make calls on your behalf.
70    ///
71    /// If you don't specify a value, AWS CloudFormation uses the role that was previously
72    /// associated with the stack. If no role is available, AWS CloudFormation uses a temporary
73    /// session that is generated from your user credentials.
74    pub role_arn: Option<String>,
75
76    /// The name or the unique stack ID that is associated with the stack.
77    pub stack_name: String,
78}
79
80impl DeleteStackInput {
81    /// Construct an input for the given `stack_name` and `template_source`.
82    pub fn new(stack_name: impl Into<String>) -> Self {
83        Self {
84            stack_name: stack_name.into(),
85
86            client_request_token: None,
87            retain_resources: None,
88            role_arn: None,
89        }
90    }
91
92    /// Set the value for `client_request_token`.
93    ///
94    /// **Note:** this consumes and returns `self` for chaining.
95    #[must_use]
96    pub fn set_client_request_token(mut self, client_request_token: impl Into<String>) -> Self {
97        self.client_request_token = Some(client_request_token.into());
98        self
99    }
100
101    /// Set the value for `client_request_token`.
102    ///
103    /// **Note:** this consumes and returns `self` for chaining.
104    #[must_use]
105    pub fn set_retain_resources<I, S>(mut self, retain_resources: I) -> Self
106    where
107        I: Into<Vec<S>>,
108        S: Into<String>,
109    {
110        self.retain_resources = Some(
111            retain_resources
112                .into()
113                .into_iter()
114                .map(Into::into)
115                .collect(),
116        );
117        self
118    }
119
120    /// Set the value for `role_arn`.
121    ///
122    /// **Note:** this consumes and returns `self` for chaining.
123    #[must_use]
124    pub fn set_role_arn(mut self, role_arn: impl Into<String>) -> Self {
125        self.role_arn = Some(role_arn.into());
126        self
127    }
128
129    fn configure(self, input: DeleteStackFluentBuilder) -> DeleteStackFluentBuilder {
130        input
131            .set_client_request_token(self.client_request_token)
132            .set_retain_resources(self.retain_resources)
133            .set_role_arn(self.role_arn)
134            .stack_name(self.stack_name)
135    }
136}
137
138/// Errors emitted by a `delete_stack` operation.
139#[derive(Debug)]
140#[allow(clippy::module_name_repetitions)]
141pub enum DeleteStackError {
142    /// A CloudFormation API error occurred.
143    ///
144    /// This is likely to be due to invalid input parameters or missing CloudFormation permissions.
145    /// The inner error should have a descriptive message.
146    ///
147    /// **Note:** the inner error will always be some variant of [`SdkError`], but since they are
148    /// generic over the type of service errors we either need a variant per API used, or `Box`. If
149    /// you do need to programmatically match a particular API error you can use [`Box::downcast`].
150    CloudFormationApi(Box<dyn std::error::Error>),
151
152    /// The delete stack operation failed.
153    Failure(StackFailure),
154
155    /// The delete stack operation succeeded with warnings.
156    Warning(StackWarning),
157}
158
159impl DeleteStackError {
160    fn from_sdk_error<E: std::error::Error + 'static>(error: SdkError<E>) -> Self {
161        Self::CloudFormationApi(error.into())
162    }
163}
164
165impl fmt::Display for DeleteStackError {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self {
168            Self::CloudFormationApi(error) => {
169                write!(f, "CloudFormation API error: {error}")
170            }
171            Self::Failure(failure) => write!(f, "{failure}"),
172            Self::Warning(warning) => write!(f, "{warning}"),
173        }
174    }
175}
176
177impl std::error::Error for DeleteStackError {
178    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
179        match self {
180            Self::CloudFormationApi(error) => Some(error.as_ref()),
181            Self::Failure { .. } | Self::Warning { .. } => None,
182        }
183    }
184}
185
186/// An ongoing `delete_stack` operation.
187///
188/// This implements `Future`, which will simply wait for the operation to conclude. If you want to
189/// observe progress, see [`DeleteStack::events`].
190pub struct DeleteStack<'client> {
191    event_stream: Pin<Box<dyn Stream<Item = Result<StackEvent, DeleteStackError>> + 'client>>,
192    output: Option<Result<(), DeleteStackError>>,
193}
194
195impl<'client> DeleteStack<'client> {
196    pub(crate) fn new(
197        client: &'client aws_sdk_cloudformation::Client,
198        input: DeleteStackInput,
199    ) -> Self {
200        let event_stream = try_stream! {
201            let Some(stack_id) = (describe_stack_id(client, input.stack_name.clone()).await?) else {
202                return;
203            };
204
205            let started_at = Utc::now();
206            input.configure(client.delete_stack()).send()
207                .await
208                .map_err(DeleteStackError::from_sdk_error)?;
209
210            let mut operation =
211                StackOperation::new(client, stack_id, started_at, check_operation_status);
212            while let Some(event) = operation
213                .try_next()
214                .await
215                .map_err(DeleteStackError::from_sdk_error)?
216            {
217                yield event;
218            }
219
220            match operation.verify() {
221                Ok(()) => {}
222                Err(StackOperationError::Failure(failure)) => {
223                    Err(DeleteStackError::Failure(failure))?;
224                    unreachable!()
225                }
226                Err(StackOperationError::Warning(warning)) => {
227                    Err(DeleteStackError::Warning(warning))?;
228                    unreachable!()
229                }
230            };
231        };
232        Self {
233            event_stream: Box::pin(event_stream),
234            output: None,
235        }
236    }
237
238    /// Get a `Stream` of `StackEvent`s.
239    pub fn events(&mut self) -> DeleteStackEvents<'client, '_> {
240        DeleteStackEvents(self)
241    }
242
243    fn poll_next_internal(&mut self, ctx: &mut task::Context) -> task::Poll<Option<StackEvent>> {
244        match self.event_stream.as_mut().poll_next(ctx) {
245            task::Poll::Pending => task::Poll::Pending,
246            task::Poll::Ready(None) => {
247                self.output.get_or_insert(Ok(()));
248                task::Poll::Ready(None)
249            }
250            task::Poll::Ready(Some(Ok(event))) => task::Poll::Ready(Some(event)),
251            task::Poll::Ready(Some(Err(error))) => {
252                self.output.replace(Err(error));
253                task::Poll::Ready(None)
254            }
255        }
256    }
257}
258
259impl Future for DeleteStack<'_> {
260    type Output = Result<(), DeleteStackError>;
261
262    fn poll(mut self: Pin<&mut Self>, ctx: &mut task::Context) -> task::Poll<Self::Output> {
263        loop {
264            match self.poll_next_internal(ctx) {
265                task::Poll::Pending => return task::Poll::Pending,
266                task::Poll::Ready(None) => {
267                    return task::Poll::Ready(
268                        self.output
269                            .take()
270                            .expect("end of stream without err or output"),
271                    )
272                }
273                task::Poll::Ready(Some(_)) => continue,
274            }
275        }
276    }
277}
278
279/// Return value of [`DeleteStack::events`].
280#[allow(clippy::module_name_repetitions)]
281pub struct DeleteStackEvents<'client, 'delete>(&'delete mut DeleteStack<'client>);
282
283impl Stream for DeleteStackEvents<'_, '_> {
284    type Item = StackEvent;
285
286    fn poll_next(
287        mut self: Pin<&mut Self>,
288        ctx: &mut task::Context,
289    ) -> task::Poll<Option<Self::Item>> {
290        self.0.poll_next_internal(ctx)
291    }
292}
293
294async fn describe_stack_id(
295    client: &aws_sdk_cloudformation::Client,
296    stack_name: String,
297) -> Result<Option<String>, DeleteStackError> {
298    let output = match client.describe_stacks().stack_name(stack_name).send().await {
299        Ok(output) => output,
300        Err(error) if is_not_exists(&error) => return Ok(None),
301        Err(error) => return Err(DeleteStackError::from_sdk_error(error)),
302    };
303
304    let stack = output
305        .stacks
306        .expect("DescribeStacksOutput without stacks")
307        .pop()
308        .expect("DescribeStacksOutput empty stacks");
309
310    if stack.stack_status == Some(aws_sdk_cloudformation::types::StackStatus::DeleteComplete) {
311        Ok(None)
312    } else {
313        Ok(Some(stack.stack_id.expect("Stack without stack_id")))
314    }
315}
316
317fn check_operation_status(stack_status: StackStatus) -> StackOperationStatus {
318    match stack_status {
319        StackStatus::DeleteInProgress => StackOperationStatus::InProgress,
320        StackStatus::DeleteComplete => StackOperationStatus::Complete,
321        StackStatus::DeleteFailed => StackOperationStatus::Failed,
322        _ => StackOperationStatus::Unexpected,
323    }
324}
325
326fn is_not_exists(error: &SdkError<DescribeStacksError>) -> bool {
327    error
328        .message()
329        .is_some_and(|msg| msg.contains("does not exist"))
330}