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().map_or(false, |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                if is_dockerfile_name(filename) {
342                    dockerfiles.push(path);
343                }
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().map_or(false, |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                if is_compose_file_name(filename) {
394                    compose_files.push(path);
395                }
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            if info.entrypoint.is_none() {
505                info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
506            }
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                    if 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
579    // Extract volumes
580    if let Some(volumes) = yaml_value.get("volumes").and_then(|v| v.as_mapping()) {
581        for (volume_name, volume_config) in volumes {
582            if let Some(name) = volume_name.as_str() {
583                info.volumes.push(name.to_string());
584
585                // Check if it's external
586                if let Some(config) = volume_config.as_mapping() {
587                    if config
588                        .get("external")
589                        .and_then(|e| e.as_bool())
590                        .unwrap_or(false)
591                    {
592                        info.external_dependencies.push(format!("volume:{}", name));
593                    }
594                }
595            }
596        }
597    }
598
599    Ok(info)
600}
601
602/// Extracts environment from filename (e.g., "dev" from "dockerfile.dev")
603fn extract_environment_from_filename(path: &PathBuf) -> Option<String> {
604    if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
605        let filename_lower = filename.to_lowercase();
606
607        // Helper to map env shorthand to full name
608        let map_env = |env: &str| -> Option<String> {
609            match env {
610                "dev" | "development" | "local" => Some("development".to_string()),
611                "prod" | "production" => Some("production".to_string()),
612                "test" | "testing" => Some("test".to_string()),
613                "stage" | "staging" => Some("staging".to_string()),
614                _ if env.len() <= 10 && !env.is_empty() => Some(env.to_string()),
615                _ => None,
616            }
617        };
618
619        // Handle patterns like "docker-compose.prod.yml" (env between two dots)
620        if let Some(last_dot) = filename_lower.rfind('.') {
621            let before_ext = &filename_lower[..last_dot];
622            if let Some(env_dot_pos) = before_ext.rfind('.') {
623                let env = &before_ext[env_dot_pos + 1..];
624                if let Some(result) = map_env(env) {
625                    return Some(result);
626                }
627            }
628        }
629
630        // Handle patterns like "Dockerfile.dev" (env is the extension itself)
631        if let Some(dot_pos) = filename_lower.rfind('.') {
632            let ext = &filename_lower[dot_pos + 1..];
633            // Only if the base is dockerfile/docker-compose related
634            let base = &filename_lower[..dot_pos];
635            if base.contains("dockerfile") || base.contains("docker-compose") || base == "compose" {
636                if let Some(result) = map_env(ext) {
637                    return Some(result);
638                }
639            }
640        }
641    }
642    None
643}
644
645/// Helper functions for parsing compose files
646fn extract_services_from_compose(compose_files: &[ComposeFileInfo]) -> Result<Vec<DockerService>> {
647    let mut services = Vec::new();
648
649    for compose_file in compose_files {
650        let content = fs::read_to_string(&compose_file.path)?;
651        let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| {
652            crate::error::AnalysisError::DependencyParsing {
653                file: compose_file.path.display().to_string(),
654                reason: format!("YAML parsing error: {}", e),
655            }
656        })?;
657
658        if let Some(services_yaml) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
659            for (service_name, service_config) in services_yaml {
660                if let (Some(name), Some(config)) =
661                    (service_name.as_str(), service_config.as_mapping())
662                {
663                    let service = parse_docker_service(name, config, &compose_file.path)?;
664                    services.push(service);
665                }
666            }
667        }
668    }
669
670    Ok(services)
671}
672
673/// Parses a Docker service from compose configuration
674fn parse_docker_service(
675    name: &str,
676    config: &serde_yaml::Mapping,
677    compose_file: &PathBuf,
678) -> Result<DockerService> {
679    let mut service = DockerService {
680        name: name.to_string(),
681        compose_file: compose_file.clone(),
682        image_or_build: ImageOrBuild::Image("unknown".to_string()),
683        ports: Vec::new(),
684        environment: HashMap::new(),
685        depends_on: Vec::new(),
686        networks: Vec::new(),
687        volumes: Vec::new(),
688        health_check: None,
689        restart_policy: None,
690        resource_limits: None,
691    };
692
693    // Parse image or build
694    if let Some(image) = config.get("image").and_then(|i| i.as_str()) {
695        service.image_or_build = ImageOrBuild::Image(image.to_string());
696    } else if let Some(build_config) = config.get("build") {
697        if let Some(context) = build_config.as_str() {
698            service.image_or_build = ImageOrBuild::Build {
699                context: context.to_string(),
700                dockerfile: None,
701                args: HashMap::new(),
702            };
703        } else if let Some(build_mapping) = build_config.as_mapping() {
704            let context = build_mapping
705                .get("context")
706                .and_then(|c| c.as_str())
707                .unwrap_or(".")
708                .to_string();
709
710            let dockerfile = build_mapping
711                .get("dockerfile")
712                .and_then(|d| d.as_str())
713                .map(|s| s.to_string());
714
715            let mut args = HashMap::new();
716            if let Some(args_config) = build_mapping.get("args").and_then(|a| a.as_mapping()) {
717                for (key, value) in args_config {
718                    if let (Some(k), Some(v)) = (key.as_str(), value.as_str()) {
719                        args.insert(k.to_string(), v.to_string());
720                    }
721                }
722            }
723
724            service.image_or_build = ImageOrBuild::Build {
725                context,
726                dockerfile,
727                args,
728            };
729        }
730    }
731
732    // Parse ports
733    if let Some(ports_config) = config.get("ports").and_then(|p| p.as_sequence()) {
734        for port_item in ports_config {
735            if let Some(port_mapping) = parse_port_mapping(port_item) {
736                service.ports.push(port_mapping);
737            }
738        }
739    }
740
741    // Parse environment variables
742    if let Some(env_config) = config.get("environment") {
743        parse_environment_variables(env_config, &mut service.environment);
744    }
745
746    // Parse depends_on
747    if let Some(depends_config) = config.get("depends_on") {
748        if let Some(depends_sequence) = depends_config.as_sequence() {
749            for dep in depends_sequence {
750                if let Some(dep_name) = dep.as_str() {
751                    service.depends_on.push(dep_name.to_string());
752                }
753            }
754        } else if let Some(depends_mapping) = depends_config.as_mapping() {
755            for (dep_name, _) in depends_mapping {
756                if let Some(name) = dep_name.as_str() {
757                    service.depends_on.push(name.to_string());
758                }
759            }
760        }
761    }
762
763    // Parse networks
764    if let Some(networks_config) = config.get("networks") {
765        if let Some(networks_sequence) = networks_config.as_sequence() {
766            for network in networks_sequence {
767                if let Some(network_name) = network.as_str() {
768                    service.networks.push(network_name.to_string());
769                }
770            }
771        } else if let Some(networks_mapping) = networks_config.as_mapping() {
772            for (network_name, _) in networks_mapping {
773                if let Some(name) = network_name.as_str() {
774                    service.networks.push(name.to_string());
775                }
776            }
777        }
778    }
779
780    // Parse volumes
781    if let Some(volumes_config) = config.get("volumes").and_then(|v| v.as_sequence()) {
782        for volume_item in volumes_config {
783            if let Some(volume_mount) = parse_volume_mount(volume_item) {
784                service.volumes.push(volume_mount);
785            }
786        }
787    }
788
789    // Parse restart policy
790    if let Some(restart) = config.get("restart").and_then(|r| r.as_str()) {
791        service.restart_policy = Some(restart.to_string());
792    }
793
794    // Parse health check
795    if let Some(healthcheck_config) = config.get("healthcheck").and_then(|h| h.as_mapping()) {
796        if let Some(test) = healthcheck_config.get("test").and_then(|t| t.as_str()) {
797            service.health_check = Some(HealthCheck {
798                test: test.to_string(),
799                interval: healthcheck_config
800                    .get("interval")
801                    .and_then(|i| i.as_str())
802                    .map(|s| s.to_string()),
803                timeout: healthcheck_config
804                    .get("timeout")
805                    .and_then(|t| t.as_str())
806                    .map(|s| s.to_string()),
807                retries: healthcheck_config
808                    .get("retries")
809                    .and_then(|r| r.as_u64())
810                    .map(|r| r as u32),
811            });
812        }
813    }
814
815    Ok(service)
816}
817
818/// Parses port mapping from YAML value
819fn parse_port_mapping(port_value: &serde_yaml::Value) -> Option<PortMapping> {
820    if let Some(port_str) = port_value.as_str() {
821        // Handle string format like "8080:80" or "80"
822        if let Some(colon_pos) = port_str.find(':') {
823            let host_part = &port_str[..colon_pos];
824            let container_part = &port_str[colon_pos + 1..];
825
826            if let (Ok(host_port), Ok(container_port)) =
827                (host_part.parse::<u16>(), container_part.parse::<u16>())
828            {
829                return Some(PortMapping {
830                    host_port: Some(host_port),
831                    container_port,
832                    protocol: "tcp".to_string(),
833                    exposed_to_host: true,
834                });
835            }
836        } else if let Ok(container_port) = port_str.parse::<u16>() {
837            return Some(PortMapping {
838                host_port: None,
839                container_port,
840                protocol: "tcp".to_string(),
841                exposed_to_host: false,
842            });
843        }
844    } else if let Some(port_num) = port_value.as_u64() {
845        return Some(PortMapping {
846            host_port: None,
847            container_port: port_num as u16,
848            protocol: "tcp".to_string(),
849            exposed_to_host: false,
850        });
851    }
852
853    None
854}
855
856/// Parses volume mount from YAML value
857fn parse_volume_mount(volume_value: &serde_yaml::Value) -> Option<VolumeMount> {
858    if let Some(volume_str) = volume_value.as_str() {
859        // Handle string format like "./data:/app/data:ro" or "./data:/app/data"
860        let parts: Vec<&str> = volume_str.split(':').collect();
861        if parts.len() >= 2 {
862            return Some(VolumeMount {
863                source: parts[0].to_string(),
864                target: parts[1].to_string(),
865                mount_type: if parts[0].starts_with('/') || parts[0].starts_with('.') {
866                    "bind".to_string()
867                } else {
868                    "volume".to_string()
869                },
870                read_only: parts.get(2).map_or(false, |&opt| opt == "ro"),
871            });
872        }
873    }
874    None
875}
876
877/// Parses environment variables from YAML
878fn parse_environment_variables(
879    env_value: &serde_yaml::Value,
880    env_map: &mut HashMap<String, String>,
881) {
882    if let Some(env_mapping) = env_value.as_mapping() {
883        for (key, value) in env_mapping {
884            if let Some(key_str) = key.as_str() {
885                let value_str = value.as_str().unwrap_or("").to_string();
886                env_map.insert(key_str.to_string(), value_str);
887            }
888        }
889    } else if let Some(env_sequence) = env_value.as_sequence() {
890        for env_item in env_sequence {
891            if let Some(env_str) = env_item.as_str() {
892                if let Some(eq_pos) = env_str.find('=') {
893                    let key = env_str[..eq_pos].to_string();
894                    let value = env_str[eq_pos + 1..].to_string();
895                    env_map.insert(key, value);
896                } else {
897                    env_map.insert(env_str.to_string(), String::new());
898                }
899            }
900        }
901    }
902}
903
904fn analyze_networking(
905    services: &[DockerService],
906    compose_files: &[ComposeFileInfo],
907) -> Result<NetworkingConfig> {
908    let mut custom_networks = Vec::new();
909    let mut connected_services: HashMap<String, Vec<String>> = HashMap::new();
910
911    // Collect networks from compose files
912    for compose_file in compose_files {
913        for network_name in &compose_file.networks {
914            let network_info = NetworkInfo {
915                name: network_name.clone(),
916                driver: None, // TODO: Parse driver from compose file
917                external: compose_file
918                    .external_dependencies
919                    .contains(&format!("network:{}", network_name)),
920                connected_services: Vec::new(),
921            };
922            custom_networks.push(network_info);
923        }
924    }
925
926    // Map services to networks
927    for service in services {
928        for network in &service.networks {
929            connected_services
930                .entry(network.clone())
931                .or_insert_with(Vec::new)
932                .push(service.name.clone());
933        }
934    }
935
936    // Update network info with connected services
937    for network in &mut custom_networks {
938        if let Some(services) = connected_services.get(&network.name) {
939            network.connected_services = services.clone();
940        }
941    }
942
943    // Analyze service discovery
944    let service_discovery = ServiceDiscoveryConfig {
945        internal_dns: !services.is_empty(), // Docker Compose provides internal DNS
946        external_tools: detect_service_discovery_tools(services),
947        service_mesh: detect_service_mesh(services),
948    };
949
950    // Analyze load balancing
951    let load_balancing = detect_load_balancers(services);
952
953    // Analyze external connectivity
954    let external_connectivity = analyze_external_connectivity(services);
955
956    Ok(NetworkingConfig {
957        custom_networks,
958        service_discovery,
959        load_balancing,
960        external_connectivity,
961    })
962}
963
964fn determine_orchestration_pattern(
965    services: &[DockerService],
966    networking: &NetworkingConfig,
967) -> OrchestrationPattern {
968    if services.is_empty() {
969        return OrchestrationPattern::SingleContainer;
970    }
971
972    if services.len() == 1 {
973        return OrchestrationPattern::SingleContainer;
974    }
975
976    // Check for microservices patterns
977    let has_multiple_backends = services
978        .iter()
979        .filter(|s| match &s.image_or_build {
980            ImageOrBuild::Image(img) => {
981                !img.contains("nginx") && !img.contains("proxy") && !img.contains("traefik")
982            }
983            _ => true,
984        })
985        .count()
986        > 2;
987
988    let has_service_discovery = networking.service_discovery.internal_dns
989        || !networking.service_discovery.external_tools.is_empty();
990
991    let has_load_balancing = !networking.load_balancing.is_empty();
992
993    let has_message_queues = services.iter().any(|s| match &s.image_or_build {
994        ImageOrBuild::Image(img) => {
995            img.contains("redis")
996                || img.contains("rabbitmq")
997                || img.contains("kafka")
998                || img.contains("nats")
999        }
1000        _ => false,
1001    });
1002
1003    if networking.service_discovery.service_mesh {
1004        OrchestrationPattern::ServiceMesh
1005    } else if has_message_queues && has_multiple_backends {
1006        OrchestrationPattern::EventDriven
1007    } else if has_multiple_backends && has_service_discovery {
1008        OrchestrationPattern::Microservices
1009    } else if has_load_balancing || services.len() > 3 {
1010        OrchestrationPattern::DockerCompose
1011    } else {
1012        OrchestrationPattern::DockerCompose
1013    }
1014}
1015
1016/// Detects service discovery tools in the services
1017fn detect_service_discovery_tools(services: &[DockerService]) -> Vec<String> {
1018    let mut tools = Vec::new();
1019
1020    for service in services {
1021        if let ImageOrBuild::Image(image) = &service.image_or_build {
1022            if image.contains("consul") {
1023                tools.push("consul".to_string());
1024            }
1025            if image.contains("etcd") {
1026                tools.push("etcd".to_string());
1027            }
1028            if image.contains("zookeeper") {
1029                tools.push("zookeeper".to_string());
1030            }
1031        }
1032    }
1033
1034    tools.sort();
1035    tools.dedup();
1036    tools
1037}
1038
1039/// Detects service mesh presence
1040fn detect_service_mesh(services: &[DockerService]) -> bool {
1041    services.iter().any(|s| {
1042        if let ImageOrBuild::Image(image) = &s.image_or_build {
1043            image.contains("istio")
1044                || image.contains("linkerd")
1045                || image.contains("envoy")
1046                || image.contains("consul-connect")
1047        } else {
1048            false
1049        }
1050    })
1051}
1052
1053/// Detects load balancers in the services
1054fn detect_load_balancers(services: &[DockerService]) -> Vec<LoadBalancerConfig> {
1055    let mut load_balancers = Vec::new();
1056
1057    for service in services {
1058        // Check if service image indicates a load balancer
1059        let is_load_balancer = match &service.image_or_build {
1060            ImageOrBuild::Image(image) => {
1061                image.contains("nginx")
1062                    || image.contains("traefik")
1063                    || image.contains("haproxy")
1064                    || image.contains("envoy")
1065                    || image.contains("kong")
1066            }
1067            _ => false,
1068        };
1069
1070        if is_load_balancer {
1071            // Find potential backend services (services this one doesn't depend on)
1072            let backends: Vec<String> = services
1073                .iter()
1074                .filter(|s| s.name != service.name && !service.depends_on.contains(&s.name))
1075                .map(|s| s.name.clone())
1076                .collect();
1077
1078            if !backends.is_empty() {
1079                let lb_type = match &service.image_or_build {
1080                    ImageOrBuild::Image(image) => {
1081                        if image.contains("nginx") {
1082                            "nginx"
1083                        } else if image.contains("traefik") {
1084                            "traefik"
1085                        } else if image.contains("haproxy") {
1086                            "haproxy"
1087                        } else if image.contains("envoy") {
1088                            "envoy"
1089                        } else if image.contains("kong") {
1090                            "kong"
1091                        } else {
1092                            "unknown"
1093                        }
1094                    }
1095                    _ => "unknown",
1096                };
1097
1098                load_balancers.push(LoadBalancerConfig {
1099                    service: service.name.clone(),
1100                    lb_type: lb_type.to_string(),
1101                    backends,
1102                });
1103            }
1104        }
1105    }
1106
1107    load_balancers
1108}
1109
1110/// Analyzes external connectivity patterns
1111fn analyze_external_connectivity(services: &[DockerService]) -> ExternalConnectivity {
1112    let mut exposed_services = Vec::new();
1113    let mut ingress_patterns = Vec::new();
1114    let mut api_gateways = Vec::new();
1115
1116    for service in services {
1117        let mut external_ports = Vec::new();
1118        let mut protocols = Vec::new();
1119
1120        // Check for exposed ports
1121        for port in &service.ports {
1122            if port.exposed_to_host {
1123                if let Some(host_port) = port.host_port {
1124                    external_ports.push(host_port);
1125                }
1126                protocols.push(port.protocol.clone());
1127            }
1128        }
1129
1130        if !external_ports.is_empty() {
1131            // Check for SSL/TLS indicators
1132            let ssl_enabled = external_ports.contains(&443)
1133                || external_ports.contains(&8443)
1134                || service
1135                    .environment
1136                    .keys()
1137                    .any(|k| k.to_lowercase().contains("ssl") || k.to_lowercase().contains("tls"));
1138
1139            exposed_services.push(ExposedService {
1140                service: service.name.clone(),
1141                external_ports,
1142                protocols: protocols
1143                    .into_iter()
1144                    .collect::<std::collections::HashSet<_>>()
1145                    .into_iter()
1146                    .collect(),
1147                ssl_enabled,
1148            });
1149        }
1150
1151        // Detect API gateways
1152        if service.name.to_lowercase().contains("gateway")
1153            || service.name.to_lowercase().contains("api")
1154            || service.name.to_lowercase().contains("proxy")
1155        {
1156            api_gateways.push(service.name.clone());
1157        }
1158
1159        // Also check image for API gateway patterns
1160        if let ImageOrBuild::Image(image) = &service.image_or_build {
1161            if image.contains("kong")
1162                || image.contains("zuul")
1163                || image.contains("ambassador")
1164                || image.contains("traefik")
1165            {
1166                if !api_gateways.contains(&service.name) {
1167                    api_gateways.push(service.name.clone());
1168                }
1169            }
1170        }
1171    }
1172
1173    // Detect ingress patterns
1174    if exposed_services.len() == 1 && api_gateways.len() == 1 {
1175        ingress_patterns.push("Single API Gateway".to_string());
1176    } else if exposed_services.len() > 1 && api_gateways.is_empty() {
1177        ingress_patterns.push("Multiple Direct Entry Points".to_string());
1178    } else if !api_gateways.is_empty() {
1179        ingress_patterns.push("API Gateway Pattern".to_string());
1180    }
1181
1182    // Detect reverse proxy patterns
1183    let has_reverse_proxy = services.iter().any(|s| {
1184        if let ImageOrBuild::Image(image) = &s.image_or_build {
1185            image.contains("nginx") || image.contains("apache") || image.contains("caddy")
1186        } else {
1187            false
1188        }
1189    });
1190
1191    if has_reverse_proxy {
1192        ingress_patterns.push("Reverse Proxy".to_string());
1193    }
1194
1195    ExternalConnectivity {
1196        exposed_services,
1197        ingress_patterns,
1198        api_gateways,
1199    }
1200}
1201
1202fn analyze_environments(
1203    dockerfiles: &[DockerfileInfo],
1204    compose_files: &[ComposeFileInfo],
1205) -> Vec<DockerEnvironment> {
1206    let mut environments: HashMap<String, DockerEnvironment> = HashMap::new();
1207
1208    // Collect environments from Dockerfiles
1209    for dockerfile in dockerfiles {
1210        let env_name = dockerfile
1211            .environment
1212            .clone()
1213            .unwrap_or_else(|| "default".to_string());
1214        environments
1215            .entry(env_name.clone())
1216            .or_insert_with(|| DockerEnvironment {
1217                name: env_name,
1218                dockerfiles: Vec::new(),
1219                compose_files: Vec::new(),
1220                config_overrides: HashMap::new(),
1221            })
1222            .dockerfiles
1223            .push(dockerfile.path.clone());
1224    }
1225
1226    // Collect environments from Compose files
1227    for compose_file in compose_files {
1228        let env_name = compose_file
1229            .environment
1230            .clone()
1231            .unwrap_or_else(|| "default".to_string());
1232        environments
1233            .entry(env_name.clone())
1234            .or_insert_with(|| DockerEnvironment {
1235                name: env_name,
1236                dockerfiles: Vec::new(),
1237                compose_files: Vec::new(),
1238                config_overrides: HashMap::new(),
1239            })
1240            .compose_files
1241            .push(compose_file.path.clone());
1242    }
1243
1244    environments.into_values().collect()
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use super::*;
1250
1251    #[test]
1252    fn test_is_dockerfile_name() {
1253        assert!(is_dockerfile_name("Dockerfile"));
1254        assert!(is_dockerfile_name("dockerfile"));
1255        assert!(is_dockerfile_name("Dockerfile.dev"));
1256        assert!(is_dockerfile_name("dockerfile.prod"));
1257        assert!(is_dockerfile_name("api.dockerfile"));
1258        assert!(!is_dockerfile_name("README.md"));
1259        assert!(!is_dockerfile_name("package.json"));
1260    }
1261
1262    #[test]
1263    fn test_is_compose_file_name() {
1264        assert!(is_compose_file_name("docker-compose.yml"));
1265        assert!(is_compose_file_name("docker-compose.yaml"));
1266        assert!(is_compose_file_name("docker-compose.dev.yml"));
1267        assert!(is_compose_file_name("docker-compose.prod.yaml"));
1268        assert!(is_compose_file_name("compose.yml"));
1269        assert!(is_compose_file_name("compose.yaml"));
1270        assert!(!is_compose_file_name("README.md"));
1271        assert!(!is_compose_file_name("package.json"));
1272    }
1273
1274    #[test]
1275    fn test_extract_environment_from_filename() {
1276        assert_eq!(
1277            extract_environment_from_filename(&PathBuf::from("Dockerfile.dev")),
1278            Some("development".to_string())
1279        );
1280        assert_eq!(
1281            extract_environment_from_filename(&PathBuf::from("docker-compose.prod.yml")),
1282            Some("production".to_string())
1283        );
1284        assert_eq!(
1285            extract_environment_from_filename(&PathBuf::from("Dockerfile")),
1286            None
1287        );
1288    }
1289}