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