aws_sdk_manager/
cloudformation.rs

1use std::{
2    thread,
3    time::{Duration, Instant},
4};
5
6use crate::errors::{
7    Error::{Other, API},
8    Result,
9};
10use aws_sdk_cloudformation::{
11    error::{DeleteStackError, DescribeStacksError},
12    model::{Capability, OnFailure, Output, Parameter, StackStatus, Tag},
13    types::SdkError,
14    Client,
15};
16use aws_types::SdkConfig as AwsSdkConfig;
17use log::{info, warn};
18
19/// Implements AWS CloudFormation manager.
20#[derive(Debug, Clone)]
21pub struct Manager {
22    #[allow(dead_code)]
23    shared_config: AwsSdkConfig,
24    cli: Client,
25}
26
27impl Manager {
28    pub fn new(shared_config: &AwsSdkConfig) -> Self {
29        let cloned = shared_config.clone();
30        let cli = Client::new(shared_config);
31        Self {
32            shared_config: cloned,
33            cli,
34        }
35    }
36
37    /// Creates a CloudFormation stack.
38    /// The separate caller is expected to poll the status asynchronously.
39    pub async fn create_stack(
40        &self,
41        stack_name: &str,
42        capabilities: Option<Vec<Capability>>,
43        on_failure: OnFailure,
44        template_body: &str,
45        tags: Option<Vec<Tag>>,
46        parameters: Option<Vec<Parameter>>,
47    ) -> Result<Stack> {
48        info!("creating stack '{}'", stack_name);
49        let ret = self
50            .cli
51            .create_stack()
52            .stack_name(stack_name)
53            .set_capabilities(capabilities)
54            .on_failure(on_failure)
55            .template_body(template_body)
56            .set_tags(tags)
57            .set_parameters(parameters)
58            .send()
59            .await;
60        let resp = match ret {
61            Ok(v) => v,
62            Err(e) => {
63                return Err(API {
64                    message: format!("failed create_stack {:?}", e),
65                    is_retryable: is_error_retryable(&e),
66                });
67            }
68        };
69
70        let stack_id = resp.stack_id().unwrap();
71        info!("created stack '{}' with '{}'", stack_name, stack_id);
72        Ok(Stack::new(
73            stack_name,
74            stack_id,
75            StackStatus::CreateInProgress,
76            None,
77        ))
78    }
79
80    /// Deletes a CloudFormation stack.
81    /// The separate caller is expected to poll the status asynchronously.
82    pub async fn delete_stack(&self, stack_name: &str) -> Result<Stack> {
83        info!("deleting stack '{}'", stack_name);
84        let ret = self.cli.delete_stack().stack_name(stack_name).send().await;
85        match ret {
86            Ok(_) => {}
87            Err(e) => {
88                if !is_error_delete_stack_does_not_exist(&e) {
89                    return Err(API {
90                        message: format!("failed schedule_key_deletion {:?}", e),
91                        is_retryable: is_error_retryable(&e),
92                    });
93                }
94                warn!("stack already deleted ({})", e);
95                return Ok(Stack::new(
96                    stack_name,
97                    "",
98                    StackStatus::DeleteComplete,
99                    None,
100                ));
101            }
102        };
103
104        Ok(Stack::new(
105            stack_name,
106            "",
107            StackStatus::DeleteInProgress,
108            None,
109        ))
110    }
111
112    /// Polls CloudFormation stack status.
113    pub async fn poll_stack(
114        &self,
115        stack_name: &str,
116        desired_status: StackStatus,
117        timeout: Duration,
118        interval: Duration,
119    ) -> Result<Stack> {
120        info!(
121            "polling stack '{}' with desired status {:?} for timeout {:?} and interval {:?}",
122            stack_name, desired_status, timeout, interval,
123        );
124
125        let start = Instant::now();
126        let mut cnt: u128 = 0;
127        loop {
128            let elapsed = start.elapsed();
129            if elapsed.gt(&timeout) {
130                break;
131            }
132
133            let itv = {
134                if cnt == 0 {
135                    // first poll with no wait
136                    Duration::from_secs(1)
137                } else {
138                    interval
139                }
140            };
141            thread::sleep(itv);
142
143            let ret = self
144                .cli
145                .describe_stacks()
146                .stack_name(stack_name)
147                .send()
148                .await;
149            let stacks = match ret {
150                Ok(v) => v.stacks,
151                Err(e) => {
152                    // CFN should fail for non-existing stack, instead of returning 0 stack
153                    if is_error_describe_stacks_does_not_exist(&e)
154                        && desired_status.eq(&StackStatus::DeleteComplete)
155                    {
156                        info!("stack already deleted as desired");
157                        return Ok(Stack::new(stack_name, "", desired_status, None));
158                    }
159                    return Err(API {
160                        message: format!("failed describe_stacks {:?}", e),
161                        is_retryable: is_error_retryable(&e),
162                    });
163                }
164            };
165            let stacks = stacks.unwrap();
166            if stacks.len() != 1 {
167                // CFN should fail for non-existing stack, instead of returning 0 stack
168                return Err(Other {
169                    message: String::from("failed to find stack"),
170                    is_retryable: false,
171                });
172            }
173
174            let stack = stacks.get(0).unwrap();
175            let current_id = stack.stack_id().unwrap();
176            let current_status = stack.stack_status().unwrap();
177            info!("poll (current {:?}, elapsed {:?})", current_status, elapsed);
178
179            if desired_status.ne(&StackStatus::DeleteComplete)
180                && current_status.eq(&StackStatus::DeleteComplete)
181            {
182                return Err(Other {
183                    message: String::from("stack create/update failed thus deleted"),
184                    is_retryable: false,
185                });
186            }
187
188            if desired_status.eq(&StackStatus::CreateComplete)
189                && current_status.eq(&StackStatus::CreateFailed)
190            {
191                return Err(Other {
192                    message: String::from("stack create failed"),
193                    is_retryable: false,
194                });
195            }
196
197            if desired_status.eq(&StackStatus::DeleteComplete)
198                && current_status.eq(&StackStatus::DeleteFailed)
199            {
200                return Err(Other {
201                    message: String::from("stack delete failed"),
202                    is_retryable: false,
203                });
204            }
205
206            if current_status.eq(&desired_status) {
207                let outputs = stack.outputs();
208                let outputs = outputs.unwrap();
209                let outputs = Vec::from(outputs);
210                let current_stack = Stack::new(
211                    stack_name,
212                    current_id,
213                    current_status.clone(),
214                    Some(outputs),
215                );
216                return Ok(current_stack);
217            }
218
219            cnt += 1;
220        }
221
222        return Err(Other {
223            message: format!("failed to poll stack {} in time", stack_name),
224            is_retryable: true,
225        });
226    }
227}
228
229/// Represents the CloudFormation stack.
230#[derive(Debug)]
231pub struct Stack {
232    pub name: String,
233    pub id: String,
234    pub status: StackStatus,
235    pub outputs: Option<Vec<Output>>,
236}
237
238impl Stack {
239    pub fn new(name: &str, id: &str, status: StackStatus, outputs: Option<Vec<Output>>) -> Self {
240        // ref. https://doc.rust-lang.org/1.0.0/style/ownership/constructors.html
241        Self {
242            name: String::from(name),
243            id: String::from(id),
244            status,
245            outputs,
246        }
247    }
248}
249
250#[inline]
251pub fn is_error_retryable<E>(e: &SdkError<E>) -> bool {
252    match e {
253        SdkError::TimeoutError(_) | SdkError::ResponseError { .. } => true,
254        SdkError::DispatchFailure(e) => e.is_timeout() || e.is_io(),
255        _ => false,
256    }
257}
258
259#[inline]
260fn is_error_delete_stack_does_not_exist(e: &SdkError<DeleteStackError>) -> bool {
261    match e {
262        SdkError::ServiceError { err, .. } => {
263            let msg = format!("{:?}", err);
264            msg.contains("does not exist")
265        }
266        _ => false,
267    }
268}
269
270#[inline]
271fn is_error_describe_stacks_does_not_exist(e: &SdkError<DescribeStacksError>) -> bool {
272    match e {
273        SdkError::ServiceError { err, .. } => {
274            let msg = format!("{:?}", err);
275            msg.contains("does not exist")
276        }
277        _ => false,
278    }
279}