syncable_cli/analyzer/
docker_analyzer.rs

1//! # Docker Analyzer Module
2//!
3//! This module provides Docker infrastructure analysis capabilities for detecting:
4//! - Dockerfiles and their variants (dockerfile.dev, dockerfile.prod, etc.)
5//! - Docker Compose files and their variants (docker-compose.dev.yaml, etc.)
6//! - Port mappings and networking configuration
7//! - Service discovery and inter-service communication
8//! - Container orchestration patterns
9
10use crate::error::Result;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17/// Represents a Docker infrastructure analysis
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct DockerAnalysis {
20    /// All Dockerfiles found in the project
21    pub dockerfiles: Vec<DockerfileInfo>,
22    /// All Docker Compose files found in the project
23    pub compose_files: Vec<ComposeFileInfo>,
24    /// Analyzed services from compose files
25    pub services: Vec<DockerService>,
26    /// Network configuration and service discovery
27    pub networking: NetworkingConfig,
28    /// Overall container orchestration pattern
29    pub orchestration_pattern: OrchestrationPattern,
30    /// Environment-specific configurations
31    pub environments: Vec<DockerEnvironment>,
32}
33
34/// Information about a Dockerfile
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct DockerfileInfo {
37    /// Path to the Dockerfile
38    pub path: PathBuf,
39    /// Environment this Dockerfile is for (dev, prod, staging, etc.)
40    pub environment: Option<String>,
41    /// Base image used
42    pub base_image: Option<String>,
43    /// Exposed ports from EXPOSE instructions
44    pub exposed_ports: Vec<u16>,
45    /// Working directory
46    pub workdir: Option<String>,
47    /// Entry point or CMD
48    pub entrypoint: Option<String>,
49    /// Environment variables defined
50    pub env_vars: Vec<String>,
51    /// Multi-stage build stages
52    pub build_stages: Vec<String>,
53    /// Whether it's a multi-stage build
54    pub is_multistage: bool,
55    /// Dockerfile instructions count (complexity indicator)
56    pub instruction_count: usize,
57}
58
59/// Information about a Docker Compose file
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct ComposeFileInfo {
62    /// Path to the compose file
63    pub path: PathBuf,
64    /// Environment this compose file is for
65    pub environment: Option<String>,
66    /// Compose file version
67    pub version: Option<String>,
68    /// Services defined in the compose file
69    pub service_names: Vec<String>,
70    /// Networks defined
71    pub networks: Vec<String>,
72    /// Volumes defined
73    pub volumes: Vec<String>,
74    /// External dependencies (external networks, volumes)
75    pub external_dependencies: Vec<String>,
76}
77
78/// Container orchestration patterns
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
80pub enum OrchestrationPattern {
81    /// Single container application
82    SingleContainer,
83    /// Multiple containers with docker-compose
84    DockerCompose,
85    /// Microservices architecture
86    Microservices,
87    /// Event-driven architecture
88    EventDriven,
89    /// Service mesh
90    ServiceMesh,
91    /// Mixed or complex pattern
92    Mixed,
93}
94
95/// Represents a Docker service from compose files
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct DockerService {
98    /// Service name
99    pub name: String,
100    /// Which compose file this service is defined in
101    pub compose_file: PathBuf,
102    /// Docker image or build context
103    pub image_or_build: ImageOrBuild,
104    /// Port mappings
105    pub ports: Vec<PortMapping>,
106    /// Environment variables
107    pub environment: HashMap<String, String>,
108    /// Service dependencies
109    pub depends_on: Vec<String>,
110    /// Networks this service is connected to
111    pub networks: Vec<String>,
112    /// Volumes mounted
113    pub volumes: Vec<VolumeMount>,
114    /// Health check configuration
115    pub health_check: Option<HealthCheck>,
116    /// Restart policy
117    pub restart_policy: Option<String>,
118    /// Resource limits
119    pub resource_limits: Option<ResourceLimits>,
120}
121
122/// Image or build configuration for a service
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub enum ImageOrBuild {
125    /// Uses a pre-built image
126    Image(String),
127    /// Builds from a Dockerfile
128    Build {
129        context: String,
130        dockerfile: Option<String>,
131        args: HashMap<String, String>,
132    },
133}
134
135/// Port mapping configuration
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct PortMapping {
138    /// Host port (external)
139    pub host_port: Option<u16>,
140    /// Container port (internal)
141    pub container_port: u16,
142    /// Protocol (tcp, udp)
143    pub protocol: String,
144    /// Whether this port is exposed to the host
145    pub exposed_to_host: bool,
146}
147
148/// Volume mount configuration
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct VolumeMount {
151    /// Source (host path or volume name)
152    pub source: String,
153    /// Target path in container
154    pub target: String,
155    /// Mount type (bind, volume, tmpfs)
156    pub mount_type: String,
157    /// Whether it's read-only
158    pub read_only: bool,
159}
160
161/// Health check configuration
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163pub struct HealthCheck {
164    /// Test command
165    pub test: String,
166    /// Interval between checks
167    pub interval: Option<String>,
168    /// Timeout for each check
169    pub timeout: Option<String>,
170    /// Number of retries
171    pub retries: Option<u32>,
172}
173
174/// Resource limits configuration
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176pub struct ResourceLimits {
177    /// CPU limit
178    pub cpu_limit: Option<String>,
179    /// Memory limit
180    pub memory_limit: Option<String>,
181    /// CPU reservation
182    pub cpu_reservation: Option<String>,
183    /// Memory reservation
184    pub memory_reservation: Option<String>,
185}
186
187/// Networking configuration analysis
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub struct NetworkingConfig {
190    /// Custom networks defined
191    pub custom_networks: Vec<NetworkInfo>,
192    /// Service discovery patterns
193    pub service_discovery: ServiceDiscoveryConfig,
194    /// Load balancing configuration
195    pub load_balancing: Vec<LoadBalancerConfig>,
196    /// External connectivity patterns
197    pub external_connectivity: ExternalConnectivity,
198}
199
200/// Network information
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct NetworkInfo {
203    /// Network name
204    pub name: String,
205    /// Network driver
206    pub driver: Option<String>,
207    /// Whether it's external
208    pub external: bool,
209    /// Connected services
210    pub connected_services: Vec<String>,
211}
212
213/// Service discovery configuration
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct ServiceDiscoveryConfig {
216    /// Whether services can discover each other by name
217    pub internal_dns: bool,
218    /// External service discovery tools
219    pub external_tools: Vec<String>,
220    /// Service mesh indicators
221    pub service_mesh: bool,
222}
223
224/// Load balancer configuration
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226pub struct LoadBalancerConfig {
227    /// Service name
228    pub service: String,
229    /// Load balancer type (nginx, traefik, etc.)
230    pub lb_type: String,
231    /// Backend services
232    pub backends: Vec<String>,
233}
234
235/// External connectivity patterns
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237pub struct ExternalConnectivity {
238    /// Services exposed to external traffic
239    pub exposed_services: Vec<ExposedService>,
240    /// Ingress patterns
241    pub ingress_patterns: Vec<String>,
242    /// API gateways
243    pub api_gateways: Vec<String>,
244}
245
246/// Service exposed to external traffic
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct ExposedService {
249    /// Service name
250    pub service: String,
251    /// External ports
252    pub external_ports: Vec<u16>,
253    /// Protocols
254    pub protocols: Vec<String>,
255    /// Whether it has SSL/TLS
256    pub ssl_enabled: bool,
257}
258
259/// Environment-specific Docker configuration
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
261pub struct DockerEnvironment {
262    /// Environment name (dev, prod, staging, etc.)
263    pub name: String,
264    /// Dockerfiles for this environment
265    pub dockerfiles: Vec<PathBuf>,
266    /// Compose files for this environment
267    pub compose_files: Vec<PathBuf>,
268    /// Environment-specific configurations
269    pub config_overrides: HashMap<String, String>,
270}
271
272/// Analyzes Docker infrastructure in a project
273pub fn analyze_docker_infrastructure(project_root: &Path) -> Result<DockerAnalysis> {
274    log::info!(
275        "Starting Docker infrastructure analysis for: {}",
276        project_root.display()
277    );
278
279    // Find all Docker-related files
280    let dockerfiles = find_dockerfiles(project_root)?;
281    let compose_files = find_compose_files(project_root)?;
282
283    log::debug!(
284        "Found {} Dockerfiles and {} Compose files",
285        dockerfiles.len(),
286        compose_files.len()
287    );
288
289    // Parse Dockerfiles
290    let parsed_dockerfiles: Vec<DockerfileInfo> = dockerfiles
291        .into_iter()
292        .filter_map(|path| parse_dockerfile(&path).ok())
293        .collect();
294
295    // Parse Compose files
296    let parsed_compose_files: Vec<ComposeFileInfo> = compose_files
297        .into_iter()
298        .filter_map(|path| parse_compose_file(&path).ok())
299        .collect();
300
301    // Extract services from compose files
302    let services = extract_services_from_compose(&parsed_compose_files)?;
303
304    // Analyze networking
305    let networking = analyze_networking(&services, &parsed_compose_files)?;
306
307    // Determine orchestration pattern
308    let orchestration_pattern = determine_orchestration_pattern(&services, &networking);
309
310    // Analyze environments
311    let environments = analyze_environments(&parsed_dockerfiles, &parsed_compose_files);
312
313    Ok(DockerAnalysis {
314        dockerfiles: parsed_dockerfiles,
315        compose_files: parsed_compose_files,
316        services,
317        networking,
318        orchestration_pattern,
319        environments,
320    })
321}
322
323/// Finds all Dockerfiles in the project, including variants
324fn find_dockerfiles(project_root: &Path) -> Result<Vec<PathBuf>> {
325    let mut dockerfiles = Vec::new();
326
327    fn collect_dockerfiles_recursive(dir: &Path, dockerfiles: &mut Vec<PathBuf>) -> Result<()> {
328        if dir.file_name().is_some_and(|name| {
329            name == "node_modules" || name == ".git" || name == "target" || name == ".next"
330        }) {
331            return Ok(());
332        }
333
334        for entry in fs::read_dir(dir)? {
335            let entry = entry?;
336            let path = entry.path();
337
338            if path.is_dir() {
339                collect_dockerfiles_recursive(&path, dockerfiles)?;
340            } else if let Some(filename) = path.file_name().and_then(|n| n.to_str())
341                && is_dockerfile_name(filename)
342            {
343                dockerfiles.push(path);
344            }
345        }
346        Ok(())
347    }
348
349    collect_dockerfiles_recursive(project_root, &mut dockerfiles)?;
350
351    Ok(dockerfiles)
352}
353
354/// Checks if a filename matches Dockerfile patterns
355fn is_dockerfile_name(filename: &str) -> bool {
356    let filename_lower = filename.to_lowercase();
357
358    // Exact matches
359    if filename_lower == "dockerfile" {
360        return true;
361    }
362
363    // Pattern matches
364    if filename_lower.starts_with("dockerfile.") {
365        return true;
366    }
367
368    if filename_lower.ends_with(".dockerfile") {
369        return true;
370    }
371
372    false
373}
374
375/// Finds all Docker Compose files in the project
376fn find_compose_files(project_root: &Path) -> Result<Vec<PathBuf>> {
377    let mut compose_files = Vec::new();
378
379    fn collect_compose_files_recursive(dir: &Path, compose_files: &mut Vec<PathBuf>) -> Result<()> {
380        if dir.file_name().is_some_and(|name| {
381            name == "node_modules" || name == ".git" || name == "target" || name == ".next"
382        }) {
383            return Ok(());
384        }
385
386        for entry in fs::read_dir(dir)? {
387            let entry = entry?;
388            let path = entry.path();
389
390            if path.is_dir() {
391                collect_compose_files_recursive(&path, compose_files)?;
392            } else if let Some(filename) = path.file_name().and_then(|n| n.to_str())
393                && is_compose_file_name(filename)
394            {
395                compose_files.push(path);
396            }
397        }
398        Ok(())
399    }
400
401    collect_compose_files_recursive(project_root, &mut compose_files)?;
402
403    Ok(compose_files)
404}
405
406/// Checks if a filename matches Docker Compose patterns
407fn is_compose_file_name(filename: &str) -> bool {
408    let filename_lower = filename.to_lowercase();
409
410    // Common compose file patterns
411    let patterns = [
412        "docker-compose.yml",
413        "docker-compose.yaml",
414        "compose.yml",
415        "compose.yaml",
416    ];
417
418    // Exact matches
419    for pattern in &patterns {
420        if filename_lower == *pattern {
421            return true;
422        }
423    }
424
425    // Environment-specific patterns
426    if filename_lower.starts_with("docker-compose.")
427        && (filename_lower.ends_with(".yml") || filename_lower.ends_with(".yaml"))
428    {
429        return true;
430    }
431
432    if filename_lower.starts_with("compose.")
433        && (filename_lower.ends_with(".yml") || filename_lower.ends_with(".yaml"))
434    {
435        return true;
436    }
437
438    false
439}
440
441/// Parses a Dockerfile and extracts information
442fn parse_dockerfile(path: &PathBuf) -> Result<DockerfileInfo> {
443    let content = fs::read_to_string(path)?;
444    let lines: Vec<&str> = content.lines().collect();
445
446    let mut info = DockerfileInfo {
447        path: path.clone(),
448        environment: extract_environment_from_filename(path),
449        base_image: None,
450        exposed_ports: Vec::new(),
451        workdir: None,
452        entrypoint: None,
453        env_vars: Vec::new(),
454        build_stages: Vec::new(),
455        is_multistage: false,
456        instruction_count: 0,
457    };
458
459    // Regex patterns for Dockerfile instructions
460    let from_regex = Regex::new(r"(?i)^FROM\s+(.+?)(?:\s+AS\s+(.+))?$").unwrap();
461    let expose_regex = Regex::new(r"(?i)^EXPOSE\s+(.+)$").unwrap();
462    let workdir_regex = Regex::new(r"(?i)^WORKDIR\s+(.+)$").unwrap();
463    let cmd_regex = Regex::new(r"(?i)^CMD\s+(.+)$").unwrap();
464    let entrypoint_regex = Regex::new(r"(?i)^ENTRYPOINT\s+(.+)$").unwrap();
465    let env_regex = Regex::new(r"(?i)^ENV\s+(.+)$").unwrap();
466
467    for line in lines {
468        let line = line.trim();
469        if line.is_empty() || line.starts_with('#') {
470            continue;
471        }
472
473        info.instruction_count += 1;
474
475        // Parse FROM instructions
476        if let Some(captures) = from_regex.captures(line) {
477            if info.base_image.is_none() {
478                info.base_image = Some(captures.get(1).unwrap().as_str().trim().to_string());
479            }
480            if let Some(stage_name) = captures.get(2) {
481                info.build_stages
482                    .push(stage_name.as_str().trim().to_string());
483                info.is_multistage = true;
484            }
485        }
486
487        // Parse EXPOSE instructions
488        if let Some(captures) = expose_regex.captures(line) {
489            let ports_str = captures.get(1).unwrap().as_str();
490            for port in ports_str.split_whitespace() {
491                if let Ok(port_num) = port.parse::<u16>() {
492                    info.exposed_ports.push(port_num);
493                }
494            }
495        }
496
497        // Parse WORKDIR
498        if let Some(captures) = workdir_regex.captures(line) {
499            info.workdir = Some(captures.get(1).unwrap().as_str().trim().to_string());
500        }
501
502        // Parse CMD and ENTRYPOINT
503        if let Some(captures) = cmd_regex.captures(line)
504            && info.entrypoint.is_none()
505        {
506            info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
507        }
508
509        if let Some(captures) = entrypoint_regex.captures(line) {
510            info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
511        }
512
513        // Parse ENV
514        if let Some(captures) = env_regex.captures(line) {
515            info.env_vars
516                .push(captures.get(1).unwrap().as_str().trim().to_string());
517        }
518    }
519
520    Ok(info)
521}
522
523/// Parses a Docker Compose file and extracts information
524fn parse_compose_file(path: &PathBuf) -> Result<ComposeFileInfo> {
525    let content = fs::read_to_string(path)?;
526
527    // Parse YAML content
528    let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| {
529        crate::error::AnalysisError::DependencyParsing {
530            file: path.display().to_string(),
531            reason: format!("YAML parsing error: {}", e),
532        }
533    })?;
534
535    let mut info = ComposeFileInfo {
536        path: path.clone(),
537        environment: extract_environment_from_filename(path),
538        version: None,
539        service_names: Vec::new(),
540        networks: Vec::new(),
541        volumes: Vec::new(),
542        external_dependencies: Vec::new(),
543    };
544
545    // Extract version
546    if let Some(version) = yaml_value.get("version").and_then(|v| v.as_str()) {
547        info.version = Some(version.to_string());
548    }
549
550    // Extract service names
551    if let Some(services) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
552        for (service_name, _) in services {
553            if let Some(name) = service_name.as_str() {
554                info.service_names.push(name.to_string());
555            }
556        }
557    }
558
559    // Extract networks
560    if let Some(networks) = yaml_value.get("networks").and_then(|n| n.as_mapping()) {
561        for (network_name, network_config) in networks {
562            if let Some(name) = network_name.as_str() {
563                info.networks.push(name.to_string());
564
565                // Check if it's external
566                if let Some(config) = network_config.as_mapping()
567                    && config
568                        .get("external")
569                        .and_then(|e| e.as_bool())
570                        .unwrap_or(false)
571                {
572                    info.external_dependencies.push(format!("network:{}", name));
573                }
574            }
575        }
576    }
577
578    // Extract volumes
579    if let Some(volumes) = yaml_value.get("volumes").and_then(|v| v.as_mapping()) {
580        for (volume_name, volume_config) in volumes {
581            if let Some(name) = volume_name.as_str() {
582                info.volumes.push(name.to_string());
583
584                // Check if it's external
585                if let Some(config) = volume_config.as_mapping()
586                    && config
587                        .get("external")
588                        .and_then(|e| e.as_bool())
589                        .unwrap_or(false)
590                {
591                    info.external_dependencies.push(format!("volume:{}", name));
592                }
593            }
594        }
595    }
596
597    Ok(info)
598}
599
600/// Extracts environment from filename (e.g., "dev" from "dockerfile.dev")
601fn extract_environment_from_filename(path: &Path) -> Option<String> {
602    if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
603        let filename_lower = filename.to_lowercase();
604
605        // Helper to map env shorthand to full name
606        let map_env = |env: &str| -> Option<String> {
607            match env {
608                "dev" | "development" | "local" => Some("development".to_string()),
609                "prod" | "production" => Some("production".to_string()),
610                "test" | "testing" => Some("test".to_string()),
611                "stage" | "staging" => Some("staging".to_string()),
612                _ if env.len() <= 10 && !env.is_empty() => Some(env.to_string()),
613                _ => None,
614            }
615        };
616
617        // Handle patterns like "docker-compose.prod.yml" (env between two dots)
618        if let Some(last_dot) = filename_lower.rfind('.')
619            && let Some(env_dot_pos) = filename_lower[..last_dot].rfind('.')
620        {
621            let env = &filename_lower[env_dot_pos + 1..last_dot];
622            if let Some(result) = map_env(env) {
623                return Some(result);
624            }
625        }
626
627        // Handle patterns like "Dockerfile.dev" (env is the extension itself)
628        if let Some(dot_pos) = filename_lower.rfind('.') {
629            let ext = &filename_lower[dot_pos + 1..];
630            // Only if the base is dockerfile/docker-compose related
631            let base = &filename_lower[..dot_pos];
632            if (base.contains("dockerfile") || base.contains("docker-compose") || base == "compose")
633                && let Some(result) = map_env(ext)
634            {
635                return Some(result);
636            }
637        }
638    }
639    None
640}
641
642/// Helper functions for parsing compose files
643fn extract_services_from_compose(compose_files: &[ComposeFileInfo]) -> Result<Vec<DockerService>> {
644    let mut services = Vec::new();
645
646    for compose_file in compose_files {
647        let content = fs::read_to_string(&compose_file.path)?;
648        let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| {
649            crate::error::AnalysisError::DependencyParsing {
650                file: compose_file.path.display().to_string(),
651                reason: format!("YAML parsing error: {}", e),
652            }
653        })?;
654
655        if let Some(services_yaml) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
656            for (service_name, service_config) in services_yaml {
657                if let (Some(name), Some(config)) =
658                    (service_name.as_str(), service_config.as_mapping())
659                {
660                    let service = parse_docker_service(name, config, &compose_file.path)?;
661                    services.push(service);
662                }
663            }
664        }
665    }
666
667    Ok(services)
668}
669
670/// Parses a Docker service from compose configuration
671fn parse_docker_service(
672    name: &str,
673    config: &serde_yaml::Mapping,
674    compose_file: &Path,
675) -> Result<DockerService> {
676    let mut service = DockerService {
677        name: name.to_string(),
678        compose_file: compose_file.to_path_buf(),
679        image_or_build: ImageOrBuild::Image("unknown".to_string()),
680        ports: Vec::new(),
681        environment: HashMap::new(),
682        depends_on: Vec::new(),
683        networks: Vec::new(),
684        volumes: Vec::new(),
685        health_check: None,
686        restart_policy: None,
687        resource_limits: None,
688    };
689
690    // Parse image or build
691    if let Some(image) = config.get("image").and_then(|i| i.as_str()) {
692        service.image_or_build = ImageOrBuild::Image(image.to_string());
693    } else if let Some(build_config) = config.get("build") {
694        if let Some(context) = build_config.as_str() {
695            service.image_or_build = ImageOrBuild::Build {
696                context: context.to_string(),
697                dockerfile: None,
698                args: HashMap::new(),
699            };
700        } else if let Some(build_mapping) = build_config.as_mapping() {
701            let context = build_mapping
702                .get("context")
703                .and_then(|c| c.as_str())
704                .unwrap_or(".")
705                .to_string();
706
707            let dockerfile = build_mapping
708                .get("dockerfile")
709                .and_then(|d| d.as_str())
710                .map(|s| s.to_string());
711
712            let mut args = HashMap::new();
713            if let Some(args_config) = build_mapping.get("args").and_then(|a| a.as_mapping()) {
714                for (key, value) in args_config {
715                    if let (Some(k), Some(v)) = (key.as_str(), value.as_str()) {
716                        args.insert(k.to_string(), v.to_string());
717                    }
718                }
719            }
720
721            service.image_or_build = ImageOrBuild::Build {
722                context,
723                dockerfile,
724                args,
725            };
726        }
727    }
728
729    // Parse ports
730    if let Some(ports_config) = config.get("ports").and_then(|p| p.as_sequence()) {
731        for port_item in ports_config {
732            if let Some(port_mapping) = parse_port_mapping(port_item) {
733                service.ports.push(port_mapping);
734            }
735        }
736    }
737
738    // Parse environment variables
739    if let Some(env_config) = config.get("environment") {
740        parse_environment_variables(env_config, &mut service.environment);
741    }
742
743    // Parse depends_on
744    if let Some(depends_config) = config.get("depends_on") {
745        if let Some(depends_sequence) = depends_config.as_sequence() {
746            for dep in depends_sequence {
747                if let Some(dep_name) = dep.as_str() {
748                    service.depends_on.push(dep_name.to_string());
749                }
750            }
751        } else if let Some(depends_mapping) = depends_config.as_mapping() {
752            for (dep_name, _) in depends_mapping {
753                if let Some(name) = dep_name.as_str() {
754                    service.depends_on.push(name.to_string());
755                }
756            }
757        }
758    }
759
760    // Parse networks
761    if let Some(networks_config) = config.get("networks") {
762        if let Some(networks_sequence) = networks_config.as_sequence() {
763            for network in networks_sequence {
764                if let Some(network_name) = network.as_str() {
765                    service.networks.push(network_name.to_string());
766                }
767            }
768        } else if let Some(networks_mapping) = networks_config.as_mapping() {
769            for (network_name, _) in networks_mapping {
770                if let Some(name) = network_name.as_str() {
771                    service.networks.push(name.to_string());
772                }
773            }
774        }
775    }
776
777    // Parse volumes
778    if let Some(volumes_config) = config.get("volumes").and_then(|v| v.as_sequence()) {
779        for volume_item in volumes_config {
780            if let Some(volume_mount) = parse_volume_mount(volume_item) {
781                service.volumes.push(volume_mount);
782            }
783        }
784    }
785
786    // Parse restart policy
787    if let Some(restart) = config.get("restart").and_then(|r| r.as_str()) {
788        service.restart_policy = Some(restart.to_string());
789    }
790
791    // Parse health check
792    if let Some(healthcheck_config) = config.get("healthcheck").and_then(|h| h.as_mapping())
793        && let Some(test) = healthcheck_config.get("test").and_then(|t| t.as_str())
794    {
795        service.health_check = Some(HealthCheck {
796            test: test.to_string(),
797            interval: healthcheck_config
798                .get("interval")
799                .and_then(|i| i.as_str())
800                .map(|s| s.to_string()),
801            timeout: healthcheck_config
802                .get("timeout")
803                .and_then(|t| t.as_str())
804                .map(|s| s.to_string()),
805            retries: healthcheck_config
806                .get("retries")
807                .and_then(|r| r.as_u64())
808                .map(|r| r as u32),
809        });
810    }
811
812    Ok(service)
813}
814
815/// Parses port mapping from YAML value
816fn parse_port_mapping(port_value: &serde_yaml::Value) -> Option<PortMapping> {
817    if let Some(port_str) = port_value.as_str() {
818        // Handle string format like "8080:80" or "80"
819        if let Some(colon_pos) = port_str.find(':') {
820            let host_part = &port_str[..colon_pos];
821            let container_part = &port_str[colon_pos + 1..];
822
823            if let (Ok(host_port), Ok(container_port)) =
824                (host_part.parse::<u16>(), container_part.parse::<u16>())
825            {
826                return Some(PortMapping {
827                    host_port: Some(host_port),
828                    container_port,
829                    protocol: "tcp".to_string(),
830                    exposed_to_host: true,
831                });
832            }
833        } else if let Ok(container_port) = port_str.parse::<u16>() {
834            return Some(PortMapping {
835                host_port: None,
836                container_port,
837                protocol: "tcp".to_string(),
838                exposed_to_host: false,
839            });
840        }
841    } else if let Some(port_num) = port_value.as_u64() {
842        return Some(PortMapping {
843            host_port: None,
844            container_port: port_num as u16,
845            protocol: "tcp".to_string(),
846            exposed_to_host: false,
847        });
848    }
849
850    None
851}
852
853/// Parses volume mount from YAML value
854fn parse_volume_mount(volume_value: &serde_yaml::Value) -> Option<VolumeMount> {
855    if let Some(volume_str) = volume_value.as_str() {
856        // Handle string format like "./data:/app/data:ro" or "./data:/app/data"
857        let parts: Vec<&str> = volume_str.split(':').collect();
858        if parts.len() >= 2 {
859            return Some(VolumeMount {
860                source: parts[0].to_string(),
861                target: parts[1].to_string(),
862                mount_type: if parts[0].starts_with('/') || parts[0].starts_with('.') {
863                    "bind".to_string()
864                } else {
865                    "volume".to_string()
866                },
867                read_only: parts.get(2).is_some_and(|&opt| opt == "ro"),
868            });
869        }
870    }
871    None
872}
873
874/// Parses environment variables from YAML
875fn parse_environment_variables(
876    env_value: &serde_yaml::Value,
877    env_map: &mut HashMap<String, String>,
878) {
879    if let Some(env_mapping) = env_value.as_mapping() {
880        for (key, value) in env_mapping {
881            if let Some(key_str) = key.as_str() {
882                let value_str = value.as_str().unwrap_or("").to_string();
883                env_map.insert(key_str.to_string(), value_str);
884            }
885        }
886    } else if let Some(env_sequence) = env_value.as_sequence() {
887        for env_item in env_sequence {
888            if let Some(env_str) = env_item.as_str() {
889                if let Some(eq_pos) = env_str.find('=') {
890                    let key = env_str[..eq_pos].to_string();
891                    let value = env_str[eq_pos + 1..].to_string();
892                    env_map.insert(key, value);
893                } else {
894                    env_map.insert(env_str.to_string(), String::new());
895                }
896            }
897        }
898    }
899}
900
901fn analyze_networking(
902    services: &[DockerService],
903    compose_files: &[ComposeFileInfo],
904) -> Result<NetworkingConfig> {
905    let mut custom_networks = Vec::new();
906    let mut connected_services: HashMap<String, Vec<String>> = HashMap::new();
907
908    // Collect networks from compose files
909    for compose_file in compose_files {
910        for network_name in &compose_file.networks {
911            let network_info = NetworkInfo {
912                name: network_name.clone(),
913                driver: None, // TODO: Parse driver from compose file
914                external: compose_file
915                    .external_dependencies
916                    .contains(&format!("network:{}", network_name)),
917                connected_services: Vec::new(),
918            };
919            custom_networks.push(network_info);
920        }
921    }
922
923    // Map services to networks
924    for service in services {
925        for network in &service.networks {
926            connected_services
927                .entry(network.clone())
928                .or_default()
929                .push(service.name.clone());
930        }
931    }
932
933    // Update network info with connected services
934    for network in &mut custom_networks {
935        if let Some(services) = connected_services.get(&network.name) {
936            network.connected_services = services.clone();
937        }
938    }
939
940    // Analyze service discovery
941    let service_discovery = ServiceDiscoveryConfig {
942        internal_dns: !services.is_empty(), // Docker Compose provides internal DNS
943        external_tools: detect_service_discovery_tools(services),
944        service_mesh: detect_service_mesh(services),
945    };
946
947    // Analyze load balancing
948    let load_balancing = detect_load_balancers(services);
949
950    // Analyze external connectivity
951    let external_connectivity = analyze_external_connectivity(services);
952
953    Ok(NetworkingConfig {
954        custom_networks,
955        service_discovery,
956        load_balancing,
957        external_connectivity,
958    })
959}
960
961fn determine_orchestration_pattern(
962    services: &[DockerService],
963    networking: &NetworkingConfig,
964) -> OrchestrationPattern {
965    if services.is_empty() {
966        return OrchestrationPattern::SingleContainer;
967    }
968
969    if services.len() == 1 {
970        return OrchestrationPattern::SingleContainer;
971    }
972
973    // Check for microservices patterns
974    let has_multiple_backends = services
975        .iter()
976        .filter(|s| match &s.image_or_build {
977            ImageOrBuild::Image(img) => {
978                !img.contains("nginx") && !img.contains("proxy") && !img.contains("traefik")
979            }
980            _ => true,
981        })
982        .count()
983        > 2;
984
985    let has_service_discovery = networking.service_discovery.internal_dns
986        || !networking.service_discovery.external_tools.is_empty();
987
988    let _has_load_balancing = !networking.load_balancing.is_empty();
989
990    let has_message_queues = services.iter().any(|s| match &s.image_or_build {
991        ImageOrBuild::Image(img) => {
992            img.contains("redis")
993                || img.contains("rabbitmq")
994                || img.contains("kafka")
995                || img.contains("nats")
996        }
997        _ => false,
998    });
999
1000    if networking.service_discovery.service_mesh {
1001        OrchestrationPattern::ServiceMesh
1002    } else if has_message_queues && has_multiple_backends {
1003        OrchestrationPattern::EventDriven
1004    } else if has_multiple_backends && has_service_discovery {
1005        OrchestrationPattern::Microservices
1006    } else {
1007        OrchestrationPattern::DockerCompose
1008    }
1009}
1010
1011/// Detects service discovery tools in the services
1012fn detect_service_discovery_tools(services: &[DockerService]) -> Vec<String> {
1013    let mut tools = Vec::new();
1014
1015    for service in services {
1016        if let ImageOrBuild::Image(image) = &service.image_or_build {
1017            if image.contains("consul") {
1018                tools.push("consul".to_string());
1019            }
1020            if image.contains("etcd") {
1021                tools.push("etcd".to_string());
1022            }
1023            if image.contains("zookeeper") {
1024                tools.push("zookeeper".to_string());
1025            }
1026        }
1027    }
1028
1029    tools.sort();
1030    tools.dedup();
1031    tools
1032}
1033
1034/// Detects service mesh presence
1035fn detect_service_mesh(services: &[DockerService]) -> bool {
1036    services.iter().any(|s| {
1037        if let ImageOrBuild::Image(image) = &s.image_or_build {
1038            image.contains("istio")
1039                || image.contains("linkerd")
1040                || image.contains("envoy")
1041                || image.contains("consul-connect")
1042        } else {
1043            false
1044        }
1045    })
1046}
1047
1048/// Detects load balancers in the services
1049fn detect_load_balancers(services: &[DockerService]) -> Vec<LoadBalancerConfig> {
1050    let mut load_balancers = Vec::new();
1051
1052    for service in services {
1053        // Check if service image indicates a load balancer
1054        let is_load_balancer = match &service.image_or_build {
1055            ImageOrBuild::Image(image) => {
1056                image.contains("nginx")
1057                    || image.contains("traefik")
1058                    || image.contains("haproxy")
1059                    || image.contains("envoy")
1060                    || image.contains("kong")
1061            }
1062            _ => false,
1063        };
1064
1065        if is_load_balancer {
1066            // Find potential backend services (services this one doesn't depend on)
1067            let backends: Vec<String> = services
1068                .iter()
1069                .filter(|s| s.name != service.name && !service.depends_on.contains(&s.name))
1070                .map(|s| s.name.clone())
1071                .collect();
1072
1073            if !backends.is_empty() {
1074                let lb_type = match &service.image_or_build {
1075                    ImageOrBuild::Image(image) => {
1076                        if image.contains("nginx") {
1077                            "nginx"
1078                        } else if image.contains("traefik") {
1079                            "traefik"
1080                        } else if image.contains("haproxy") {
1081                            "haproxy"
1082                        } else if image.contains("envoy") {
1083                            "envoy"
1084                        } else if image.contains("kong") {
1085                            "kong"
1086                        } else {
1087                            "unknown"
1088                        }
1089                    }
1090                    _ => "unknown",
1091                };
1092
1093                load_balancers.push(LoadBalancerConfig {
1094                    service: service.name.clone(),
1095                    lb_type: lb_type.to_string(),
1096                    backends,
1097                });
1098            }
1099        }
1100    }
1101
1102    load_balancers
1103}
1104
1105/// Analyzes external connectivity patterns
1106fn analyze_external_connectivity(services: &[DockerService]) -> ExternalConnectivity {
1107    let mut exposed_services = Vec::new();
1108    let mut ingress_patterns = Vec::new();
1109    let mut api_gateways = Vec::new();
1110
1111    for service in services {
1112        let mut external_ports = Vec::new();
1113        let mut protocols = Vec::new();
1114
1115        // Check for exposed ports
1116        for port in &service.ports {
1117            if port.exposed_to_host {
1118                if let Some(host_port) = port.host_port {
1119                    external_ports.push(host_port);
1120                }
1121                protocols.push(port.protocol.clone());
1122            }
1123        }
1124
1125        if !external_ports.is_empty() {
1126            // Check for SSL/TLS indicators
1127            let ssl_enabled = external_ports.contains(&443)
1128                || external_ports.contains(&8443)
1129                || service
1130                    .environment
1131                    .keys()
1132                    .any(|k| k.to_lowercase().contains("ssl") || k.to_lowercase().contains("tls"));
1133
1134            exposed_services.push(ExposedService {
1135                service: service.name.clone(),
1136                external_ports,
1137                protocols: protocols
1138                    .into_iter()
1139                    .collect::<std::collections::HashSet<_>>()
1140                    .into_iter()
1141                    .collect(),
1142                ssl_enabled,
1143            });
1144        }
1145
1146        // Detect API gateways
1147        if service.name.to_lowercase().contains("gateway")
1148            || service.name.to_lowercase().contains("api")
1149            || service.name.to_lowercase().contains("proxy")
1150        {
1151            api_gateways.push(service.name.clone());
1152        }
1153
1154        // Also check image for API gateway patterns
1155        if let ImageOrBuild::Image(image) = &service.image_or_build
1156            && (image.contains("kong")
1157                || image.contains("zuul")
1158                || image.contains("ambassador")
1159                || image.contains("traefik"))
1160            && !api_gateways.contains(&service.name)
1161        {
1162            api_gateways.push(service.name.clone());
1163        }
1164    }
1165
1166    // Detect ingress patterns
1167    if exposed_services.len() == 1 && api_gateways.len() == 1 {
1168        ingress_patterns.push("Single API Gateway".to_string());
1169    } else if exposed_services.len() > 1 && api_gateways.is_empty() {
1170        ingress_patterns.push("Multiple Direct Entry Points".to_string());
1171    } else if !api_gateways.is_empty() {
1172        ingress_patterns.push("API Gateway Pattern".to_string());
1173    }
1174
1175    // Detect reverse proxy patterns
1176    let has_reverse_proxy = services.iter().any(|s| {
1177        if let ImageOrBuild::Image(image) = &s.image_or_build {
1178            image.contains("nginx") || image.contains("apache") || image.contains("caddy")
1179        } else {
1180            false
1181        }
1182    });
1183
1184    if has_reverse_proxy {
1185        ingress_patterns.push("Reverse Proxy".to_string());
1186    }
1187
1188    ExternalConnectivity {
1189        exposed_services,
1190        ingress_patterns,
1191        api_gateways,
1192    }
1193}
1194
1195fn analyze_environments(
1196    dockerfiles: &[DockerfileInfo],
1197    compose_files: &[ComposeFileInfo],
1198) -> Vec<DockerEnvironment> {
1199    let mut environments: HashMap<String, DockerEnvironment> = HashMap::new();
1200
1201    // Collect environments from Dockerfiles
1202    for dockerfile in dockerfiles {
1203        let env_name = dockerfile
1204            .environment
1205            .clone()
1206            .unwrap_or_else(|| "default".to_string());
1207        environments
1208            .entry(env_name.clone())
1209            .or_insert_with(|| DockerEnvironment {
1210                name: env_name,
1211                dockerfiles: Vec::new(),
1212                compose_files: Vec::new(),
1213                config_overrides: HashMap::new(),
1214            })
1215            .dockerfiles
1216            .push(dockerfile.path.clone());
1217    }
1218
1219    // Collect environments from Compose files
1220    for compose_file in compose_files {
1221        let env_name = compose_file
1222            .environment
1223            .clone()
1224            .unwrap_or_else(|| "default".to_string());
1225        environments
1226            .entry(env_name.clone())
1227            .or_insert_with(|| DockerEnvironment {
1228                name: env_name,
1229                dockerfiles: Vec::new(),
1230                compose_files: Vec::new(),
1231                config_overrides: HashMap::new(),
1232            })
1233            .compose_files
1234            .push(compose_file.path.clone());
1235    }
1236
1237    environments.into_values().collect()
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::*;
1243
1244    #[test]
1245    fn test_is_dockerfile_name() {
1246        assert!(is_dockerfile_name("Dockerfile"));
1247        assert!(is_dockerfile_name("dockerfile"));
1248        assert!(is_dockerfile_name("Dockerfile.dev"));
1249        assert!(is_dockerfile_name("dockerfile.prod"));
1250        assert!(is_dockerfile_name("api.dockerfile"));
1251        assert!(!is_dockerfile_name("README.md"));
1252        assert!(!is_dockerfile_name("package.json"));
1253    }
1254
1255    #[test]
1256    fn test_is_compose_file_name() {
1257        assert!(is_compose_file_name("docker-compose.yml"));
1258        assert!(is_compose_file_name("docker-compose.yaml"));
1259        assert!(is_compose_file_name("docker-compose.dev.yml"));
1260        assert!(is_compose_file_name("docker-compose.prod.yaml"));
1261        assert!(is_compose_file_name("compose.yml"));
1262        assert!(is_compose_file_name("compose.yaml"));
1263        assert!(!is_compose_file_name("README.md"));
1264        assert!(!is_compose_file_name("package.json"));
1265    }
1266
1267    #[test]
1268    fn test_extract_environment_from_filename() {
1269        assert_eq!(
1270            extract_environment_from_filename(&PathBuf::from("Dockerfile.dev")),
1271            Some("development".to_string())
1272        );
1273        assert_eq!(
1274            extract_environment_from_filename(&PathBuf::from("docker-compose.prod.yml")),
1275            Some("production".to_string())
1276        );
1277        assert_eq!(
1278            extract_environment_from_filename(&PathBuf::from("Dockerfile")),
1279            None
1280        );
1281    }
1282}