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 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 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}