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