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 serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::fs;
15use regex::Regex;
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!("Starting Docker infrastructure analysis for: {}", project_root.display());
275    
276    // Find all Docker-related files
277    let dockerfiles = find_dockerfiles(project_root)?;
278    let compose_files = find_compose_files(project_root)?;
279    
280    log::debug!("Found {} Dockerfiles and {} Compose files", dockerfiles.len(), compose_files.len());
281    
282    // Parse Dockerfiles
283    let parsed_dockerfiles: Vec<DockerfileInfo> = dockerfiles.into_iter()
284        .filter_map(|path| parse_dockerfile(&path).ok())
285        .collect();
286    
287    // Parse Compose files
288    let parsed_compose_files: Vec<ComposeFileInfo> = compose_files.into_iter()
289        .filter_map(|path| parse_compose_file(&path).ok())
290        .collect();
291    
292    // Extract services from compose files
293    let services = extract_services_from_compose(&parsed_compose_files)?;
294    
295    // Analyze networking
296    let networking = analyze_networking(&services, &parsed_compose_files)?;
297    
298    // Determine orchestration pattern
299    let orchestration_pattern = determine_orchestration_pattern(&services, &networking);
300    
301    // Analyze environments
302    let environments = analyze_environments(&parsed_dockerfiles, &parsed_compose_files);
303    
304    Ok(DockerAnalysis {
305        dockerfiles: parsed_dockerfiles,
306        compose_files: parsed_compose_files,
307        services,
308        networking,
309        orchestration_pattern,
310        environments,
311    })
312}
313
314/// Finds all Dockerfiles in the project, including variants
315fn find_dockerfiles(project_root: &Path) -> Result<Vec<PathBuf>> {
316    let mut dockerfiles = Vec::new();
317    
318    fn collect_dockerfiles_recursive(dir: &Path, dockerfiles: &mut Vec<PathBuf>) -> Result<()> {
319        if dir.file_name().map_or(false, |name| {
320            name == "node_modules" || name == ".git" || name == "target" || name == ".next"
321        }) {
322            return Ok(());
323        }
324        
325        for entry in fs::read_dir(dir)? {
326            let entry = entry?;
327            let path = entry.path();
328            
329            if path.is_dir() {
330                collect_dockerfiles_recursive(&path, dockerfiles)?;
331            } else if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
332                if is_dockerfile_name(filename) {
333                    dockerfiles.push(path);
334                }
335            }
336        }
337        Ok(())
338    }
339    
340    collect_dockerfiles_recursive(project_root, &mut dockerfiles)?;
341    
342    Ok(dockerfiles)
343}
344
345/// Checks if a filename matches Dockerfile patterns
346fn is_dockerfile_name(filename: &str) -> bool {
347    let filename_lower = filename.to_lowercase();
348    
349    // Exact matches
350    if filename_lower == "dockerfile" {
351        return true;
352    }
353    
354    // Pattern matches
355    if filename_lower.starts_with("dockerfile.") {
356        return true;
357    }
358    
359    if filename_lower.ends_with(".dockerfile") {
360        return true;
361    }
362    
363    false
364}
365
366/// Finds all Docker Compose files in the project
367fn find_compose_files(project_root: &Path) -> Result<Vec<PathBuf>> {
368    let mut compose_files = Vec::new();
369    
370    fn collect_compose_files_recursive(dir: &Path, compose_files: &mut Vec<PathBuf>) -> Result<()> {
371        if dir.file_name().map_or(false, |name| {
372            name == "node_modules" || name == ".git" || name == "target" || name == ".next"
373        }) {
374            return Ok(());
375        }
376        
377        for entry in fs::read_dir(dir)? {
378            let entry = entry?;
379            let path = entry.path();
380            
381            if path.is_dir() {
382                collect_compose_files_recursive(&path, compose_files)?;
383            } else if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
384                if is_compose_file_name(filename) {
385                    compose_files.push(path);
386                }
387            }
388        }
389        Ok(())
390    }
391    
392    collect_compose_files_recursive(project_root, &mut compose_files)?;
393    
394    Ok(compose_files)
395}
396
397/// Checks if a filename matches Docker Compose patterns
398fn is_compose_file_name(filename: &str) -> bool {
399    let filename_lower = filename.to_lowercase();
400    
401    // Common compose file patterns
402    let patterns = [
403        "docker-compose.yml",
404        "docker-compose.yaml",
405        "compose.yml",
406        "compose.yaml",
407    ];
408    
409    // Exact matches
410    for pattern in &patterns {
411        if filename_lower == *pattern {
412            return true;
413        }
414    }
415    
416    // Environment-specific patterns
417    if filename_lower.starts_with("docker-compose.") && 
418       (filename_lower.ends_with(".yml") || filename_lower.ends_with(".yaml")) {
419        return true;
420    }
421    
422    if filename_lower.starts_with("compose.") && 
423       (filename_lower.ends_with(".yml") || filename_lower.ends_with(".yaml")) {
424        return true;
425    }
426    
427    false
428}
429
430/// Parses a Dockerfile and extracts information
431fn parse_dockerfile(path: &PathBuf) -> Result<DockerfileInfo> {
432    let content = fs::read_to_string(path)?;
433    let lines: Vec<&str> = content.lines().collect();
434    
435    let mut info = DockerfileInfo {
436        path: path.clone(),
437        environment: extract_environment_from_filename(path),
438        base_image: None,
439        exposed_ports: Vec::new(),
440        workdir: None,
441        entrypoint: None,
442        env_vars: Vec::new(),
443        build_stages: Vec::new(),
444        is_multistage: false,
445        instruction_count: 0,
446    };
447    
448    // Regex patterns for Dockerfile instructions
449    let from_regex = Regex::new(r"(?i)^FROM\s+(.+?)(?:\s+AS\s+(.+))?$").unwrap();
450    let expose_regex = Regex::new(r"(?i)^EXPOSE\s+(.+)$").unwrap();
451    let workdir_regex = Regex::new(r"(?i)^WORKDIR\s+(.+)$").unwrap();
452    let cmd_regex = Regex::new(r"(?i)^CMD\s+(.+)$").unwrap();
453    let entrypoint_regex = Regex::new(r"(?i)^ENTRYPOINT\s+(.+)$").unwrap();
454    let env_regex = Regex::new(r"(?i)^ENV\s+(.+)$").unwrap();
455    
456    for line in lines {
457        let line = line.trim();
458        if line.is_empty() || line.starts_with('#') {
459            continue;
460        }
461        
462        info.instruction_count += 1;
463        
464        // Parse FROM instructions
465        if let Some(captures) = from_regex.captures(line) {
466            if info.base_image.is_none() {
467                info.base_image = Some(captures.get(1).unwrap().as_str().trim().to_string());
468            }
469            if let Some(stage_name) = captures.get(2) {
470                info.build_stages.push(stage_name.as_str().trim().to_string());
471                info.is_multistage = true;
472            }
473        }
474        
475        // Parse EXPOSE instructions
476        if let Some(captures) = expose_regex.captures(line) {
477            let ports_str = captures.get(1).unwrap().as_str();
478            for port in ports_str.split_whitespace() {
479                if let Ok(port_num) = port.parse::<u16>() {
480                    info.exposed_ports.push(port_num);
481                }
482            }
483        }
484        
485        // Parse WORKDIR
486        if let Some(captures) = workdir_regex.captures(line) {
487            info.workdir = Some(captures.get(1).unwrap().as_str().trim().to_string());
488        }
489        
490        // Parse CMD and ENTRYPOINT
491        if let Some(captures) = cmd_regex.captures(line) {
492            if info.entrypoint.is_none() {
493                info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
494            }
495        }
496        
497        if let Some(captures) = entrypoint_regex.captures(line) {
498            info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
499        }
500        
501        // Parse ENV
502        if let Some(captures) = env_regex.captures(line) {
503            info.env_vars.push(captures.get(1).unwrap().as_str().trim().to_string());
504        }
505    }
506    
507    Ok(info)
508}
509
510/// Parses a Docker Compose file and extracts information
511fn parse_compose_file(path: &PathBuf) -> Result<ComposeFileInfo> {
512    let content = fs::read_to_string(path)?;
513    
514    // Parse YAML content
515    let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
516        .map_err(|e| crate::error::AnalysisError::DependencyParsing {
517            file: path.display().to_string(),
518            reason: format!("YAML parsing error: {}", e),
519        })?;
520    
521    let mut info = ComposeFileInfo {
522        path: path.clone(),
523        environment: extract_environment_from_filename(path),
524        version: None,
525        service_names: Vec::new(),
526        networks: Vec::new(),
527        volumes: Vec::new(),
528        external_dependencies: Vec::new(),
529    };
530    
531    // Extract version
532    if let Some(version) = yaml_value.get("version").and_then(|v| v.as_str()) {
533        info.version = Some(version.to_string());
534    }
535    
536    // Extract service names
537    if let Some(services) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
538        for (service_name, _) in services {
539            if let Some(name) = service_name.as_str() {
540                info.service_names.push(name.to_string());
541            }
542        }
543    }
544    
545    // Extract networks
546    if let Some(networks) = yaml_value.get("networks").and_then(|n| n.as_mapping()) {
547        for (network_name, network_config) in networks {
548            if let Some(name) = network_name.as_str() {
549                info.networks.push(name.to_string());
550                
551                // Check if it's external
552                if let Some(config) = network_config.as_mapping() {
553                    if config.get("external").and_then(|e| e.as_bool()).unwrap_or(false) {
554                        info.external_dependencies.push(format!("network:{}", name));
555                    }
556                }
557            }
558        }
559    }
560    
561    // Extract volumes
562    if let Some(volumes) = yaml_value.get("volumes").and_then(|v| v.as_mapping()) {
563        for (volume_name, volume_config) in volumes {
564            if let Some(name) = volume_name.as_str() {
565                info.volumes.push(name.to_string());
566                
567                // Check if it's external
568                if let Some(config) = volume_config.as_mapping() {
569                    if config.get("external").and_then(|e| e.as_bool()).unwrap_or(false) {
570                        info.external_dependencies.push(format!("volume:{}", name));
571                    }
572                }
573            }
574        }
575    }
576    
577    Ok(info)
578}
579
580/// Extracts environment from filename (e.g., "dev" from "dockerfile.dev")
581fn extract_environment_from_filename(path: &PathBuf) -> Option<String> {
582    if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
583        let filename_lower = filename.to_lowercase();
584        
585        // Extract environment from patterns like "dockerfile.dev", "docker-compose.prod.yml"
586        if let Some(dot_pos) = filename_lower.rfind('.') {
587            let before_ext = &filename_lower[..dot_pos];
588            if let Some(env_dot_pos) = before_ext.rfind('.') {
589                let env = &before_ext[env_dot_pos + 1..];
590                
591                // Common environment names
592                match env {
593                    "dev" | "development" | "local" => return Some("development".to_string()),
594                    "prod" | "production" => return Some("production".to_string()),
595                    "test" | "testing" => return Some("test".to_string()),
596                    "stage" | "staging" => return Some("staging".to_string()),
597                    _ if env.len() <= 10 => return Some(env.to_string()), // Reasonable env name length
598                    _ => {}
599                }
600            }
601        }
602    }
603    None
604}
605
606/// Helper functions for parsing compose files
607fn extract_services_from_compose(compose_files: &[ComposeFileInfo]) -> Result<Vec<DockerService>> {
608    let mut services = Vec::new();
609    
610    for compose_file in compose_files {
611        let content = fs::read_to_string(&compose_file.path)?;
612        let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)
613            .map_err(|e| crate::error::AnalysisError::DependencyParsing {
614                file: compose_file.path.display().to_string(),
615                reason: format!("YAML parsing error: {}", e),
616            })?;
617        
618        if let Some(services_yaml) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
619            for (service_name, service_config) in services_yaml {
620                if let (Some(name), Some(config)) = (service_name.as_str(), service_config.as_mapping()) {
621                    let service = parse_docker_service(name, config, &compose_file.path)?;
622                    services.push(service);
623                }
624            }
625        }
626    }
627    
628    Ok(services)
629}
630
631/// Parses a Docker service from compose configuration
632fn parse_docker_service(
633    name: &str,
634    config: &serde_yaml::Mapping,
635    compose_file: &PathBuf,
636) -> Result<DockerService> {
637    let mut service = DockerService {
638        name: name.to_string(),
639        compose_file: compose_file.clone(),
640        image_or_build: ImageOrBuild::Image("unknown".to_string()),
641        ports: Vec::new(),
642        environment: HashMap::new(),
643        depends_on: Vec::new(),
644        networks: Vec::new(),
645        volumes: Vec::new(),
646        health_check: None,
647        restart_policy: None,
648        resource_limits: None,
649    };
650    
651    // Parse image or build
652    if let Some(image) = config.get("image").and_then(|i| i.as_str()) {
653        service.image_or_build = ImageOrBuild::Image(image.to_string());
654    } else if let Some(build_config) = config.get("build") {
655        if let Some(context) = build_config.as_str() {
656            service.image_or_build = ImageOrBuild::Build {
657                context: context.to_string(),
658                dockerfile: None,
659                args: HashMap::new(),
660            };
661        } else if let Some(build_mapping) = build_config.as_mapping() {
662            let context = build_mapping.get("context")
663                .and_then(|c| c.as_str())
664                .unwrap_or(".")
665                .to_string();
666            
667            let dockerfile = build_mapping.get("dockerfile")
668                .and_then(|d| d.as_str())
669                .map(|s| s.to_string());
670            
671            let mut args = HashMap::new();
672            if let Some(args_config) = build_mapping.get("args").and_then(|a| a.as_mapping()) {
673                for (key, value) in args_config {
674                    if let (Some(k), Some(v)) = (key.as_str(), value.as_str()) {
675                        args.insert(k.to_string(), v.to_string());
676                    }
677                }
678            }
679            
680            service.image_or_build = ImageOrBuild::Build {
681                context,
682                dockerfile,
683                args,
684            };
685        }
686    }
687    
688    // Parse ports
689    if let Some(ports_config) = config.get("ports").and_then(|p| p.as_sequence()) {
690        for port_item in ports_config {
691            if let Some(port_mapping) = parse_port_mapping(port_item) {
692                service.ports.push(port_mapping);
693            }
694        }
695    }
696    
697    // Parse environment variables
698    if let Some(env_config) = config.get("environment") {
699        parse_environment_variables(env_config, &mut service.environment);
700    }
701    
702    // Parse depends_on
703    if let Some(depends_config) = config.get("depends_on") {
704        if let Some(depends_sequence) = depends_config.as_sequence() {
705            for dep in depends_sequence {
706                if let Some(dep_name) = dep.as_str() {
707                    service.depends_on.push(dep_name.to_string());
708                }
709            }
710        } else if let Some(depends_mapping) = depends_config.as_mapping() {
711            for (dep_name, _) in depends_mapping {
712                if let Some(name) = dep_name.as_str() {
713                    service.depends_on.push(name.to_string());
714                }
715            }
716        }
717    }
718    
719    // Parse networks
720    if let Some(networks_config) = config.get("networks") {
721        if let Some(networks_sequence) = networks_config.as_sequence() {
722            for network in networks_sequence {
723                if let Some(network_name) = network.as_str() {
724                    service.networks.push(network_name.to_string());
725                }
726            }
727        } else if let Some(networks_mapping) = networks_config.as_mapping() {
728            for (network_name, _) in networks_mapping {
729                if let Some(name) = network_name.as_str() {
730                    service.networks.push(name.to_string());
731                }
732            }
733        }
734    }
735    
736    // Parse volumes
737    if let Some(volumes_config) = config.get("volumes").and_then(|v| v.as_sequence()) {
738        for volume_item in volumes_config {
739            if let Some(volume_mount) = parse_volume_mount(volume_item) {
740                service.volumes.push(volume_mount);
741            }
742        }
743    }
744    
745    // Parse restart policy
746    if let Some(restart) = config.get("restart").and_then(|r| r.as_str()) {
747        service.restart_policy = Some(restart.to_string());
748    }
749    
750    // Parse health check
751    if let Some(healthcheck_config) = config.get("healthcheck").and_then(|h| h.as_mapping()) {
752        if let Some(test) = healthcheck_config.get("test").and_then(|t| t.as_str()) {
753            service.health_check = Some(HealthCheck {
754                test: test.to_string(),
755                interval: healthcheck_config.get("interval").and_then(|i| i.as_str()).map(|s| s.to_string()),
756                timeout: healthcheck_config.get("timeout").and_then(|t| t.as_str()).map(|s| s.to_string()),
757                retries: healthcheck_config.get("retries").and_then(|r| r.as_u64()).map(|r| r as u32),
758            });
759        }
760    }
761    
762    Ok(service)
763}
764
765/// Parses port mapping from YAML value
766fn parse_port_mapping(port_value: &serde_yaml::Value) -> Option<PortMapping> {
767    if let Some(port_str) = port_value.as_str() {
768        // Handle string format like "8080:80" or "80"
769        if let Some(colon_pos) = port_str.find(':') {
770            let host_part = &port_str[..colon_pos];
771            let container_part = &port_str[colon_pos + 1..];
772            
773            if let (Ok(host_port), Ok(container_port)) = (host_part.parse::<u16>(), container_part.parse::<u16>()) {
774                return Some(PortMapping {
775                    host_port: Some(host_port),
776                    container_port,
777                    protocol: "tcp".to_string(),
778                    exposed_to_host: true,
779                });
780            }
781        } else if let Ok(container_port) = port_str.parse::<u16>() {
782            return Some(PortMapping {
783                host_port: None,
784                container_port,
785                protocol: "tcp".to_string(),
786                exposed_to_host: false,
787            });
788        }
789    } else if let Some(port_num) = port_value.as_u64() {
790        return Some(PortMapping {
791            host_port: None,
792            container_port: port_num as u16,
793            protocol: "tcp".to_string(),
794            exposed_to_host: false,
795        });
796    }
797    
798    None
799}
800
801/// Parses volume mount from YAML value
802fn parse_volume_mount(volume_value: &serde_yaml::Value) -> Option<VolumeMount> {
803    if let Some(volume_str) = volume_value.as_str() {
804        // Handle string format like "./data:/app/data:ro" or "./data:/app/data"
805        let parts: Vec<&str> = volume_str.split(':').collect();
806        if parts.len() >= 2 {
807            return Some(VolumeMount {
808                source: parts[0].to_string(),
809                target: parts[1].to_string(),
810                mount_type: if parts[0].starts_with('/') || parts[0].starts_with('.') {
811                    "bind".to_string()
812                } else {
813                    "volume".to_string()
814                },
815                read_only: parts.get(2).map_or(false, |&opt| opt == "ro"),
816            });
817        }
818    }
819    None
820}
821
822/// Parses environment variables from YAML
823fn parse_environment_variables(env_value: &serde_yaml::Value, env_map: &mut HashMap<String, String>) {
824    if let Some(env_mapping) = env_value.as_mapping() {
825        for (key, value) in env_mapping {
826            if let Some(key_str) = key.as_str() {
827                let value_str = value.as_str().unwrap_or("").to_string();
828                env_map.insert(key_str.to_string(), value_str);
829            }
830        }
831    } else if let Some(env_sequence) = env_value.as_sequence() {
832        for env_item in env_sequence {
833            if let Some(env_str) = env_item.as_str() {
834                if let Some(eq_pos) = env_str.find('=') {
835                    let key = env_str[..eq_pos].to_string();
836                    let value = env_str[eq_pos + 1..].to_string();
837                    env_map.insert(key, value);
838                } else {
839                    env_map.insert(env_str.to_string(), String::new());
840                }
841            }
842        }
843    }
844}
845
846fn analyze_networking(
847    services: &[DockerService],
848    compose_files: &[ComposeFileInfo],
849) -> Result<NetworkingConfig> {
850    let mut custom_networks = Vec::new();
851    let mut connected_services: HashMap<String, Vec<String>> = HashMap::new();
852    
853    // Collect networks from compose files
854    for compose_file in compose_files {
855        for network_name in &compose_file.networks {
856            let network_info = NetworkInfo {
857                name: network_name.clone(),
858                driver: None, // TODO: Parse driver from compose file
859                external: compose_file.external_dependencies.contains(&format!("network:{}", network_name)),
860                connected_services: Vec::new(),
861            };
862            custom_networks.push(network_info);
863        }
864    }
865    
866    // Map services to networks
867    for service in services {
868        for network in &service.networks {
869            connected_services
870                .entry(network.clone())
871                .or_insert_with(Vec::new)
872                .push(service.name.clone());
873        }
874    }
875    
876    // Update network info with connected services
877    for network in &mut custom_networks {
878        if let Some(services) = connected_services.get(&network.name) {
879            network.connected_services = services.clone();
880        }
881    }
882    
883    // Analyze service discovery
884    let service_discovery = ServiceDiscoveryConfig {
885        internal_dns: !services.is_empty(), // Docker Compose provides internal DNS
886        external_tools: detect_service_discovery_tools(services),
887        service_mesh: detect_service_mesh(services),
888    };
889    
890    // Analyze load balancing
891    let load_balancing = detect_load_balancers(services);
892    
893    // Analyze external connectivity
894    let external_connectivity = analyze_external_connectivity(services);
895    
896    Ok(NetworkingConfig {
897        custom_networks,
898        service_discovery,
899        load_balancing,
900        external_connectivity,
901    })
902}
903
904fn determine_orchestration_pattern(
905    services: &[DockerService],
906    networking: &NetworkingConfig,
907) -> OrchestrationPattern {
908    if services.is_empty() {
909        return OrchestrationPattern::SingleContainer;
910    }
911    
912    if services.len() == 1 {
913        return OrchestrationPattern::SingleContainer;
914    }
915    
916    // Check for microservices patterns
917    let has_multiple_backends = services.iter()
918        .filter(|s| match &s.image_or_build {
919            ImageOrBuild::Image(img) => !img.contains("nginx") && !img.contains("proxy") && !img.contains("traefik"),
920            _ => true,
921        })
922        .count() > 2;
923    
924    let has_service_discovery = networking.service_discovery.internal_dns || 
925                               !networking.service_discovery.external_tools.is_empty();
926    
927    let has_load_balancing = !networking.load_balancing.is_empty();
928    
929    let has_message_queues = services.iter().any(|s| match &s.image_or_build {
930        ImageOrBuild::Image(img) => {
931            img.contains("redis") || img.contains("rabbitmq") || 
932            img.contains("kafka") || img.contains("nats")
933        },
934        _ => false,
935    });
936    
937    if networking.service_discovery.service_mesh {
938        OrchestrationPattern::ServiceMesh
939    } else if has_message_queues && has_multiple_backends {
940        OrchestrationPattern::EventDriven
941    } else if has_multiple_backends && has_service_discovery {
942        OrchestrationPattern::Microservices
943    } else if has_load_balancing || services.len() > 3 {
944        OrchestrationPattern::DockerCompose
945    } else {
946        OrchestrationPattern::DockerCompose
947    }
948}
949
950/// Detects service discovery tools in the services
951fn detect_service_discovery_tools(services: &[DockerService]) -> Vec<String> {
952    let mut tools = Vec::new();
953    
954    for service in services {
955        if let ImageOrBuild::Image(image) = &service.image_or_build {
956            if image.contains("consul") {
957                tools.push("consul".to_string());
958            }
959            if image.contains("etcd") {
960                tools.push("etcd".to_string());
961            }
962            if image.contains("zookeeper") {
963                tools.push("zookeeper".to_string());
964            }
965        }
966    }
967    
968    tools.sort();
969    tools.dedup();
970    tools
971}
972
973/// Detects service mesh presence
974fn detect_service_mesh(services: &[DockerService]) -> bool {
975    services.iter().any(|s| {
976        if let ImageOrBuild::Image(image) = &s.image_or_build {
977            image.contains("istio") || image.contains("linkerd") || 
978            image.contains("envoy") || image.contains("consul-connect")
979        } else {
980            false
981        }
982    })
983}
984
985/// Detects load balancers in the services
986fn detect_load_balancers(services: &[DockerService]) -> Vec<LoadBalancerConfig> {
987    let mut load_balancers = Vec::new();
988    
989    for service in services {
990        // Check if service image indicates a load balancer
991        let is_load_balancer = match &service.image_or_build {
992            ImageOrBuild::Image(image) => {
993                image.contains("nginx") || 
994                image.contains("traefik") || 
995                image.contains("haproxy") ||
996                image.contains("envoy") ||
997                image.contains("kong")
998            },
999            _ => false,
1000        };
1001        
1002        if is_load_balancer {
1003            // Find potential backend services (services this one doesn't depend on)
1004            let backends: Vec<String> = services
1005                .iter()
1006                .filter(|s| s.name != service.name && !service.depends_on.contains(&s.name))
1007                .map(|s| s.name.clone())
1008                .collect();
1009            
1010            if !backends.is_empty() {
1011                let lb_type = match &service.image_or_build {
1012                    ImageOrBuild::Image(image) => {
1013                        if image.contains("nginx") { "nginx" }
1014                        else if image.contains("traefik") { "traefik" }
1015                        else if image.contains("haproxy") { "haproxy" }
1016                        else if image.contains("envoy") { "envoy" }
1017                        else if image.contains("kong") { "kong" }
1018                        else { "unknown" }
1019                    },
1020                    _ => "unknown",
1021                };
1022                
1023                load_balancers.push(LoadBalancerConfig {
1024                    service: service.name.clone(),
1025                    lb_type: lb_type.to_string(),
1026                    backends,
1027                });
1028            }
1029        }
1030    }
1031    
1032    load_balancers
1033}
1034
1035/// Analyzes external connectivity patterns
1036fn analyze_external_connectivity(services: &[DockerService]) -> ExternalConnectivity {
1037    let mut exposed_services = Vec::new();
1038    let mut ingress_patterns = Vec::new();
1039    let mut api_gateways = Vec::new();
1040    
1041    for service in services {
1042        let mut external_ports = Vec::new();
1043        let mut protocols = Vec::new();
1044        
1045        // Check for exposed ports
1046        for port in &service.ports {
1047            if port.exposed_to_host {
1048                if let Some(host_port) = port.host_port {
1049                    external_ports.push(host_port);
1050                }
1051                protocols.push(port.protocol.clone());
1052            }
1053        }
1054        
1055        if !external_ports.is_empty() {
1056            // Check for SSL/TLS indicators
1057            let ssl_enabled = external_ports.contains(&443) || 
1058                            external_ports.contains(&8443) ||
1059                            service.environment.keys().any(|k| k.to_lowercase().contains("ssl") || k.to_lowercase().contains("tls"));
1060            
1061            exposed_services.push(ExposedService {
1062                service: service.name.clone(),
1063                external_ports,
1064                protocols: protocols.into_iter().collect::<std::collections::HashSet<_>>().into_iter().collect(),
1065                ssl_enabled,
1066            });
1067        }
1068        
1069        // Detect API gateways
1070        if service.name.to_lowercase().contains("gateway") || 
1071           service.name.to_lowercase().contains("api") ||
1072           service.name.to_lowercase().contains("proxy") {
1073            api_gateways.push(service.name.clone());
1074        }
1075        
1076        // Also check image for API gateway patterns
1077        if let ImageOrBuild::Image(image) = &service.image_or_build {
1078            if image.contains("kong") || image.contains("zuul") || 
1079               image.contains("ambassador") || image.contains("traefik") {
1080                if !api_gateways.contains(&service.name) {
1081                    api_gateways.push(service.name.clone());
1082                }
1083            }
1084        }
1085    }
1086    
1087    // Detect ingress patterns
1088    if exposed_services.len() == 1 && api_gateways.len() == 1 {
1089        ingress_patterns.push("Single API Gateway".to_string());
1090    } else if exposed_services.len() > 1 && api_gateways.is_empty() {
1091        ingress_patterns.push("Multiple Direct Entry Points".to_string());
1092    } else if !api_gateways.is_empty() {
1093        ingress_patterns.push("API Gateway Pattern".to_string());
1094    }
1095    
1096    // Detect reverse proxy patterns
1097    let has_reverse_proxy = services.iter().any(|s| {
1098        if let ImageOrBuild::Image(image) = &s.image_or_build {
1099            image.contains("nginx") || image.contains("apache") || image.contains("caddy")
1100        } else {
1101            false
1102        }
1103    });
1104    
1105    if has_reverse_proxy {
1106        ingress_patterns.push("Reverse Proxy".to_string());
1107    }
1108    
1109    ExternalConnectivity {
1110        exposed_services,
1111        ingress_patterns,
1112        api_gateways,
1113    }
1114}
1115
1116fn analyze_environments(
1117    dockerfiles: &[DockerfileInfo],
1118    compose_files: &[ComposeFileInfo],
1119) -> Vec<DockerEnvironment> {
1120    let mut environments: HashMap<String, DockerEnvironment> = HashMap::new();
1121    
1122    // Collect environments from Dockerfiles
1123    for dockerfile in dockerfiles {
1124        let env_name = dockerfile.environment.clone().unwrap_or_else(|| "default".to_string());
1125        environments
1126            .entry(env_name.clone())
1127            .or_insert_with(|| DockerEnvironment {
1128                name: env_name,
1129                dockerfiles: Vec::new(),
1130                compose_files: Vec::new(),
1131                config_overrides: HashMap::new(),
1132            })
1133            .dockerfiles
1134            .push(dockerfile.path.clone());
1135    }
1136    
1137    // Collect environments from Compose files
1138    for compose_file in compose_files {
1139        let env_name = compose_file.environment.clone().unwrap_or_else(|| "default".to_string());
1140        environments
1141            .entry(env_name.clone())
1142            .or_insert_with(|| DockerEnvironment {
1143                name: env_name,
1144                dockerfiles: Vec::new(),
1145                compose_files: Vec::new(),
1146                config_overrides: HashMap::new(),
1147            })
1148            .compose_files
1149            .push(compose_file.path.clone());
1150    }
1151    
1152    environments.into_values().collect()
1153}
1154
1155#[cfg(test)]
1156mod tests {
1157    use super::*;
1158    
1159    #[test]
1160    fn test_is_dockerfile_name() {
1161        assert!(is_dockerfile_name("Dockerfile"));
1162        assert!(is_dockerfile_name("dockerfile"));
1163        assert!(is_dockerfile_name("Dockerfile.dev"));
1164        assert!(is_dockerfile_name("dockerfile.prod"));
1165        assert!(is_dockerfile_name("api.dockerfile"));
1166        assert!(!is_dockerfile_name("README.md"));
1167        assert!(!is_dockerfile_name("package.json"));
1168    }
1169    
1170    #[test]
1171    fn test_is_compose_file_name() {
1172        assert!(is_compose_file_name("docker-compose.yml"));
1173        assert!(is_compose_file_name("docker-compose.yaml"));
1174        assert!(is_compose_file_name("docker-compose.dev.yml"));
1175        assert!(is_compose_file_name("docker-compose.prod.yaml"));
1176        assert!(is_compose_file_name("compose.yml"));
1177        assert!(is_compose_file_name("compose.yaml"));
1178        assert!(!is_compose_file_name("README.md"));
1179        assert!(!is_compose_file_name("package.json"));
1180    }
1181    
1182    #[test]
1183    fn test_extract_environment_from_filename() {
1184        assert_eq!(
1185            extract_environment_from_filename(&PathBuf::from("Dockerfile.dev")),
1186            Some("development".to_string())
1187        );
1188        assert_eq!(
1189            extract_environment_from_filename(&PathBuf::from("docker-compose.prod.yml")),
1190            Some("production".to_string())
1191        );
1192        assert_eq!(
1193            extract_environment_from_filename(&PathBuf::from("Dockerfile")),
1194            None
1195        );
1196    }
1197}