rusticity_core/
lambda.rs

1use crate::config::AwsConfig;
2use anyhow::Result;
3
4#[derive(Clone, Debug)]
5pub struct LambdaFunction {
6    pub name: String,
7    pub arn: String,
8    pub application: Option<String>,
9    pub description: String,
10    pub package_type: String,
11    pub runtime: String,
12    pub architecture: String,
13    pub code_size: i64,
14    pub code_sha256: String,
15    pub memory_mb: i32,
16    pub timeout_seconds: i32,
17    pub last_modified: String,
18    pub layers: Vec<LambdaLayer>,
19}
20
21#[derive(Clone, Debug)]
22pub struct LambdaLayer {
23    pub arn: String,
24    pub code_size: i64,
25}
26
27#[derive(Clone, Debug)]
28pub struct LambdaVersion {
29    pub version: String,
30    pub aliases: String,
31    pub description: String,
32    pub last_modified: String,
33    pub architecture: String,
34}
35
36#[derive(Clone, Debug)]
37pub struct LambdaAlias {
38    pub name: String,
39    pub versions: String,
40    pub description: String,
41}
42
43pub struct LambdaClient {
44    config: AwsConfig,
45}
46
47impl LambdaClient {
48    pub fn new(config: AwsConfig) -> Self {
49        Self { config }
50    }
51
52    pub async fn list_functions(&self) -> Result<Vec<LambdaFunction>> {
53        let client = self.config.lambda_client().await;
54
55        let mut functions = Vec::new();
56        let mut next_marker: Option<String> = None;
57
58        loop {
59            let mut request = client.list_functions().max_items(100);
60            if let Some(marker) = next_marker {
61                request = request.marker(marker);
62            }
63
64            let response = request.send().await?;
65
66            if let Some(funcs) = response.functions {
67                for func in funcs {
68                    // AWS returns last_modified in format: "2024-10-31T12:30:45.000+0000"
69                    let last_modified = func
70                        .last_modified
71                        .as_deref()
72                        .map(|s| {
73                            // Try parsing with timezone
74                            if let Ok(dt) =
75                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
76                            {
77                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
78                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
79                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
80                            } else {
81                                s.to_string()
82                            }
83                        })
84                        .unwrap_or_default();
85
86                    let function_name = func.function_name.unwrap_or_default();
87
88                    // Extract application name from function name pattern
89                    // e.g., "storefront-studio-beta-api" -> "storefront-studio-beta"
90                    let application = function_name
91                        .rsplit_once('-')
92                        .map(|(prefix, _)| prefix.to_string());
93
94                    let layers = func
95                        .layers
96                        .unwrap_or_default()
97                        .into_iter()
98                        .map(|layer| LambdaLayer {
99                            arn: layer.arn.unwrap_or_default(),
100                            code_size: layer.code_size,
101                        })
102                        .collect();
103
104                    functions.push(LambdaFunction {
105                        name: function_name,
106                        arn: func.function_arn.unwrap_or_default(),
107                        application,
108                        description: func.description.unwrap_or_default(),
109                        package_type: func
110                            .package_type
111                            .map(|p| format!("{:?}", p))
112                            .unwrap_or_default(),
113                        runtime: func.runtime.map(|r| format!("{:?}", r)).unwrap_or_default(),
114                        architecture: func
115                            .architectures
116                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
117                            .unwrap_or_default(),
118                        code_size: func.code_size,
119                        code_sha256: func.code_sha256.unwrap_or_default(),
120                        memory_mb: func.memory_size.unwrap_or(0),
121                        timeout_seconds: func.timeout.unwrap_or(0),
122                        last_modified,
123                        layers,
124                    });
125                }
126            }
127
128            next_marker = response.next_marker;
129            if next_marker.is_none() {
130                break;
131            }
132        }
133
134        Ok(functions)
135    }
136
137    pub async fn list_applications(&self) -> Result<Vec<LambdaApplication>> {
138        let client = self.config.cloudformation_client().await;
139
140        let mut applications = Vec::new();
141        let mut next_token: Option<String> = None;
142
143        loop {
144            let mut request = client.list_stacks();
145            if let Some(token) = next_token {
146                request = request.next_token(token);
147            }
148
149            let response = request.send().await?;
150
151            if let Some(stacks) = response.stack_summaries {
152                for stack in stacks {
153                    // Only include active stacks
154                    let status = stack
155                        .stack_status
156                        .map(|s| format!("{:?}", s))
157                        .unwrap_or_default();
158                    if status.contains("DELETE") {
159                        continue;
160                    }
161
162                    applications.push(LambdaApplication {
163                        name: stack.stack_name.unwrap_or_default(),
164                        arn: stack.stack_id.unwrap_or_default(),
165                        description: stack.template_description.unwrap_or_default(),
166                        status,
167                        last_modified: stack
168                            .last_updated_time
169                            .or(stack.creation_time)
170                            .map(|dt| {
171                                let timestamp = dt.secs();
172                                let datetime = chrono::DateTime::from_timestamp(timestamp, 0)
173                                    .unwrap_or_default();
174                                datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
175                            })
176                            .unwrap_or_default(),
177                    });
178                }
179            }
180
181            next_token = response.next_token;
182            if next_token.is_none() {
183                break;
184            }
185        }
186
187        Ok(applications)
188    }
189
190    pub async fn list_versions(&self, function_name: &str) -> Result<Vec<LambdaVersion>> {
191        let client = self.config.lambda_client().await;
192
193        let mut versions = Vec::new();
194        let mut next_marker: Option<String> = None;
195
196        loop {
197            let mut request = client
198                .list_versions_by_function()
199                .function_name(function_name)
200                .max_items(100);
201            if let Some(marker) = next_marker {
202                request = request.marker(marker);
203            }
204
205            let response = request.send().await?;
206
207            if let Some(vers) = response.versions {
208                for ver in vers {
209                    let last_modified = ver
210                        .last_modified
211                        .as_deref()
212                        .map(|s| {
213                            if let Ok(dt) =
214                                chrono::DateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.3f%z")
215                            {
216                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
217                            } else if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
218                                dt.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
219                            } else {
220                                s.to_string()
221                            }
222                        })
223                        .unwrap_or_default();
224
225                    versions.push(LambdaVersion {
226                        version: ver.version.unwrap_or_default(),
227                        aliases: String::new(), // Will be populated below
228                        description: ver.description.unwrap_or_default(),
229                        last_modified,
230                        architecture: ver
231                            .architectures
232                            .and_then(|a| a.first().map(|arch| format!("{:?}", arch)))
233                            .unwrap_or_default(),
234                    });
235                }
236            }
237
238            next_marker = response.next_marker;
239            if next_marker.is_none() {
240                break;
241            }
242        }
243
244        // Fetch aliases for all versions
245        let aliases_response = client
246            .list_aliases()
247            .function_name(function_name)
248            .send()
249            .await?;
250
251        if let Some(aliases) = aliases_response.aliases {
252            for alias in aliases {
253                let alias_name = alias.name.unwrap_or_default();
254
255                // Add alias to primary version
256                if let Some(version) = alias.function_version {
257                    if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
258                        if !ver.aliases.is_empty() {
259                            ver.aliases.push_str(", ");
260                        }
261                        ver.aliases.push_str(&alias_name);
262                    }
263                }
264
265                // Add alias to additional versions in routing config
266                if let Some(routing_config) = alias.routing_config {
267                    if let Some(additional_version_weights) =
268                        routing_config.additional_version_weights
269                    {
270                        for (version, _weight) in additional_version_weights {
271                            if let Some(ver) = versions.iter_mut().find(|v| v.version == version) {
272                                if !ver.aliases.is_empty() {
273                                    ver.aliases.push_str(", ");
274                                }
275                                ver.aliases.push_str(&alias_name);
276                            }
277                        }
278                    }
279                }
280            }
281        }
282
283        Ok(versions)
284    }
285}
286
287#[derive(Clone, Debug)]
288pub struct LambdaApplication {
289    pub name: String,
290    pub arn: String,
291    pub description: String,
292    pub status: String,
293    pub last_modified: String,
294}
295
296impl LambdaClient {
297    pub async fn list_aliases(&self, function_name: &str) -> Result<Vec<LambdaAlias>> {
298        let client = self.config.lambda_client().await;
299        let response = client
300            .list_aliases()
301            .function_name(function_name)
302            .send()
303            .await?;
304
305        let mut aliases = Vec::new();
306        if let Some(alias_list) = response.aliases {
307            for alias in alias_list {
308                let primary_version = alias.function_version.unwrap_or_default();
309
310                // Check for additional versions in routing config
311                let mut versions_str = primary_version.clone();
312                if let Some(routing_config) = alias.routing_config {
313                    if let Some(additional_version_weights) =
314                        routing_config.additional_version_weights
315                    {
316                        for (version, weight) in additional_version_weights {
317                            versions_str.push_str(&format!(
318                                ", {} ({}%)",
319                                version,
320                                (weight * 100.0) as i32
321                            ));
322                        }
323                    }
324                }
325
326                aliases.push(LambdaAlias {
327                    name: alias.name.unwrap_or_default(),
328                    versions: versions_str,
329                    description: alias.description.unwrap_or_default(),
330                });
331            }
332        }
333
334        Ok(aliases)
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    #[test]
341    fn test_application_extraction_from_function_name() {
342        // Test typical pattern: app-name-suffix
343        let name = "storefront-studio-beta-api";
344        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
345        assert_eq!(application, "storefront-studio-beta");
346
347        // Test single dash
348        let name = "myapp-api";
349        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
350        assert_eq!(application, "myapp");
351
352        // Test no dash
353        let name = "simplefunction";
354        let application = name.rsplit_once('-').map(|(prefix, _)| prefix);
355        assert_eq!(application, None);
356
357        // Test multiple dashes
358        let name = "my-complex-app-name-worker";
359        let application = name.rsplit_once('-').map(|(prefix, _)| prefix).unwrap();
360        assert_eq!(application, "my-complex-app-name");
361    }
362}