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