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().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
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().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
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 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 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 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 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 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
602fn 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 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 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 if let Some(dot_pos) = filename_lower.rfind('.') {
632 let ext = &filename_lower[dot_pos + 1..];
633 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
645fn 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
673fn 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 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 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 if let Some(env_config) = config.get("environment") {
743 parse_environment_variables(env_config, &mut service.environment);
744 }
745
746 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 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 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 if let Some(restart) = config.get("restart").and_then(|r| r.as_str()) {
791 service.restart_policy = Some(restart.to_string());
792 }
793
794 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
818fn parse_port_mapping(port_value: &serde_yaml::Value) -> Option<PortMapping> {
820 if let Some(port_str) = port_value.as_str() {
821 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
856fn parse_volume_mount(volume_value: &serde_yaml::Value) -> Option<VolumeMount> {
858 if let Some(volume_str) = volume_value.as_str() {
859 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
877fn 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 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, 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 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 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 let service_discovery = ServiceDiscoveryConfig {
945 internal_dns: !services.is_empty(), external_tools: detect_service_discovery_tools(services),
947 service_mesh: detect_service_mesh(services),
948 };
949
950 let load_balancing = detect_load_balancers(services);
952
953 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 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
1016fn 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
1039fn 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
1053fn detect_load_balancers(services: &[DockerService]) -> Vec<LoadBalancerConfig> {
1055 let mut load_balancers = Vec::new();
1056
1057 for service in services {
1058 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 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
1110fn 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 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 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 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 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 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 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 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 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}