cloudformatious/
delete_stack.rs1use 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#[allow(clippy::module_name_repetitions)]
40pub struct DeleteStackInput {
41 pub client_request_token: Option<String>,
58
59 pub retain_resources: Option<Vec<String>>,
66
67 pub role_arn: Option<String>,
75
76 pub stack_name: String,
78}
79
80impl DeleteStackInput {
81 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 #[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 #[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 #[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#[derive(Debug)]
140#[allow(clippy::module_name_repetitions)]
141pub enum DeleteStackError {
142 CloudFormationApi(Box<dyn std::error::Error>),
151
152 Failure(StackFailure),
154
155 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
186pub 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 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#[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}