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