Skip to main content

rusticity_core/
cfn.rs

1use crate::config::AwsConfig;
2use anyhow::Result;
3
4#[derive(Clone, Debug)]
5pub struct Stack {
6    pub name: String,
7    pub stack_id: String,
8    pub status: String,
9    pub created_time: String,
10    pub updated_time: String,
11    pub deleted_time: String,
12    pub drift_status: String,
13    pub last_drift_check_time: String,
14    pub status_reason: String,
15    pub description: String,
16    pub root_stack: String,
17    pub parent_stack: String,
18}
19
20pub struct CloudFormationClient {
21    config: AwsConfig,
22}
23
24impl CloudFormationClient {
25    pub fn new(config: AwsConfig) -> Self {
26        Self { config }
27    }
28
29    pub async fn list_stacks(&self, include_nested: bool) -> Result<Vec<Stack>> {
30        let client = self.config.cloudformation_client();
31
32        let mut stacks = Vec::new();
33        let mut next_token: Option<String> = None;
34
35        loop {
36            let mut request = client.list_stacks();
37            if let Some(token) = next_token {
38                request = request.next_token(token);
39            }
40
41            let response = request.send().await?;
42
43            if let Some(stack_summaries) = response.stack_summaries {
44                for stack in stack_summaries {
45                    // Skip nested stacks if not requested
46                    if !include_nested {
47                        if let Some(root_id) = &stack.root_id {
48                            if root_id != stack.stack_id.as_deref().unwrap_or("") {
49                                continue;
50                            }
51                        }
52                    }
53
54                    stacks.push(Stack {
55                        name: stack.stack_name.unwrap_or_default(),
56                        stack_id: stack.stack_id.unwrap_or_default(),
57                        status: stack
58                            .stack_status
59                            .map(|s| s.as_str().to_string())
60                            .unwrap_or_default(),
61                        created_time: stack
62                            .creation_time
63                            .map(|dt| {
64                                let timestamp = dt.secs();
65                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
66                                    .unwrap_or_default();
67                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
68                            })
69                            .unwrap_or_default(),
70                        updated_time: stack
71                            .last_updated_time
72                            .map(|dt| {
73                                let timestamp = dt.secs();
74                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
75                                    .unwrap_or_default();
76                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
77                            })
78                            .unwrap_or_default(),
79                        deleted_time: stack
80                            .deletion_time
81                            .map(|dt| {
82                                let timestamp = dt.secs();
83                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
84                                    .unwrap_or_default();
85                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
86                            })
87                            .unwrap_or_default(),
88                        drift_status: stack
89                            .drift_information
90                            .as_ref()
91                            .and_then(|d| d.stack_drift_status.as_ref())
92                            .map(|s| format!("{:?}", s))
93                            .unwrap_or_default(),
94                        last_drift_check_time: stack
95                            .drift_information
96                            .and_then(|d| d.last_check_timestamp)
97                            .map(|dt| {
98                                let timestamp = dt.secs();
99                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
100                                    .unwrap_or_default();
101                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
102                            })
103                            .unwrap_or_default(),
104                        status_reason: stack.stack_status_reason.unwrap_or_default(),
105                        description: stack.template_description.unwrap_or_default(),
106                        root_stack: stack.root_id.unwrap_or_default(),
107                        parent_stack: stack.parent_id.unwrap_or_default(),
108                    });
109                }
110            }
111
112            next_token = response.next_token;
113            if next_token.is_none() {
114                break;
115            }
116        }
117
118        Ok(stacks)
119    }
120
121    pub async fn describe_stack(&self, stack_name: &str) -> Result<StackDetails> {
122        let client = self.config.cloudformation_client();
123
124        let response = client
125            .describe_stacks()
126            .stack_name(stack_name)
127            .send()
128            .await?;
129
130        let stack = response
131            .stacks()
132            .first()
133            .ok_or_else(|| anyhow::anyhow!("Stack not found"))?;
134
135        Ok(StackDetails {
136            detailed_status: String::new(),
137            root_stack: stack.root_id().unwrap_or("").to_string(),
138            parent_stack: stack.parent_id().unwrap_or("").to_string(),
139            termination_protection: stack.enable_termination_protection().unwrap_or(false),
140            iam_role: stack.role_arn().unwrap_or("").to_string(),
141            tags: stack
142                .tags()
143                .iter()
144                .map(|t| {
145                    (
146                        t.key().unwrap_or("").to_string(),
147                        t.value().unwrap_or("").to_string(),
148                    )
149                })
150                .collect(),
151            stack_policy: String::new(),
152            rollback_monitoring_time: String::new(),
153            rollback_alarms: stack
154                .rollback_configuration()
155                .map(|rc| {
156                    rc.rollback_triggers()
157                        .iter()
158                        .map(|t| t.arn().unwrap_or("").to_string())
159                        .collect()
160                })
161                .unwrap_or_default(),
162            notification_arns: stack
163                .notification_arns()
164                .iter()
165                .map(|s| s.to_string())
166                .collect(),
167        })
168    }
169}
170
171#[derive(Debug, Clone)]
172pub struct StackDetails {
173    pub detailed_status: String,
174    pub root_stack: String,
175    pub parent_stack: String,
176    pub termination_protection: bool,
177    pub iam_role: String,
178    pub tags: Vec<(String, String)>,
179    pub stack_policy: String,
180    pub rollback_monitoring_time: String,
181    pub rollback_alarms: Vec<String>,
182    pub notification_arns: Vec<String>,
183}
184
185impl CloudFormationClient {
186    pub async fn get_template(&self, stack_name: &str) -> Result<String> {
187        let client = self.config.cloudformation_client();
188        let response = client.get_template().stack_name(stack_name).send().await?;
189
190        Ok(response.template_body().unwrap_or("").to_string())
191    }
192
193    pub async fn get_stack_parameters(&self, stack_name: &str) -> Result<Vec<StackParameter>> {
194        let client = self.config.cloudformation_client();
195        let response = client
196            .describe_stacks()
197            .stack_name(stack_name)
198            .send()
199            .await?;
200
201        let stack = response
202            .stacks()
203            .first()
204            .ok_or_else(|| anyhow::anyhow!("Stack not found"))?;
205
206        let mut parameters = Vec::new();
207        for param in stack.parameters() {
208            parameters.push(StackParameter {
209                key: param.parameter_key().unwrap_or("").to_string(),
210                value: param.parameter_value().unwrap_or("").to_string(),
211                resolved_value: param.resolved_value().unwrap_or("").to_string(),
212            });
213        }
214
215        Ok(parameters)
216    }
217
218    pub async fn get_stack_outputs(&self, stack_name: &str) -> Result<Vec<StackOutput>> {
219        let client = self.config.cloudformation_client();
220        let response = client
221            .describe_stacks()
222            .stack_name(stack_name)
223            .send()
224            .await?;
225
226        let stack = response
227            .stacks()
228            .first()
229            .ok_or_else(|| anyhow::anyhow!("Stack not found"))?;
230
231        let mut outputs = Vec::new();
232        for output in stack.outputs() {
233            outputs.push(StackOutput {
234                key: output.output_key().unwrap_or("").to_string(),
235                value: output.output_value().unwrap_or("").to_string(),
236                description: output.description().unwrap_or("").to_string(),
237                export_name: output.export_name().unwrap_or("").to_string(),
238            });
239        }
240
241        outputs.sort_by(|a, b| a.key.cmp(&b.key));
242
243        Ok(outputs)
244    }
245
246    pub async fn get_stack_resources(&self, stack_name: &str) -> Result<Vec<StackResource>> {
247        let client = self.config.cloudformation_client();
248        let response = client
249            .describe_stack_resources()
250            .stack_name(stack_name)
251            .send()
252            .await?;
253
254        let mut resources = Vec::new();
255        for resource in response.stack_resources() {
256            resources.push(StackResource {
257                logical_id: resource.logical_resource_id().unwrap_or("").to_string(),
258                physical_id: resource.physical_resource_id().unwrap_or("").to_string(),
259                resource_type: resource.resource_type().unwrap_or("").to_string(),
260                status: resource
261                    .resource_status()
262                    .map(|s| s.as_str())
263                    .unwrap_or("")
264                    .to_string(),
265                module_info: resource
266                    .module_info()
267                    .and_then(|m| m.logical_id_hierarchy())
268                    .unwrap_or("")
269                    .to_string(),
270            });
271        }
272
273        resources.sort_by(|a, b| a.logical_id.cmp(&b.logical_id));
274
275        Ok(resources)
276    }
277
278    pub async fn list_stack_events(&self, stack_name: &str) -> Result<Vec<StackEvent>> {
279        let client = self.config.cloudformation_client();
280        let mut events = Vec::new();
281        let mut next_token: Option<String> = None;
282
283        loop {
284            let mut req = client.describe_stack_events().stack_name(stack_name);
285            if let Some(ref token) = next_token {
286                req = req.next_token(token);
287            }
288            let response = req.send().await?;
289
290            for e in response.stack_events() {
291                events.push(StackEvent {
292                    event_id: e.event_id().unwrap_or("").to_string(),
293                    timestamp: e
294                        .timestamp()
295                        .map(|t| {
296                            t.fmt(aws_smithy_types::date_time::Format::DateTime)
297                                .unwrap_or_default()
298                        })
299                        .unwrap_or_default(),
300                    logical_id: e.logical_resource_id().unwrap_or("").to_string(),
301                    status: e
302                        .resource_status()
303                        .map(|s| s.as_str())
304                        .unwrap_or("")
305                        .to_string(),
306                    detailed_status: String::new(),
307                    status_reason: e.resource_status_reason().unwrap_or("").to_string(),
308                    hook_invocation_count: String::new(),
309                    resource_type: e.resource_type().unwrap_or("").to_string(),
310                    physical_id: e.physical_resource_id().unwrap_or("").to_string(),
311                    client_request_token: e.client_request_token().unwrap_or("").to_string(),
312                    operation_id: String::new(),
313                });
314            }
315
316            next_token = response.next_token().map(|s| s.to_string());
317            if next_token.is_none() {
318                break;
319            }
320        }
321
322        // Already sorted newest-first by the API, keep that order
323        Ok(events)
324    }
325
326    pub async fn list_change_sets(&self, stack_name: &str) -> Result<Vec<StackChangeSet>> {
327        let client = self.config.cloudformation_client();
328        let mut change_sets = Vec::new();
329        let mut next_token: Option<String> = None;
330
331        loop {
332            let mut req = client.list_change_sets().stack_name(stack_name);
333            if let Some(ref token) = next_token {
334                req = req.next_token(token);
335            }
336            let response = req.send().await?;
337
338            for cs in response.summaries() {
339                let created_time = cs
340                    .creation_time()
341                    .map(|t| {
342                        t.fmt(aws_smithy_types::date_time::Format::DateTime)
343                            .unwrap_or_default()
344                    })
345                    .unwrap_or_default();
346
347                change_sets.push(StackChangeSet {
348                    name: cs.change_set_name().unwrap_or("").to_string(),
349                    change_set_id: cs.change_set_id().unwrap_or("").to_string(),
350                    created_time,
351                    status: cs.status().map(|s| s.as_str()).unwrap_or("").to_string(),
352                    description: cs.description().unwrap_or("").to_string(),
353                    root_change_set_id: cs.root_change_set_id().unwrap_or("").to_string(),
354                    parent_change_set_id: cs.parent_change_set_id().unwrap_or("").to_string(),
355                });
356            }
357
358            next_token = response.next_token().map(|s| s.to_string());
359            if next_token.is_none() {
360                break;
361            }
362        }
363
364        Ok(change_sets)
365    }
366}
367
368#[derive(Debug, Clone)]
369pub struct StackParameter {
370    pub key: String,
371    pub value: String,
372    pub resolved_value: String,
373}
374
375#[derive(Debug, Clone)]
376pub struct StackOutput {
377    pub key: String,
378    pub value: String,
379    pub description: String,
380    pub export_name: String,
381}
382
383#[derive(Debug, Clone)]
384pub struct StackResource {
385    pub logical_id: String,
386    pub physical_id: String,
387    pub resource_type: String,
388    pub status: String,
389    pub module_info: String,
390}
391
392#[derive(Debug, Clone)]
393pub struct StackEvent {
394    pub event_id: String,
395    pub timestamp: String,
396    pub logical_id: String,
397    pub status: String,
398    pub detailed_status: String,
399    pub status_reason: String,
400    pub hook_invocation_count: String,
401    pub resource_type: String,
402    pub physical_id: String,
403    pub client_request_token: String,
404    pub operation_id: String,
405}
406
407#[derive(Debug, Clone)]
408pub struct StackChangeSet {
409    pub name: String,
410    pub change_set_id: String,
411    pub created_time: String,
412    pub status: String,
413    pub description: String,
414    pub root_change_set_id: String,
415    pub parent_change_set_id: String,
416}