use super::yaml::{detect_workload_type, parse_cpu_to_millicores, parse_memory_to_bytes};
use crate::analyzer::k8s_optimize::types::WorkloadType;
use hcl::{self, Block, Body};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct TfResourceSpec {
pub cpu: Option<u64>,
pub memory: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct TerraformK8sResource {
pub resource_type: String,
pub tf_name: String,
pub k8s_name: Option<String>,
pub namespace: Option<String>,
pub workload_type: WorkloadType,
pub containers: Vec<TerraformContainer>,
pub source_file: String,
}
#[derive(Debug, Clone)]
pub struct TerraformContainer {
pub name: String,
pub image: Option<String>,
pub requests: Option<TfResourceSpec>,
pub limits: Option<TfResourceSpec>,
}
pub fn parse_terraform_k8s_resources(path: &Path) -> Vec<TerraformK8sResource> {
let mut resources = Vec::new();
if path.is_file() {
if let Some(ext) = path.extension()
&& ext == "tf"
&& let Ok(content) = std::fs::read_to_string(path)
{
resources.extend(parse_tf_content(&content, path));
}
} else if path.is_dir()
&& let Ok(entries) = std::fs::read_dir(path)
{
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_file()
&& let Some(ext) = entry_path.extension()
&& ext == "tf"
&& let Ok(content) = std::fs::read_to_string(&entry_path)
{
resources.extend(parse_tf_content(&content, &entry_path));
}
}
}
resources
}
fn parse_tf_content(content: &str, file_path: &Path) -> Vec<TerraformK8sResource> {
let mut resources = Vec::new();
let body: Result<Body, _> = hcl::from_str(content);
let body = match body {
Ok(b) => b,
Err(e) => {
log::debug!("Failed to parse HCL in {:?}: {}", file_path, e);
return resources;
}
};
for structure in body.iter() {
if let hcl::Structure::Block(block) = structure
&& block.identifier() == "resource"
&& let Some(resource) = parse_resource_block(block, file_path)
{
resources.push(resource);
}
}
resources
}
const K8S_RESOURCE_TYPES: &[&str] = &[
"kubernetes_deployment",
"kubernetes_deployment_v1",
"kubernetes_stateful_set",
"kubernetes_stateful_set_v1",
"kubernetes_daemon_set",
"kubernetes_daemon_set_v1",
"kubernetes_replication_controller",
"kubernetes_replication_controller_v1",
"kubernetes_job",
"kubernetes_job_v1",
"kubernetes_cron_job",
"kubernetes_cron_job_v1",
"kubernetes_pod",
"kubernetes_pod_v1",
];
fn parse_resource_block(block: &Block, file_path: &Path) -> Option<TerraformK8sResource> {
let labels: Vec<&str> = block.labels().iter().map(|l| l.as_str()).collect();
if labels.len() < 2 {
return None;
}
let resource_type = labels[0];
let tf_name = labels[1];
if !K8S_RESOURCE_TYPES.contains(&resource_type) {
return None;
}
let mut k8s_name = None;
let mut namespace = None;
let mut containers = Vec::new();
for attr_or_block in block.body().iter() {
if let hcl::Structure::Block(inner_block) = attr_or_block {
match inner_block.identifier() {
"metadata" => {
(k8s_name, namespace) = parse_metadata_block(inner_block);
}
"spec" => {
containers = parse_spec_block(inner_block, resource_type);
}
_ => {}
}
}
}
let image = containers.first().and_then(|c| c.image.as_deref());
let container_name = containers.first().map(|c| c.name.as_str());
let kind = match resource_type {
t if t.contains("deployment") => "Deployment",
t if t.contains("stateful_set") => "StatefulSet",
t if t.contains("daemon_set") => "DaemonSet",
t if t.contains("job") => "Job",
t if t.contains("cron_job") => "CronJob",
t if t.contains("pod") => "Pod",
_ => "Deployment",
};
let workload_type = detect_workload_type(image, container_name, kind);
Some(TerraformK8sResource {
resource_type: resource_type.to_string(),
tf_name: tf_name.to_string(),
k8s_name,
namespace,
workload_type,
containers,
source_file: file_path.to_string_lossy().to_string(),
})
}
fn parse_metadata_block(block: &Block) -> (Option<String>, Option<String>) {
let mut name = None;
let mut namespace = None;
for structure in block.body().iter() {
if let hcl::Structure::Attribute(attr) = structure {
match attr.key() {
"name" => {
name = expr_to_string(attr.expr());
}
"namespace" => {
namespace = expr_to_string(attr.expr());
}
_ => {}
}
}
}
(name, namespace)
}
fn parse_spec_block(block: &Block, resource_type: &str) -> Vec<TerraformContainer> {
let mut containers = Vec::new();
for structure in block.body().iter() {
if let hcl::Structure::Block(inner) = structure {
match inner.identifier() {
"template" => {
containers.extend(parse_template_block(inner));
}
"container" => {
if let Some(c) = parse_container_block(inner) {
containers.push(c);
}
}
"spec" if resource_type.contains("pod") => {
for s in inner.body().iter() {
if let hcl::Structure::Block(container_block) = s
&& container_block.identifier() == "container"
&& let Some(c) = parse_container_block(container_block)
{
containers.push(c);
}
}
}
_ => {}
}
}
}
containers
}
fn parse_template_block(block: &Block) -> Vec<TerraformContainer> {
let mut containers = Vec::new();
for structure in block.body().iter() {
if let hcl::Structure::Block(inner) = structure
&& inner.identifier() == "spec"
{
for s in inner.body().iter() {
if let hcl::Structure::Block(container_block) = s
&& container_block.identifier() == "container"
&& let Some(c) = parse_container_block(container_block)
{
containers.push(c);
}
}
}
}
containers
}
fn parse_container_block(block: &Block) -> Option<TerraformContainer> {
let mut name = String::new();
let mut image = None;
let mut requests = None;
let mut limits = None;
for structure in block.body().iter() {
match structure {
hcl::Structure::Attribute(attr) => match attr.key() {
"name" => {
name = expr_to_string(attr.expr()).unwrap_or_default();
}
"image" => {
image = expr_to_string(attr.expr());
}
_ => {}
},
hcl::Structure::Block(inner) => {
if inner.identifier() == "resources" {
(requests, limits) = parse_resources_block(inner);
}
}
}
}
if name.is_empty() {
return None;
}
Some(TerraformContainer {
name,
image,
requests,
limits,
})
}
fn parse_resources_block(block: &Block) -> (Option<TfResourceSpec>, Option<TfResourceSpec>) {
let mut requests = None;
let mut limits = None;
for structure in block.body().iter() {
if let hcl::Structure::Block(inner) = structure {
match inner.identifier() {
"requests" => {
requests = parse_resource_spec_block(inner);
}
"limits" => {
limits = parse_resource_spec_block(inner);
}
_ => {}
}
}
}
(requests, limits)
}
fn parse_resource_spec_block(block: &Block) -> Option<TfResourceSpec> {
let mut cpu = None;
let mut memory = None;
for structure in block.body().iter() {
if let hcl::Structure::Attribute(attr) = structure {
match attr.key() {
"cpu" => {
if let Some(cpu_str) = expr_to_string(attr.expr()) {
cpu = parse_cpu_to_millicores(&cpu_str);
}
}
"memory" => {
if let Some(mem_str) = expr_to_string(attr.expr()) {
memory = parse_memory_to_bytes(&mem_str);
}
}
_ => {}
}
}
}
if cpu.is_some() || memory.is_some() {
Some(TfResourceSpec { cpu, memory })
} else {
None
}
}
fn expr_to_string(expr: &hcl::Expression) -> Option<String> {
match expr {
hcl::Expression::String(s) => Some(s.clone()),
hcl::Expression::Number(n) => Some(n.to_string()),
hcl::Expression::TemplateExpr(t) => {
Some(format!("{}", t))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
#[ignore] fn test_parse_kubernetes_deployment() {
let tf_content = r#"
resource "kubernetes_deployment" "nginx" {
metadata {
name = "nginx-deployment"
namespace = "default"
}
spec {
replicas = 3
template {
spec {
container {
name = "nginx"
image = "nginx:1.21"
resources {
requests {
cpu = "100m"
memory = "128Mi"
}
limits {
cpu = "500m"
memory = "512Mi"
}
}
}
}
}
}
}
"#;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(tf_content.as_bytes()).unwrap();
let path = temp.path();
let resources = parse_terraform_k8s_resources(path);
assert_eq!(resources.len(), 1);
let res = &resources[0];
assert_eq!(res.resource_type, "kubernetes_deployment");
assert_eq!(res.tf_name, "nginx");
assert_eq!(res.k8s_name, Some("nginx-deployment".to_string()));
assert_eq!(res.namespace, Some("default".to_string()));
assert_eq!(res.containers.len(), 1);
let container = &res.containers[0];
assert_eq!(container.name, "nginx");
assert_eq!(container.image, Some("nginx:1.21".to_string()));
let requests = container.requests.as_ref().unwrap();
assert_eq!(requests.cpu, Some(100)); assert_eq!(requests.memory, Some(128 * 1024 * 1024));
let limits = container.limits.as_ref().unwrap();
assert_eq!(limits.cpu, Some(500)); assert_eq!(limits.memory, Some(512 * 1024 * 1024)); }
#[test]
#[ignore] fn test_parse_deployment_missing_resources() {
let tf_content = r#"
resource "kubernetes_deployment_v1" "app" {
metadata {
name = "my-app"
}
spec {
template {
spec {
container {
name = "app"
image = "myapp:latest"
}
}
}
}
}
"#;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(tf_content.as_bytes()).unwrap();
let resources = parse_terraform_k8s_resources(temp.path());
assert_eq!(resources.len(), 1);
let container = &resources[0].containers[0];
assert!(container.requests.is_none());
assert!(container.limits.is_none());
}
#[test]
#[ignore] fn test_ignores_non_k8s_resources() {
let tf_content = r#"
resource "aws_instance" "example" {
ami = "ami-12345"
instance_type = "t2.micro"
}
resource "kubernetes_deployment" "app" {
metadata {
name = "my-app"
}
spec {
template {
spec {
container {
name = "app"
image = "myapp:latest"
}
}
}
}
}
"#;
let mut temp = tempfile::NamedTempFile::new().unwrap();
temp.write_all(tf_content.as_bytes()).unwrap();
let resources = parse_terraform_k8s_resources(temp.path());
assert_eq!(resources.len(), 1);
assert_eq!(resources[0].resource_type, "kubernetes_deployment");
}
}