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().await;
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().await;
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().await;
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().await;
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().await;
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().await;
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
275#[derive(Debug, Clone)]
276pub struct StackParameter {
277    pub key: String,
278    pub value: String,
279    pub resolved_value: String,
280}
281
282#[derive(Debug, Clone)]
283pub struct StackOutput {
284    pub key: String,
285    pub value: String,
286    pub description: String,
287    pub export_name: String,
288}
289
290#[derive(Debug, Clone)]
291pub struct StackResource {
292    pub logical_id: String,
293    pub physical_id: String,
294    pub resource_type: String,
295    pub status: String,
296    pub module_info: String,
297}