syncable_cli/analyzer/k8s_optimize/parser/
terraform.rs1use super::yaml::{detect_workload_type, parse_cpu_to_millicores, parse_memory_to_bytes};
7use crate::analyzer::k8s_optimize::types::WorkloadType;
8use hcl::{self, Block, Body};
9use std::path::Path;
10
11#[derive(Debug, Clone)]
13pub struct TfResourceSpec {
14 pub cpu: Option<u64>,
16 pub memory: Option<u64>,
18}
19
20#[derive(Debug, Clone)]
22pub struct TerraformK8sResource {
23 pub resource_type: String,
25 pub tf_name: String,
27 pub k8s_name: Option<String>,
29 pub namespace: Option<String>,
31 pub workload_type: WorkloadType,
33 pub containers: Vec<TerraformContainer>,
35 pub source_file: String,
37}
38
39#[derive(Debug, Clone)]
41pub struct TerraformContainer {
42 pub name: String,
44 pub image: Option<String>,
46 pub requests: Option<TfResourceSpec>,
48 pub limits: Option<TfResourceSpec>,
50}
51
52pub fn parse_terraform_k8s_resources(path: &Path) -> Vec<TerraformK8sResource> {
54 let mut resources = Vec::new();
55
56 if path.is_file() {
57 if let Some(ext) = path.extension() {
58 if ext == "tf" {
59 if let Ok(content) = std::fs::read_to_string(path) {
60 resources.extend(parse_tf_content(&content, path));
61 }
62 }
63 }
64 } else if path.is_dir() {
65 if let Ok(entries) = std::fs::read_dir(path) {
66 for entry in entries.flatten() {
67 let entry_path = entry.path();
68 if entry_path.is_file() {
69 if let Some(ext) = entry_path.extension() {
70 if ext == "tf" {
71 if let Ok(content) = std::fs::read_to_string(&entry_path) {
72 resources.extend(parse_tf_content(&content, &entry_path));
73 }
74 }
75 }
76 }
77 }
78 }
79 }
80
81 resources
82}
83
84fn parse_tf_content(content: &str, file_path: &Path) -> Vec<TerraformK8sResource> {
86 let mut resources = Vec::new();
87
88 let body: Result<Body, _> = hcl::from_str(content);
90 let body = match body {
91 Ok(b) => b,
92 Err(e) => {
93 log::debug!("Failed to parse HCL in {:?}: {}", file_path, e);
94 return resources;
95 }
96 };
97
98 for structure in body.iter() {
100 if let hcl::Structure::Block(block) = structure {
101 if block.identifier() == "resource" {
102 if let Some(resource) = parse_resource_block(block, file_path) {
103 resources.push(resource);
104 }
105 }
106 }
107 }
108
109 resources
110}
111
112const K8S_RESOURCE_TYPES: &[&str] = &[
114 "kubernetes_deployment",
115 "kubernetes_deployment_v1",
116 "kubernetes_stateful_set",
117 "kubernetes_stateful_set_v1",
118 "kubernetes_daemon_set",
119 "kubernetes_daemon_set_v1",
120 "kubernetes_replication_controller",
121 "kubernetes_replication_controller_v1",
122 "kubernetes_job",
123 "kubernetes_job_v1",
124 "kubernetes_cron_job",
125 "kubernetes_cron_job_v1",
126 "kubernetes_pod",
127 "kubernetes_pod_v1",
128];
129
130fn parse_resource_block(block: &Block, file_path: &Path) -> Option<TerraformK8sResource> {
132 let labels: Vec<&str> = block.labels().iter().map(|l| l.as_str()).collect();
133
134 if labels.len() < 2 {
135 return None;
136 }
137
138 let resource_type = labels[0];
139 let tf_name = labels[1];
140
141 if !K8S_RESOURCE_TYPES.contains(&resource_type) {
143 return None;
144 }
145
146 let mut k8s_name = None;
147 let mut namespace = None;
148 let mut containers = Vec::new();
149
150 for attr_or_block in block.body().iter() {
152 if let hcl::Structure::Block(inner_block) = attr_or_block {
153 match inner_block.identifier() {
154 "metadata" => {
155 (k8s_name, namespace) = parse_metadata_block(inner_block);
156 }
157 "spec" => {
158 containers = parse_spec_block(inner_block, resource_type);
159 }
160 _ => {}
161 }
162 }
163 }
164
165 let image = containers.first().and_then(|c| c.image.as_deref());
167 let container_name = containers.first().map(|c| c.name.as_str());
168 let kind = match resource_type {
169 t if t.contains("deployment") => "Deployment",
170 t if t.contains("stateful_set") => "StatefulSet",
171 t if t.contains("daemon_set") => "DaemonSet",
172 t if t.contains("job") => "Job",
173 t if t.contains("cron_job") => "CronJob",
174 t if t.contains("pod") => "Pod",
175 _ => "Deployment",
176 };
177 let workload_type = detect_workload_type(image, container_name, kind);
178
179 Some(TerraformK8sResource {
180 resource_type: resource_type.to_string(),
181 tf_name: tf_name.to_string(),
182 k8s_name,
183 namespace,
184 workload_type,
185 containers,
186 source_file: file_path.to_string_lossy().to_string(),
187 })
188}
189
190fn parse_metadata_block(block: &Block) -> (Option<String>, Option<String>) {
192 let mut name = None;
193 let mut namespace = None;
194
195 for structure in block.body().iter() {
196 if let hcl::Structure::Attribute(attr) = structure {
197 match attr.key() {
198 "name" => {
199 name = expr_to_string(attr.expr());
200 }
201 "namespace" => {
202 namespace = expr_to_string(attr.expr());
203 }
204 _ => {}
205 }
206 }
207 }
208
209 (name, namespace)
210}
211
212fn parse_spec_block(block: &Block, resource_type: &str) -> Vec<TerraformContainer> {
214 let mut containers = Vec::new();
215
216 for structure in block.body().iter() {
219 if let hcl::Structure::Block(inner) = structure {
220 match inner.identifier() {
221 "template" => {
222 containers.extend(parse_template_block(inner));
223 }
224 "container" => {
225 if let Some(c) = parse_container_block(inner) {
227 containers.push(c);
228 }
229 }
230 "spec" if resource_type.contains("pod") => {
231 for s in inner.body().iter() {
233 if let hcl::Structure::Block(container_block) = s {
234 if container_block.identifier() == "container" {
235 if let Some(c) = parse_container_block(container_block) {
236 containers.push(c);
237 }
238 }
239 }
240 }
241 }
242 _ => {}
243 }
244 }
245 }
246
247 containers
248}
249
250fn parse_template_block(block: &Block) -> Vec<TerraformContainer> {
252 let mut containers = Vec::new();
253
254 for structure in block.body().iter() {
255 if let hcl::Structure::Block(inner) = structure {
256 if inner.identifier() == "spec" {
257 for s in inner.body().iter() {
258 if let hcl::Structure::Block(container_block) = s {
259 if container_block.identifier() == "container" {
260 if let Some(c) = parse_container_block(container_block) {
261 containers.push(c);
262 }
263 }
264 }
265 }
266 }
267 }
268 }
269
270 containers
271}
272
273fn parse_container_block(block: &Block) -> Option<TerraformContainer> {
275 let mut name = String::new();
276 let mut image = None;
277 let mut requests = None;
278 let mut limits = None;
279
280 for structure in block.body().iter() {
281 match structure {
282 hcl::Structure::Attribute(attr) => match attr.key() {
283 "name" => {
284 name = expr_to_string(attr.expr()).unwrap_or_default();
285 }
286 "image" => {
287 image = expr_to_string(attr.expr());
288 }
289 _ => {}
290 },
291 hcl::Structure::Block(inner) => {
292 if inner.identifier() == "resources" {
293 (requests, limits) = parse_resources_block(inner);
294 }
295 }
296 }
297 }
298
299 if name.is_empty() {
300 return None;
301 }
302
303 Some(TerraformContainer {
304 name,
305 image,
306 requests,
307 limits,
308 })
309}
310
311fn parse_resources_block(block: &Block) -> (Option<TfResourceSpec>, Option<TfResourceSpec>) {
313 let mut requests = None;
314 let mut limits = None;
315
316 for structure in block.body().iter() {
317 if let hcl::Structure::Block(inner) = structure {
318 match inner.identifier() {
319 "requests" => {
320 requests = parse_resource_spec_block(inner);
321 }
322 "limits" => {
323 limits = parse_resource_spec_block(inner);
324 }
325 _ => {}
326 }
327 }
328 }
329
330 (requests, limits)
331}
332
333fn parse_resource_spec_block(block: &Block) -> Option<TfResourceSpec> {
335 let mut cpu = None;
336 let mut memory = None;
337
338 for structure in block.body().iter() {
339 if let hcl::Structure::Attribute(attr) = structure {
340 match attr.key() {
341 "cpu" => {
342 if let Some(cpu_str) = expr_to_string(attr.expr()) {
343 cpu = parse_cpu_to_millicores(&cpu_str);
344 }
345 }
346 "memory" => {
347 if let Some(mem_str) = expr_to_string(attr.expr()) {
348 memory = parse_memory_to_bytes(&mem_str);
349 }
350 }
351 _ => {}
352 }
353 }
354 }
355
356 if cpu.is_some() || memory.is_some() {
357 Some(TfResourceSpec { cpu, memory })
358 } else {
359 None
360 }
361}
362
363fn expr_to_string(expr: &hcl::Expression) -> Option<String> {
365 match expr {
366 hcl::Expression::String(s) => Some(s.clone()),
367 hcl::Expression::Number(n) => Some(n.to_string()),
368 hcl::Expression::TemplateExpr(t) => {
369 Some(format!("{}", t))
371 }
372 _ => None,
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use std::io::Write;
380
381 #[test]
382 #[ignore] fn test_parse_kubernetes_deployment() {
384 let tf_content = r#"
385resource "kubernetes_deployment" "nginx" {
386 metadata {
387 name = "nginx-deployment"
388 namespace = "default"
389 }
390
391 spec {
392 replicas = 3
393
394 template {
395 spec {
396 container {
397 name = "nginx"
398 image = "nginx:1.21"
399
400 resources {
401 requests {
402 cpu = "100m"
403 memory = "128Mi"
404 }
405 limits {
406 cpu = "500m"
407 memory = "512Mi"
408 }
409 }
410 }
411 }
412 }
413 }
414}
415"#;
416 let mut temp = tempfile::NamedTempFile::new().unwrap();
418 temp.write_all(tf_content.as_bytes()).unwrap();
419 let path = temp.path();
420
421 let resources = parse_terraform_k8s_resources(path);
422
423 assert_eq!(resources.len(), 1);
424 let res = &resources[0];
425 assert_eq!(res.resource_type, "kubernetes_deployment");
426 assert_eq!(res.tf_name, "nginx");
427 assert_eq!(res.k8s_name, Some("nginx-deployment".to_string()));
428 assert_eq!(res.namespace, Some("default".to_string()));
429 assert_eq!(res.containers.len(), 1);
430
431 let container = &res.containers[0];
432 assert_eq!(container.name, "nginx");
433 assert_eq!(container.image, Some("nginx:1.21".to_string()));
434
435 let requests = container.requests.as_ref().unwrap();
437 assert_eq!(requests.cpu, Some(100)); assert_eq!(requests.memory, Some(128 * 1024 * 1024)); let limits = container.limits.as_ref().unwrap();
442 assert_eq!(limits.cpu, Some(500)); assert_eq!(limits.memory, Some(512 * 1024 * 1024)); }
445
446 #[test]
447 #[ignore] fn test_parse_deployment_missing_resources() {
449 let tf_content = r#"
450resource "kubernetes_deployment_v1" "app" {
451 metadata {
452 name = "my-app"
453 }
454
455 spec {
456 template {
457 spec {
458 container {
459 name = "app"
460 image = "myapp:latest"
461 }
462 }
463 }
464 }
465}
466"#;
467 let mut temp = tempfile::NamedTempFile::new().unwrap();
468 temp.write_all(tf_content.as_bytes()).unwrap();
469
470 let resources = parse_terraform_k8s_resources(temp.path());
471
472 assert_eq!(resources.len(), 1);
473 let container = &resources[0].containers[0];
474 assert!(container.requests.is_none());
475 assert!(container.limits.is_none());
476 }
477
478 #[test]
479 #[ignore] fn test_ignores_non_k8s_resources() {
481 let tf_content = r#"
482resource "aws_instance" "example" {
483 ami = "ami-12345"
484 instance_type = "t2.micro"
485}
486
487resource "kubernetes_deployment" "app" {
488 metadata {
489 name = "my-app"
490 }
491 spec {
492 template {
493 spec {
494 container {
495 name = "app"
496 image = "myapp:latest"
497 }
498 }
499 }
500 }
501}
502"#;
503 let mut temp = tempfile::NamedTempFile::new().unwrap();
504 temp.write_all(tf_content.as_bytes()).unwrap();
505
506 let resources = parse_terraform_k8s_resources(temp.path());
507
508 assert_eq!(resources.len(), 1);
510 assert_eq!(resources[0].resource_type, "kubernetes_deployment");
511 }
512}