syncable_cli/analyzer/dclint/rules/
dcl002.rs1use std::collections::HashMap;
6
7use crate::analyzer::dclint::rules::{LintContext, Rule, SimpleRule, make_failure};
8use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
9
10const CODE: &str = "DCL002";
11const NAME: &str = "no-duplicate-container-names";
12const DESCRIPTION: &str = "Container names must be unique across all services.";
13const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/no-duplicate-container-names-rule.md";
14
15pub fn rule() -> impl Rule {
16 SimpleRule::new(
17 CODE,
18 NAME,
19 Severity::Error,
20 RuleCategory::BestPractice,
21 DESCRIPTION,
22 URL,
23 check,
24 )
25}
26
27fn check(ctx: &LintContext) -> Vec<CheckFailure> {
28 let mut failures = Vec::new();
29 let mut container_names: HashMap<String, Vec<(String, u32)>> = HashMap::new();
30
31 for (service_name, service) in &ctx.compose.services {
33 if let Some(container_name) = &service.container_name {
34 let line = service
35 .container_name_pos
36 .map(|p| p.line)
37 .unwrap_or(service.position.line);
38
39 container_names
40 .entry(container_name.clone())
41 .or_default()
42 .push((service_name.clone(), line));
43 }
44 }
45
46 for (container_name, services) in container_names {
48 if services.len() > 1 {
49 for (service_name, line) in &services {
50 let other_services: Vec<&str> = services
51 .iter()
52 .filter(|(name, _)| name != service_name)
53 .map(|(name, _)| name.as_str())
54 .collect();
55
56 let message = format!(
57 "Container name \"{}\" is used by multiple services: \"{}\" and \"{}\".",
58 container_name,
59 service_name,
60 other_services.join("\", \"")
61 );
62
63 failures.push(
64 make_failure(
65 &CODE.into(),
66 NAME,
67 Severity::Error,
68 RuleCategory::BestPractice,
69 message,
70 *line,
71 1,
72 false,
73 )
74 .with_data("containerName", container_name.clone())
75 .with_data("serviceName", service_name.clone()),
76 );
77 }
78 }
79 }
80
81 failures
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::analyzer::dclint::parser::parse_compose;
88
89 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
90 let compose = parse_compose(yaml).unwrap();
91 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
92 check(&ctx)
93 }
94
95 #[test]
96 fn test_no_violation_unique_names() {
97 let yaml = r#"
98services:
99 web:
100 image: nginx
101 container_name: my-web
102 db:
103 image: postgres
104 container_name: my-db
105"#;
106 assert!(check_yaml(yaml).is_empty());
107 }
108
109 #[test]
110 fn test_no_violation_no_container_names() {
111 let yaml = r#"
112services:
113 web:
114 image: nginx
115 db:
116 image: postgres
117"#;
118 assert!(check_yaml(yaml).is_empty());
119 }
120
121 #[test]
122 fn test_violation_duplicate_names() {
123 let yaml = r#"
124services:
125 web:
126 image: nginx
127 container_name: my-container
128 api:
129 image: node
130 container_name: my-container
131"#;
132 let failures = check_yaml(yaml);
133 assert_eq!(failures.len(), 2); assert!(failures[0].message.contains("my-container"));
135 }
136
137 #[test]
138 fn test_violation_multiple_duplicates() {
139 let yaml = r#"
140services:
141 web:
142 image: nginx
143 container_name: shared-name
144 api:
145 image: node
146 container_name: shared-name
147 worker:
148 image: worker
149 container_name: shared-name
150"#;
151 let failures = check_yaml(yaml);
152 assert_eq!(failures.len(), 3); }
154}