1use crate::error::Result;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fs;
15use std::path::{Path, PathBuf};
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct DockerAnalysis {
20 pub dockerfiles: Vec<DockerfileInfo>,
22 pub compose_files: Vec<ComposeFileInfo>,
24 pub services: Vec<DockerService>,
26 pub networking: NetworkingConfig,
28 pub orchestration_pattern: OrchestrationPattern,
30 pub environments: Vec<DockerEnvironment>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct DockerfileInfo {
37 pub path: PathBuf,
39 pub environment: Option<String>,
41 pub base_image: Option<String>,
43 pub exposed_ports: Vec<u16>,
45 pub workdir: Option<String>,
47 pub entrypoint: Option<String>,
49 pub env_vars: Vec<String>,
51 pub build_stages: Vec<String>,
53 pub is_multistage: bool,
55 pub instruction_count: usize,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct ComposeFileInfo {
62 pub path: PathBuf,
64 pub environment: Option<String>,
66 pub version: Option<String>,
68 pub service_names: Vec<String>,
70 pub networks: Vec<String>,
72 pub volumes: Vec<String>,
74 pub external_dependencies: Vec<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
80pub enum OrchestrationPattern {
81 SingleContainer,
83 DockerCompose,
85 Microservices,
87 EventDriven,
89 ServiceMesh,
91 Mixed,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct DockerService {
98 pub name: String,
100 pub compose_file: PathBuf,
102 pub image_or_build: ImageOrBuild,
104 pub ports: Vec<PortMapping>,
106 pub environment: HashMap<String, String>,
108 pub depends_on: Vec<String>,
110 pub networks: Vec<String>,
112 pub volumes: Vec<VolumeMount>,
114 pub health_check: Option<HealthCheck>,
116 pub restart_policy: Option<String>,
118 pub resource_limits: Option<ResourceLimits>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub enum ImageOrBuild {
125 Image(String),
127 Build {
129 context: String,
130 dockerfile: Option<String>,
131 args: HashMap<String, String>,
132 },
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct PortMapping {
138 pub host_port: Option<u16>,
140 pub container_port: u16,
142 pub protocol: String,
144 pub exposed_to_host: bool,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct VolumeMount {
151 pub source: String,
153 pub target: String,
155 pub mount_type: String,
157 pub read_only: bool,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163pub struct HealthCheck {
164 pub test: String,
166 pub interval: Option<String>,
168 pub timeout: Option<String>,
170 pub retries: Option<u32>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176pub struct ResourceLimits {
177 pub cpu_limit: Option<String>,
179 pub memory_limit: Option<String>,
181 pub cpu_reservation: Option<String>,
183 pub memory_reservation: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub struct NetworkingConfig {
190 pub custom_networks: Vec<NetworkInfo>,
192 pub service_discovery: ServiceDiscoveryConfig,
194 pub load_balancing: Vec<LoadBalancerConfig>,
196 pub external_connectivity: ExternalConnectivity,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct NetworkInfo {
203 pub name: String,
205 pub driver: Option<String>,
207 pub external: bool,
209 pub connected_services: Vec<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
215pub struct ServiceDiscoveryConfig {
216 pub internal_dns: bool,
218 pub external_tools: Vec<String>,
220 pub service_mesh: bool,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226pub struct LoadBalancerConfig {
227 pub service: String,
229 pub lb_type: String,
231 pub backends: Vec<String>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237pub struct ExternalConnectivity {
238 pub exposed_services: Vec<ExposedService>,
240 pub ingress_patterns: Vec<String>,
242 pub api_gateways: Vec<String>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
248pub struct ExposedService {
249 pub service: String,
251 pub external_ports: Vec<u16>,
253 pub protocols: Vec<String>,
255 pub ssl_enabled: bool,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
261pub struct DockerEnvironment {
262 pub name: String,
264 pub dockerfiles: Vec<PathBuf>,
266 pub compose_files: Vec<PathBuf>,
268 pub config_overrides: HashMap<String, String>,
270}
271
272pub 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 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 let parsed_dockerfiles: Vec<DockerfileInfo> = dockerfiles
291 .into_iter()
292 .filter_map(|path| parse_dockerfile(&path).ok())
293 .collect();
294
295 let parsed_compose_files: Vec<ComposeFileInfo> = compose_files
297 .into_iter()
298 .filter_map(|path| parse_compose_file(&path).ok())
299 .collect();
300
301 let services = extract_services_from_compose(&parsed_compose_files)?;
303
304 let networking = analyze_networking(&services, &parsed_compose_files)?;
306
307 let orchestration_pattern = determine_orchestration_pattern(&services, &networking);
309
310 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
323fn find_dockerfiles(project_root: &Path) -> Result<Vec<PathBuf>> {
325 let mut dockerfiles = Vec::new();
326
327 fn collect_dockerfiles_recursive(dir: &Path, dockerfiles: &mut Vec<PathBuf>) -> Result<()> {
328 if dir.file_name().is_some_and(|name| {
329 name == "node_modules" || name == ".git" || name == "target" || name == ".next"
330 }) {
331 return Ok(());
332 }
333
334 for entry in fs::read_dir(dir)? {
335 let entry = entry?;
336 let path = entry.path();
337
338 if path.is_dir() {
339 collect_dockerfiles_recursive(&path, dockerfiles)?;
340 } else if let Some(filename) = path.file_name().and_then(|n| n.to_str())
341 && is_dockerfile_name(filename)
342 {
343 dockerfiles.push(path);
344 }
345 }
346 Ok(())
347 }
348
349 collect_dockerfiles_recursive(project_root, &mut dockerfiles)?;
350
351 Ok(dockerfiles)
352}
353
354fn is_dockerfile_name(filename: &str) -> bool {
356 let filename_lower = filename.to_lowercase();
357
358 if filename_lower == "dockerfile" {
360 return true;
361 }
362
363 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
375fn find_compose_files(project_root: &Path) -> Result<Vec<PathBuf>> {
377 let mut compose_files = Vec::new();
378
379 fn collect_compose_files_recursive(dir: &Path, compose_files: &mut Vec<PathBuf>) -> Result<()> {
380 if dir.file_name().is_some_and(|name| {
381 name == "node_modules" || name == ".git" || name == "target" || name == ".next"
382 }) {
383 return Ok(());
384 }
385
386 for entry in fs::read_dir(dir)? {
387 let entry = entry?;
388 let path = entry.path();
389
390 if path.is_dir() {
391 collect_compose_files_recursive(&path, compose_files)?;
392 } else if let Some(filename) = path.file_name().and_then(|n| n.to_str())
393 && is_compose_file_name(filename)
394 {
395 compose_files.push(path);
396 }
397 }
398 Ok(())
399 }
400
401 collect_compose_files_recursive(project_root, &mut compose_files)?;
402
403 Ok(compose_files)
404}
405
406fn is_compose_file_name(filename: &str) -> bool {
408 let filename_lower = filename.to_lowercase();
409
410 let patterns = [
412 "docker-compose.yml",
413 "docker-compose.yaml",
414 "compose.yml",
415 "compose.yaml",
416 ];
417
418 for pattern in &patterns {
420 if filename_lower == *pattern {
421 return true;
422 }
423 }
424
425 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
441fn 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 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 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 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 if let Some(captures) = workdir_regex.captures(line) {
499 info.workdir = Some(captures.get(1).unwrap().as_str().trim().to_string());
500 }
501
502 if let Some(captures) = cmd_regex.captures(line)
504 && info.entrypoint.is_none()
505 {
506 info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
507 }
508
509 if let Some(captures) = entrypoint_regex.captures(line) {
510 info.entrypoint = Some(captures.get(1).unwrap().as_str().trim().to_string());
511 }
512
513 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
523fn parse_compose_file(path: &PathBuf) -> Result<ComposeFileInfo> {
525 let content = fs::read_to_string(path)?;
526
527 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 if let Some(version) = yaml_value.get("version").and_then(|v| v.as_str()) {
547 info.version = Some(version.to_string());
548 }
549
550 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 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 if let Some(config) = network_config.as_mapping()
567 && config
568 .get("external")
569 .and_then(|e| e.as_bool())
570 .unwrap_or(false)
571 {
572 info.external_dependencies.push(format!("network:{}", name));
573 }
574 }
575 }
576 }
577
578 if let Some(volumes) = yaml_value.get("volumes").and_then(|v| v.as_mapping()) {
580 for (volume_name, volume_config) in volumes {
581 if let Some(name) = volume_name.as_str() {
582 info.volumes.push(name.to_string());
583
584 if let Some(config) = volume_config.as_mapping()
586 && config
587 .get("external")
588 .and_then(|e| e.as_bool())
589 .unwrap_or(false)
590 {
591 info.external_dependencies.push(format!("volume:{}", name));
592 }
593 }
594 }
595 }
596
597 Ok(info)
598}
599
600fn extract_environment_from_filename(path: &Path) -> Option<String> {
602 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
603 let filename_lower = filename.to_lowercase();
604
605 let map_env = |env: &str| -> Option<String> {
607 match env {
608 "dev" | "development" | "local" => Some("development".to_string()),
609 "prod" | "production" => Some("production".to_string()),
610 "test" | "testing" => Some("test".to_string()),
611 "stage" | "staging" => Some("staging".to_string()),
612 _ if env.len() <= 10 && !env.is_empty() => Some(env.to_string()),
613 _ => None,
614 }
615 };
616
617 if let Some(last_dot) = filename_lower.rfind('.')
619 && let Some(env_dot_pos) = filename_lower[..last_dot].rfind('.')
620 {
621 let env = &filename_lower[env_dot_pos + 1..last_dot];
622 if let Some(result) = map_env(env) {
623 return Some(result);
624 }
625 }
626
627 if let Some(dot_pos) = filename_lower.rfind('.') {
629 let ext = &filename_lower[dot_pos + 1..];
630 let base = &filename_lower[..dot_pos];
632 if (base.contains("dockerfile") || base.contains("docker-compose") || base == "compose")
633 && let Some(result) = map_env(ext)
634 {
635 return Some(result);
636 }
637 }
638 }
639 None
640}
641
642fn extract_services_from_compose(compose_files: &[ComposeFileInfo]) -> Result<Vec<DockerService>> {
644 let mut services = Vec::new();
645
646 for compose_file in compose_files {
647 let content = fs::read_to_string(&compose_file.path)?;
648 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| {
649 crate::error::AnalysisError::DependencyParsing {
650 file: compose_file.path.display().to_string(),
651 reason: format!("YAML parsing error: {}", e),
652 }
653 })?;
654
655 if let Some(services_yaml) = yaml_value.get("services").and_then(|s| s.as_mapping()) {
656 for (service_name, service_config) in services_yaml {
657 if let (Some(name), Some(config)) =
658 (service_name.as_str(), service_config.as_mapping())
659 {
660 let service = parse_docker_service(name, config, &compose_file.path)?;
661 services.push(service);
662 }
663 }
664 }
665 }
666
667 Ok(services)
668}
669
670fn parse_docker_service(
672 name: &str,
673 config: &serde_yaml::Mapping,
674 compose_file: &Path,
675) -> Result<DockerService> {
676 let mut service = DockerService {
677 name: name.to_string(),
678 compose_file: compose_file.to_path_buf(),
679 image_or_build: ImageOrBuild::Image("unknown".to_string()),
680 ports: Vec::new(),
681 environment: HashMap::new(),
682 depends_on: Vec::new(),
683 networks: Vec::new(),
684 volumes: Vec::new(),
685 health_check: None,
686 restart_policy: None,
687 resource_limits: None,
688 };
689
690 if let Some(image) = config.get("image").and_then(|i| i.as_str()) {
692 service.image_or_build = ImageOrBuild::Image(image.to_string());
693 } else if let Some(build_config) = config.get("build") {
694 if let Some(context) = build_config.as_str() {
695 service.image_or_build = ImageOrBuild::Build {
696 context: context.to_string(),
697 dockerfile: None,
698 args: HashMap::new(),
699 };
700 } else if let Some(build_mapping) = build_config.as_mapping() {
701 let context = build_mapping
702 .get("context")
703 .and_then(|c| c.as_str())
704 .unwrap_or(".")
705 .to_string();
706
707 let dockerfile = build_mapping
708 .get("dockerfile")
709 .and_then(|d| d.as_str())
710 .map(|s| s.to_string());
711
712 let mut args = HashMap::new();
713 if let Some(args_config) = build_mapping.get("args").and_then(|a| a.as_mapping()) {
714 for (key, value) in args_config {
715 if let (Some(k), Some(v)) = (key.as_str(), value.as_str()) {
716 args.insert(k.to_string(), v.to_string());
717 }
718 }
719 }
720
721 service.image_or_build = ImageOrBuild::Build {
722 context,
723 dockerfile,
724 args,
725 };
726 }
727 }
728
729 if let Some(ports_config) = config.get("ports").and_then(|p| p.as_sequence()) {
731 for port_item in ports_config {
732 if let Some(port_mapping) = parse_port_mapping(port_item) {
733 service.ports.push(port_mapping);
734 }
735 }
736 }
737
738 if let Some(env_config) = config.get("environment") {
740 parse_environment_variables(env_config, &mut service.environment);
741 }
742
743 if let Some(depends_config) = config.get("depends_on") {
745 if let Some(depends_sequence) = depends_config.as_sequence() {
746 for dep in depends_sequence {
747 if let Some(dep_name) = dep.as_str() {
748 service.depends_on.push(dep_name.to_string());
749 }
750 }
751 } else if let Some(depends_mapping) = depends_config.as_mapping() {
752 for (dep_name, _) in depends_mapping {
753 if let Some(name) = dep_name.as_str() {
754 service.depends_on.push(name.to_string());
755 }
756 }
757 }
758 }
759
760 if let Some(networks_config) = config.get("networks") {
762 if let Some(networks_sequence) = networks_config.as_sequence() {
763 for network in networks_sequence {
764 if let Some(network_name) = network.as_str() {
765 service.networks.push(network_name.to_string());
766 }
767 }
768 } else if let Some(networks_mapping) = networks_config.as_mapping() {
769 for (network_name, _) in networks_mapping {
770 if let Some(name) = network_name.as_str() {
771 service.networks.push(name.to_string());
772 }
773 }
774 }
775 }
776
777 if let Some(volumes_config) = config.get("volumes").and_then(|v| v.as_sequence()) {
779 for volume_item in volumes_config {
780 if let Some(volume_mount) = parse_volume_mount(volume_item) {
781 service.volumes.push(volume_mount);
782 }
783 }
784 }
785
786 if let Some(restart) = config.get("restart").and_then(|r| r.as_str()) {
788 service.restart_policy = Some(restart.to_string());
789 }
790
791 if let Some(healthcheck_config) = config.get("healthcheck").and_then(|h| h.as_mapping())
793 && let Some(test) = healthcheck_config.get("test").and_then(|t| t.as_str())
794 {
795 service.health_check = Some(HealthCheck {
796 test: test.to_string(),
797 interval: healthcheck_config
798 .get("interval")
799 .and_then(|i| i.as_str())
800 .map(|s| s.to_string()),
801 timeout: healthcheck_config
802 .get("timeout")
803 .and_then(|t| t.as_str())
804 .map(|s| s.to_string()),
805 retries: healthcheck_config
806 .get("retries")
807 .and_then(|r| r.as_u64())
808 .map(|r| r as u32),
809 });
810 }
811
812 Ok(service)
813}
814
815fn parse_port_mapping(port_value: &serde_yaml::Value) -> Option<PortMapping> {
817 if let Some(port_str) = port_value.as_str() {
818 if let Some(colon_pos) = port_str.find(':') {
820 let host_part = &port_str[..colon_pos];
821 let container_part = &port_str[colon_pos + 1..];
822
823 if let (Ok(host_port), Ok(container_port)) =
824 (host_part.parse::<u16>(), container_part.parse::<u16>())
825 {
826 return Some(PortMapping {
827 host_port: Some(host_port),
828 container_port,
829 protocol: "tcp".to_string(),
830 exposed_to_host: true,
831 });
832 }
833 } else if let Ok(container_port) = port_str.parse::<u16>() {
834 return Some(PortMapping {
835 host_port: None,
836 container_port,
837 protocol: "tcp".to_string(),
838 exposed_to_host: false,
839 });
840 }
841 } else if let Some(port_num) = port_value.as_u64() {
842 return Some(PortMapping {
843 host_port: None,
844 container_port: port_num as u16,
845 protocol: "tcp".to_string(),
846 exposed_to_host: false,
847 });
848 }
849
850 None
851}
852
853fn parse_volume_mount(volume_value: &serde_yaml::Value) -> Option<VolumeMount> {
855 if let Some(volume_str) = volume_value.as_str() {
856 let parts: Vec<&str> = volume_str.split(':').collect();
858 if parts.len() >= 2 {
859 return Some(VolumeMount {
860 source: parts[0].to_string(),
861 target: parts[1].to_string(),
862 mount_type: if parts[0].starts_with('/') || parts[0].starts_with('.') {
863 "bind".to_string()
864 } else {
865 "volume".to_string()
866 },
867 read_only: parts.get(2).is_some_and(|&opt| opt == "ro"),
868 });
869 }
870 }
871 None
872}
873
874fn parse_environment_variables(
876 env_value: &serde_yaml::Value,
877 env_map: &mut HashMap<String, String>,
878) {
879 if let Some(env_mapping) = env_value.as_mapping() {
880 for (key, value) in env_mapping {
881 if let Some(key_str) = key.as_str() {
882 let value_str = value.as_str().unwrap_or("").to_string();
883 env_map.insert(key_str.to_string(), value_str);
884 }
885 }
886 } else if let Some(env_sequence) = env_value.as_sequence() {
887 for env_item in env_sequence {
888 if let Some(env_str) = env_item.as_str() {
889 if let Some(eq_pos) = env_str.find('=') {
890 let key = env_str[..eq_pos].to_string();
891 let value = env_str[eq_pos + 1..].to_string();
892 env_map.insert(key, value);
893 } else {
894 env_map.insert(env_str.to_string(), String::new());
895 }
896 }
897 }
898 }
899}
900
901fn analyze_networking(
902 services: &[DockerService],
903 compose_files: &[ComposeFileInfo],
904) -> Result<NetworkingConfig> {
905 let mut custom_networks = Vec::new();
906 let mut connected_services: HashMap<String, Vec<String>> = HashMap::new();
907
908 for compose_file in compose_files {
910 for network_name in &compose_file.networks {
911 let network_info = NetworkInfo {
912 name: network_name.clone(),
913 driver: None, external: compose_file
915 .external_dependencies
916 .contains(&format!("network:{}", network_name)),
917 connected_services: Vec::new(),
918 };
919 custom_networks.push(network_info);
920 }
921 }
922
923 for service in services {
925 for network in &service.networks {
926 connected_services
927 .entry(network.clone())
928 .or_default()
929 .push(service.name.clone());
930 }
931 }
932
933 for network in &mut custom_networks {
935 if let Some(services) = connected_services.get(&network.name) {
936 network.connected_services = services.clone();
937 }
938 }
939
940 let service_discovery = ServiceDiscoveryConfig {
942 internal_dns: !services.is_empty(), external_tools: detect_service_discovery_tools(services),
944 service_mesh: detect_service_mesh(services),
945 };
946
947 let load_balancing = detect_load_balancers(services);
949
950 let external_connectivity = analyze_external_connectivity(services);
952
953 Ok(NetworkingConfig {
954 custom_networks,
955 service_discovery,
956 load_balancing,
957 external_connectivity,
958 })
959}
960
961fn determine_orchestration_pattern(
962 services: &[DockerService],
963 networking: &NetworkingConfig,
964) -> OrchestrationPattern {
965 if services.is_empty() {
966 return OrchestrationPattern::SingleContainer;
967 }
968
969 if services.len() == 1 {
970 return OrchestrationPattern::SingleContainer;
971 }
972
973 let has_multiple_backends = services
975 .iter()
976 .filter(|s| match &s.image_or_build {
977 ImageOrBuild::Image(img) => {
978 !img.contains("nginx") && !img.contains("proxy") && !img.contains("traefik")
979 }
980 _ => true,
981 })
982 .count()
983 > 2;
984
985 let has_service_discovery = networking.service_discovery.internal_dns
986 || !networking.service_discovery.external_tools.is_empty();
987
988 let _has_load_balancing = !networking.load_balancing.is_empty();
989
990 let has_message_queues = services.iter().any(|s| match &s.image_or_build {
991 ImageOrBuild::Image(img) => {
992 img.contains("redis")
993 || img.contains("rabbitmq")
994 || img.contains("kafka")
995 || img.contains("nats")
996 }
997 _ => false,
998 });
999
1000 if networking.service_discovery.service_mesh {
1001 OrchestrationPattern::ServiceMesh
1002 } else if has_message_queues && has_multiple_backends {
1003 OrchestrationPattern::EventDriven
1004 } else if has_multiple_backends && has_service_discovery {
1005 OrchestrationPattern::Microservices
1006 } else {
1007 OrchestrationPattern::DockerCompose
1008 }
1009}
1010
1011fn detect_service_discovery_tools(services: &[DockerService]) -> Vec<String> {
1013 let mut tools = Vec::new();
1014
1015 for service in services {
1016 if let ImageOrBuild::Image(image) = &service.image_or_build {
1017 if image.contains("consul") {
1018 tools.push("consul".to_string());
1019 }
1020 if image.contains("etcd") {
1021 tools.push("etcd".to_string());
1022 }
1023 if image.contains("zookeeper") {
1024 tools.push("zookeeper".to_string());
1025 }
1026 }
1027 }
1028
1029 tools.sort();
1030 tools.dedup();
1031 tools
1032}
1033
1034fn detect_service_mesh(services: &[DockerService]) -> bool {
1036 services.iter().any(|s| {
1037 if let ImageOrBuild::Image(image) = &s.image_or_build {
1038 image.contains("istio")
1039 || image.contains("linkerd")
1040 || image.contains("envoy")
1041 || image.contains("consul-connect")
1042 } else {
1043 false
1044 }
1045 })
1046}
1047
1048fn detect_load_balancers(services: &[DockerService]) -> Vec<LoadBalancerConfig> {
1050 let mut load_balancers = Vec::new();
1051
1052 for service in services {
1053 let is_load_balancer = match &service.image_or_build {
1055 ImageOrBuild::Image(image) => {
1056 image.contains("nginx")
1057 || image.contains("traefik")
1058 || image.contains("haproxy")
1059 || image.contains("envoy")
1060 || image.contains("kong")
1061 }
1062 _ => false,
1063 };
1064
1065 if is_load_balancer {
1066 let backends: Vec<String> = services
1068 .iter()
1069 .filter(|s| s.name != service.name && !service.depends_on.contains(&s.name))
1070 .map(|s| s.name.clone())
1071 .collect();
1072
1073 if !backends.is_empty() {
1074 let lb_type = match &service.image_or_build {
1075 ImageOrBuild::Image(image) => {
1076 if image.contains("nginx") {
1077 "nginx"
1078 } else if image.contains("traefik") {
1079 "traefik"
1080 } else if image.contains("haproxy") {
1081 "haproxy"
1082 } else if image.contains("envoy") {
1083 "envoy"
1084 } else if image.contains("kong") {
1085 "kong"
1086 } else {
1087 "unknown"
1088 }
1089 }
1090 _ => "unknown",
1091 };
1092
1093 load_balancers.push(LoadBalancerConfig {
1094 service: service.name.clone(),
1095 lb_type: lb_type.to_string(),
1096 backends,
1097 });
1098 }
1099 }
1100 }
1101
1102 load_balancers
1103}
1104
1105fn analyze_external_connectivity(services: &[DockerService]) -> ExternalConnectivity {
1107 let mut exposed_services = Vec::new();
1108 let mut ingress_patterns = Vec::new();
1109 let mut api_gateways = Vec::new();
1110
1111 for service in services {
1112 let mut external_ports = Vec::new();
1113 let mut protocols = Vec::new();
1114
1115 for port in &service.ports {
1117 if port.exposed_to_host {
1118 if let Some(host_port) = port.host_port {
1119 external_ports.push(host_port);
1120 }
1121 protocols.push(port.protocol.clone());
1122 }
1123 }
1124
1125 if !external_ports.is_empty() {
1126 let ssl_enabled = external_ports.contains(&443)
1128 || external_ports.contains(&8443)
1129 || service
1130 .environment
1131 .keys()
1132 .any(|k| k.to_lowercase().contains("ssl") || k.to_lowercase().contains("tls"));
1133
1134 exposed_services.push(ExposedService {
1135 service: service.name.clone(),
1136 external_ports,
1137 protocols: protocols
1138 .into_iter()
1139 .collect::<std::collections::HashSet<_>>()
1140 .into_iter()
1141 .collect(),
1142 ssl_enabled,
1143 });
1144 }
1145
1146 if service.name.to_lowercase().contains("gateway")
1148 || service.name.to_lowercase().contains("api")
1149 || service.name.to_lowercase().contains("proxy")
1150 {
1151 api_gateways.push(service.name.clone());
1152 }
1153
1154 if let ImageOrBuild::Image(image) = &service.image_or_build
1156 && (image.contains("kong")
1157 || image.contains("zuul")
1158 || image.contains("ambassador")
1159 || image.contains("traefik"))
1160 && !api_gateways.contains(&service.name)
1161 {
1162 api_gateways.push(service.name.clone());
1163 }
1164 }
1165
1166 if exposed_services.len() == 1 && api_gateways.len() == 1 {
1168 ingress_patterns.push("Single API Gateway".to_string());
1169 } else if exposed_services.len() > 1 && api_gateways.is_empty() {
1170 ingress_patterns.push("Multiple Direct Entry Points".to_string());
1171 } else if !api_gateways.is_empty() {
1172 ingress_patterns.push("API Gateway Pattern".to_string());
1173 }
1174
1175 let has_reverse_proxy = services.iter().any(|s| {
1177 if let ImageOrBuild::Image(image) = &s.image_or_build {
1178 image.contains("nginx") || image.contains("apache") || image.contains("caddy")
1179 } else {
1180 false
1181 }
1182 });
1183
1184 if has_reverse_proxy {
1185 ingress_patterns.push("Reverse Proxy".to_string());
1186 }
1187
1188 ExternalConnectivity {
1189 exposed_services,
1190 ingress_patterns,
1191 api_gateways,
1192 }
1193}
1194
1195fn analyze_environments(
1196 dockerfiles: &[DockerfileInfo],
1197 compose_files: &[ComposeFileInfo],
1198) -> Vec<DockerEnvironment> {
1199 let mut environments: HashMap<String, DockerEnvironment> = HashMap::new();
1200
1201 for dockerfile in dockerfiles {
1203 let env_name = dockerfile
1204 .environment
1205 .clone()
1206 .unwrap_or_else(|| "default".to_string());
1207 environments
1208 .entry(env_name.clone())
1209 .or_insert_with(|| DockerEnvironment {
1210 name: env_name,
1211 dockerfiles: Vec::new(),
1212 compose_files: Vec::new(),
1213 config_overrides: HashMap::new(),
1214 })
1215 .dockerfiles
1216 .push(dockerfile.path.clone());
1217 }
1218
1219 for compose_file in compose_files {
1221 let env_name = compose_file
1222 .environment
1223 .clone()
1224 .unwrap_or_else(|| "default".to_string());
1225 environments
1226 .entry(env_name.clone())
1227 .or_insert_with(|| DockerEnvironment {
1228 name: env_name,
1229 dockerfiles: Vec::new(),
1230 compose_files: Vec::new(),
1231 config_overrides: HashMap::new(),
1232 })
1233 .compose_files
1234 .push(compose_file.path.clone());
1235 }
1236
1237 environments.into_values().collect()
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243
1244 #[test]
1245 fn test_is_dockerfile_name() {
1246 assert!(is_dockerfile_name("Dockerfile"));
1247 assert!(is_dockerfile_name("dockerfile"));
1248 assert!(is_dockerfile_name("Dockerfile.dev"));
1249 assert!(is_dockerfile_name("dockerfile.prod"));
1250 assert!(is_dockerfile_name("api.dockerfile"));
1251 assert!(!is_dockerfile_name("README.md"));
1252 assert!(!is_dockerfile_name("package.json"));
1253 }
1254
1255 #[test]
1256 fn test_is_compose_file_name() {
1257 assert!(is_compose_file_name("docker-compose.yml"));
1258 assert!(is_compose_file_name("docker-compose.yaml"));
1259 assert!(is_compose_file_name("docker-compose.dev.yml"));
1260 assert!(is_compose_file_name("docker-compose.prod.yaml"));
1261 assert!(is_compose_file_name("compose.yml"));
1262 assert!(is_compose_file_name("compose.yaml"));
1263 assert!(!is_compose_file_name("README.md"));
1264 assert!(!is_compose_file_name("package.json"));
1265 }
1266
1267 #[test]
1268 fn test_extract_environment_from_filename() {
1269 assert_eq!(
1270 extract_environment_from_filename(&PathBuf::from("Dockerfile.dev")),
1271 Some("development".to_string())
1272 );
1273 assert_eq!(
1274 extract_environment_from_filename(&PathBuf::from("docker-compose.prod.yml")),
1275 Some("production".to_string())
1276 );
1277 assert_eq!(
1278 extract_environment_from_filename(&PathBuf::from("Dockerfile")),
1279 None
1280 );
1281 }
1282}