1#[derive(Debug, Clone, Default)]
10pub struct NetworkConfig {
11 pub hostname: String,
12 pub cluster_domain: String,
13}
14
15#[derive(Debug, Clone, Default)]
17pub struct ClusterConfig {
18 pub network: Option<NetworkConfig>,
19}
20
21impl ClusterConfig {
22 #[must_use]
27 pub fn get_all_hostnames(&self, cluster_name: &str) -> Vec<String> {
28 let hostname = self
29 .network
30 .as_ref()
31 .map(|network| {
32 network
33 .hostname
34 .replace("${CAPRONI_CLUSTER_NAME}", cluster_name)
35 })
36 .unwrap_or_default();
37 vec![hostname]
38 }
39}
40
41#[must_use]
43pub fn default_permitted_tlds() -> Vec<String> {
44 vec![
45 ".local".to_string(),
46 ".test".to_string(),
47 ".svc".to_string(),
48 ]
49}
50
51#[must_use]
53pub fn permitted_dns_for(cluster: &ClusterConfig, cluster_name: &str) -> Vec<String> {
54 match &cluster.network {
55 None => build_permitted_dns("", ""),
56 Some(network) => build_permitted_dns(
57 &cluster.get_all_hostnames(cluster_name)[0],
58 &network.cluster_domain,
59 ),
60 }
61}
62
63#[must_use]
65pub fn build_permitted_dns(hostname: &str, cluster_domain: &str) -> Vec<String> {
66 let defaults = default_permitted_tlds();
67 let mut permitted = defaults.clone();
68
69 let subtree = if !cluster_domain.is_empty() && !covered(cluster_domain, &defaults) {
70 format!(".{cluster_domain}")
71 } else {
72 String::new()
73 };
74
75 if !hostname.is_empty() {
76 let mut with_subtree = defaults.clone();
77 if !subtree.is_empty() {
78 with_subtree.push(subtree.clone());
79 }
80 if !covered(hostname, &with_subtree) {
81 permitted.push(hostname.to_string());
82 }
83 }
84
85 if !subtree.is_empty() {
86 permitted.push(subtree);
87 }
88
89 permitted
90}
91
92fn covered(name: &str, subtrees: &[String]) -> bool {
94 subtrees
95 .iter()
96 .any(|s| !s.is_empty() && name.ends_with(s.as_str()))
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
105 fn build_permitted_dns_computes_name_constraints() {
106 let defaults = default_permitted_tlds();
107
108 struct Case {
109 name: &'static str,
110 hostname: &'static str,
111 cluster_domain: &'static str,
112 want: Vec<String>,
113 }
114
115 let mut with_hostname = defaults.clone();
116 with_hostname.push("gitlab.dev".to_string());
117
118 let mut with_deep_subdomain = defaults.clone();
119 with_deep_subdomain.push("my.custom.domain".to_string());
120
121 let mut with_custom_cluster_domain = defaults.clone();
122 with_custom_cluster_domain.push(".corp.example".to_string());
123
124 let mut with_hostname_and_cluster_domain = defaults.clone();
125 with_hostname_and_cluster_domain.push("gitlab.dev".to_string());
126 with_hostname_and_cluster_domain.push(".corp.example".to_string());
127
128 let mut with_hostname_inside_cluster_domain = defaults.clone();
129 with_hostname_inside_cluster_domain.push(".corp.example".to_string());
130
131 let cases = vec![
132 Case {
133 name: "empty inputs",
134 hostname: "",
135 cluster_domain: "",
136 want: defaults.clone(),
137 },
138 Case {
139 name: "hostname covered by .test",
140 hostname: "gitlab.test",
141 cluster_domain: "",
142 want: defaults.clone(),
143 },
144 Case {
145 name: "hostname covered by .local",
146 hostname: "app.local",
147 cluster_domain: "",
148 want: defaults.clone(),
149 },
150 Case {
151 name: "hostname not covered",
152 hostname: "gitlab.dev",
153 cluster_domain: "",
154 want: with_hostname,
155 },
156 Case {
157 name: "deep subdomain not covered",
158 hostname: "my.custom.domain",
159 cluster_domain: "",
160 want: with_deep_subdomain,
161 },
162 Case {
163 name: "bare TLD match",
164 hostname: "something.test",
165 cluster_domain: "",
166 want: defaults.clone(),
167 },
168 Case {
169 name: "cluster.local is covered",
170 hostname: "",
171 cluster_domain: "cluster.local",
172 want: defaults.clone(),
173 },
174 Case {
175 name: "custom cluster domain is added as a subtree",
176 hostname: "",
177 cluster_domain: "corp.example",
178 want: with_custom_cluster_domain,
179 },
180 Case {
181 name: "hostname and cluster domain",
182 hostname: "gitlab.dev",
183 cluster_domain: "corp.example",
184 want: with_hostname_and_cluster_domain,
185 },
186 Case {
187 name: "hostname inside the cluster domain",
188 hostname: "gitlab.corp.example",
189 cluster_domain: "corp.example",
190 want: with_hostname_inside_cluster_domain,
191 },
192 ];
193
194 for case in cases {
195 let got = build_permitted_dns(case.hostname, case.cluster_domain);
196 assert_eq!(got, case.want, "case: {}", case.name);
197 }
198 }
199
200 #[test]
202 fn permitted_dns_for_derives_from_cluster_config() {
203 struct Case {
204 name: &'static str,
205 network: Option<NetworkConfig>,
206 want: Vec<&'static str>,
207 }
208
209 let cases = vec![
210 Case {
211 name: "no network",
212 network: None,
213 want: vec![".local", ".test", ".svc"],
214 },
215 Case {
216 name: "covered hostname",
217 network: Some(NetworkConfig {
218 hostname: "gitlab.test".into(),
219 cluster_domain: String::new(),
220 }),
221 want: vec![".local", ".test", ".svc"],
222 },
223 Case {
224 name: "hostname and cluster domain",
225 network: Some(NetworkConfig {
226 hostname: "gitlab.example.com".into(),
227 cluster_domain: "corp.example".into(),
228 }),
229 want: vec![
230 ".local",
231 ".test",
232 ".svc",
233 "gitlab.example.com",
234 ".corp.example",
235 ],
236 },
237 Case {
238 name: "cluster name is expanded",
239 network: Some(NetworkConfig {
240 hostname: "gitlab.${CAPRONI_CLUSTER_NAME}.example.com".into(),
241 cluster_domain: String::new(),
242 }),
243 want: vec![".local", ".test", ".svc", "gitlab.dev.example.com"],
244 },
245 ];
246
247 for case in cases {
248 let cluster = ClusterConfig {
249 network: case.network,
250 };
251 let got = permitted_dns_for(&cluster, "dev");
252 let want: Vec<String> = case.want.iter().map(|s| s.to_string()).collect();
253 assert_eq!(got, want, "case: {}", case.name);
254 }
255 }
256}