use crate::analyzer::InfrastructurePresence;
use crate::common::file_utils::is_readable_file;
use std::path::{Path, PathBuf};
const K8S_DIRECTORIES: &[&str] = &[
"k8s",
"kubernetes",
"deploy",
"deployment",
"deployments",
"manifests",
"kube",
"charts",
".k8s",
];
const COMPOSE_FILES: &[&str] = &[
"docker-compose.yml",
"docker-compose.yaml",
"compose.yml",
"compose.yaml",
"docker-compose.dev.yml",
"docker-compose.prod.yml",
"docker-compose.local.yml",
];
pub fn detect_infrastructure(project_root: &Path) -> InfrastructurePresence {
let mut infra = InfrastructurePresence::default();
for compose_file in COMPOSE_FILES {
if is_readable_file(&project_root.join(compose_file)) {
infra.has_docker_compose = true;
break;
}
}
let k8s_paths = detect_kubernetes_manifests(project_root);
if !k8s_paths.is_empty() {
infra.has_kubernetes = true;
infra.kubernetes_paths = k8s_paths;
}
let helm_paths = detect_helm_charts(project_root);
if !helm_paths.is_empty() {
infra.has_helm = true;
infra.helm_chart_paths = helm_paths;
}
let tf_paths = detect_terraform(project_root);
if !tf_paths.is_empty() {
infra.has_terraform = true;
infra.terraform_paths = tf_paths;
}
infra.has_deployment_config = project_root.join(".syncable").is_dir()
|| is_readable_file(&project_root.join("syncable.json"))
|| is_readable_file(&project_root.join("syncable.yaml"))
|| is_readable_file(&project_root.join("syncable.yml"));
if infra.has_any() {
let types = infra.detected_types();
infra.summary = Some(format!("Detected: {}", types.join(", ")));
}
infra
}
fn detect_kubernetes_manifests(project_root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
for dir_name in K8S_DIRECTORIES {
let dir_path = project_root.join(dir_name);
if dir_path.is_dir() && has_kubernetes_files(&dir_path) {
paths.push(dir_path);
}
}
if let Ok(entries) = std::fs::read_dir(project_root) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if (ext == "yaml" || ext == "yml") && is_kubernetes_manifest(&path) {
paths.push(path);
}
}
}
}
}
paths
}
fn has_kubernetes_files(dir: &Path) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if (ext == "yaml" || ext == "yml") && is_kubernetes_manifest(&path) {
return true;
}
}
}
}
}
false
}
fn is_kubernetes_manifest(path: &Path) -> bool {
if let Ok(content) = std::fs::read_to_string(path) {
let check_content = if content.len() > 2048 {
&content[..2048]
} else {
&content
};
let k8s_kinds = [
"kind: Deployment",
"kind: Service",
"kind: Pod",
"kind: ConfigMap",
"kind: Secret",
"kind: Ingress",
"kind: StatefulSet",
"kind: DaemonSet",
"kind: Job",
"kind: CronJob",
"kind: PersistentVolumeClaim",
"kind: ServiceAccount",
"kind: Role",
"kind: RoleBinding",
"kind: ClusterRole",
"kind: ClusterRoleBinding",
"kind: NetworkPolicy",
"kind: HorizontalPodAutoscaler",
"kind: PodDisruptionBudget",
"kind: Namespace",
];
if check_content.contains("apiVersion:") {
for kind in &k8s_kinds {
if check_content.contains(*kind) {
return true;
}
}
}
}
false
}
fn detect_helm_charts(project_root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
if is_readable_file(&project_root.join("Chart.yaml")) {
paths.push(project_root.to_path_buf());
}
let helm_locations = ["charts", "helm", "deploy/helm", "deployment/helm"];
for location in &helm_locations {
let dir = project_root.join(location);
if dir.is_dir() {
if is_readable_file(&dir.join("Chart.yaml")) {
paths.push(dir.clone());
}
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() && is_readable_file(&path.join("Chart.yaml")) {
paths.push(path);
}
}
}
}
}
paths
}
fn detect_terraform(project_root: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
let tf_locations = ["terraform", "infra", "infrastructure", "tf", "iac"];
for location in &tf_locations {
let dir = project_root.join(location);
if dir.is_dir() && has_terraform_files(&dir) {
paths.push(dir);
}
}
if has_terraform_files(project_root) {
paths.push(project_root.to_path_buf());
}
paths
}
fn has_terraform_files(dir: &Path) -> bool {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "tf" {
return true;
}
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_detect_empty_project() {
let temp_dir = TempDir::new().unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(!infra.has_any());
}
#[test]
fn test_detect_docker_compose() {
let temp_dir = TempDir::new().unwrap();
fs::write(
temp_dir.path().join("docker-compose.yml"),
"version: '3'\nservices:\n app:\n build: .",
)
.unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_docker_compose);
assert!(infra.has_any());
}
#[test]
fn test_detect_kubernetes_manifest() {
let temp_dir = TempDir::new().unwrap();
let k8s_dir = temp_dir.path().join("k8s");
fs::create_dir(&k8s_dir).unwrap();
fs::write(
k8s_dir.join("deployment.yaml"),
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: test",
)
.unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_kubernetes);
assert_eq!(infra.kubernetes_paths.len(), 1);
}
#[test]
fn test_detect_helm_chart() {
let temp_dir = TempDir::new().unwrap();
let helm_dir = temp_dir.path().join("charts").join("myapp");
fs::create_dir_all(&helm_dir).unwrap();
fs::write(
helm_dir.join("Chart.yaml"),
"apiVersion: v2\nname: myapp\nversion: 1.0.0",
)
.unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_helm);
assert!(!infra.helm_chart_paths.is_empty());
}
#[test]
fn test_detect_terraform() {
let temp_dir = TempDir::new().unwrap();
let tf_dir = temp_dir.path().join("terraform");
fs::create_dir(&tf_dir).unwrap();
fs::write(
tf_dir.join("main.tf"),
"provider \"aws\" {\n region = \"us-east-1\"\n}",
)
.unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_terraform);
assert!(!infra.terraform_paths.is_empty());
}
#[test]
fn test_detect_syncable_config() {
let temp_dir = TempDir::new().unwrap();
let syncable_dir = temp_dir.path().join(".syncable");
fs::create_dir(&syncable_dir).unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_deployment_config);
}
#[test]
fn test_infrastructure_summary() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("docker-compose.yml"), "version: '3'").unwrap();
let tf_dir = temp_dir.path().join("terraform");
fs::create_dir(&tf_dir).unwrap();
fs::write(tf_dir.join("main.tf"), "provider \"aws\" {}").unwrap();
let infra = detect_infrastructure(temp_dir.path());
assert!(infra.has_docker_compose);
assert!(infra.has_terraform);
assert!(infra.summary.is_some());
let summary = infra.summary.unwrap();
assert!(summary.contains("Docker Compose"));
assert!(summary.contains("Terraform"));
}
}