xianyun_cluster/clusterdomain/
mod.rs1pub mod resolve;
6
7use crate::driver::{DnsReader, DomainSource, Driver, DriverError};
8use thiserror::Error;
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct DiscoverResult {
13 pub zone: String,
14 pub source: String,
15}
16
17#[derive(Debug, Error, PartialEq, Eq)]
20pub enum ClusterDomainError {
21 #[error("no cluster DNS zone could be determined from any known source: {0}")]
22 NoZoneFound(String),
23 #[error("no svc.<zone> entry in the resolv.conf search list")]
24 NoSvcZoneInSearch,
25 #[error("no ConfigMap with a Corefile in kube-system")]
26 NoCorefile,
27 #[error("no kubernetes plugin in the Corefile")]
28 NoKubernetesPlugin,
29 #[error("no --domain argument on the kube-dns Deployment")]
30 NoKubeDNSDomainArg,
31 #[error("cluster has no nodes")]
32 NoNodes,
33 #[error("no clusterDomain in kubelet configz")]
34 NoKubeletClusterDomain,
35 #[error("parsing ConfigMap list: {0}")]
36 ParsingConfigMapList(String),
37 #[error("parsing kube-dns Deployment: {0}")]
38 ParsingKubeDNSDeployment(String),
39 #[error("parsing kubelet configz: {0}")]
40 ParsingKubeletConfigZ(String),
41 #[error("parsing node list: {0}")]
42 ParsingNodeList(String),
43 #[error("listing nodes: {0}")]
44 ListingNodes(String),
45 #[error("{0}")]
46 Driver(#[from] DriverError),
47}
48
49fn kubernetes_plugin_zone(corefile: &str) -> Option<String> {
52 for line in corefile.lines() {
53 let trimmed = line.trim_start();
54 if let Some(rest) = trimmed.strip_prefix("kubernetes") {
55 let mut fields = rest.split_whitespace();
56 if let Some(zone) = fields.next() {
57 return Some(zone.to_string());
58 }
59 }
60 }
61
62 None
63}
64
65fn resolv_conf_search_zone(reader: &dyn DnsReader) -> Result<String, DriverError> {
67 let out = reader.run_job(
68 "busybox:1.36",
69 "default",
70 std::time::Duration::from_secs(120),
71 &["cat", "/etc/resolv.conf"],
72 )?;
73 parse_resolv_conf(&out).map_err(|e| DriverError::Other(e.to_string()))
74}
75
76fn corefile_zone(reader: &dyn DnsReader) -> Result<String, DriverError> {
78 let out = reader.get("configmap", "", "kube-system")?;
79 parse_corefile(&out).map_err(|e| DriverError::Other(e.to_string()))
80}
81
82fn kube_dns_domain_arg(reader: &dyn DnsReader) -> Result<String, DriverError> {
84 let out = reader.get("deployment", "kube-dns", "kube-system")?;
85 parse_kube_dns_args(&out).map_err(|e| DriverError::Other(e.to_string()))
86}
87
88fn kubelet_config_zone_domain(reader: &dyn DnsReader) -> Result<String, DriverError> {
90 let node = first_node_name(reader).map_err(|e| DriverError::Other(e.to_string()))?;
91 let out = reader.get_raw(&format!("/api/v1/nodes/{node}/proxy/configz"))?;
92 parse_kubelet_config_z(&out).map_err(|e| DriverError::Other(e.to_string()))
93}
94
95pub const RESOLV_CONF_SEARCH: DomainSource = DomainSource {
98 name: "pod /etc/resolv.conf search list",
99 read: resolv_conf_search_zone,
100};
101
102pub const COREDNS_COREFILE: DomainSource = DomainSource {
105 name: "CoreDNS Corefile",
106 read: corefile_zone,
107};
108
109pub const KUBE_DNS_DOMAIN_ARG: DomainSource = DomainSource {
112 name: "kube-dns Deployment --domain argument",
113 read: kube_dns_domain_arg,
114};
115
116pub const KUBELET_CONFIGZ: DomainSource = DomainSource {
119 name: "kubelet configz",
120 read: kubelet_config_zone_domain,
121};
122
123pub fn defaults() -> Vec<DomainSource> {
125 vec![
126 RESOLV_CONF_SEARCH,
127 COREDNS_COREFILE,
128 KUBE_DNS_DOMAIN_ARG,
129 KUBELET_CONFIGZ,
130 ]
131}
132
133pub fn sources_for(mgr: Option<&dyn Driver>) -> Vec<DomainSource> {
135 if let Some(mgr) = mgr
136 && let Some(sources) = mgr.domain_sources()
137 {
138 return sources;
139 }
140
141 defaults()
142}
143
144pub fn discover(
146 reader: &dyn DnsReader,
147 sources: &[DomainSource],
148) -> Result<DiscoverResult, ClusterDomainError> {
149 let mut attempts = Vec::with_capacity(sources.len());
150
151 for source in sources {
152 match (source.read)(reader) {
153 Ok(zone) => {
154 return Ok(DiscoverResult {
155 zone,
156 source: source.name.to_string(),
157 });
158 }
159 Err(err) => {
160 attempts.push(format!(
161 "{}: {} ({err})",
162 source.name,
163 refused_or_absent_driver_error(&err)
164 ));
165 }
166 }
167 }
168
169 Err(ClusterDomainError::NoZoneFound(attempts.join("; ")))
170}
171
172pub fn refused_or_absent(err: &ClusterDomainError) -> &'static str {
175 if let ClusterDomainError::Driver(driver_err) = err {
176 return refused_or_absent_driver_error(driver_err);
177 }
178
179 "absent"
180}
181
182fn refused_or_absent_driver_error(err: &DriverError) -> &'static str {
186 if is_forbidden(Some(err)) {
187 "refused"
188 } else {
189 "absent"
190 }
191}
192
193pub fn is_forbidden(err: Option<&DriverError>) -> bool {
196 match err {
197 Some(err) => err.to_string().contains("Forbidden"),
198 None => false,
199 }
200}
201
202pub fn parse_resolv_conf(raw: &str) -> Result<String, ClusterDomainError> {
204 for line in raw.lines() {
205 let fields: Vec<&str> = line.split_whitespace().collect();
206 if fields.len() < 2 || fields[0] != "search" {
207 continue;
208 }
209
210 for entry in &fields[1..] {
211 if let Some(zone) = entry.strip_prefix("svc.") {
212 return Ok(zone.to_string());
213 }
214 }
215 }
216
217 Err(ClusterDomainError::NoSvcZoneInSearch)
218}
219
220pub fn parse_corefile(raw: &str) -> Result<String, ClusterDomainError> {
222 #[derive(serde::Deserialize)]
223 struct ConfigMapList {
224 #[serde(default)]
225 items: Vec<ConfigMapItem>,
226 }
227
228 #[derive(serde::Deserialize)]
229 struct ConfigMapItem {
230 #[serde(default)]
231 data: std::collections::HashMap<String, String>,
232 }
233
234 let list: ConfigMapList = serde_json::from_str(raw)
235 .map_err(|e| ClusterDomainError::ParsingConfigMapList(e.to_string()))?;
236
237 for item in &list.items {
238 if let Some(corefile) = item.data.get("Corefile") {
239 return kubernetes_plugin_zone(corefile).ok_or(ClusterDomainError::NoKubernetesPlugin);
240 }
241 }
242
243 Err(ClusterDomainError::NoCorefile)
244}
245
246pub fn parse_kube_dns_args(raw: &str) -> Result<String, ClusterDomainError> {
248 #[derive(serde::Deserialize)]
249 struct Deployment {
250 spec: Spec,
251 }
252 #[derive(serde::Deserialize)]
253 struct Spec {
254 template: Template,
255 }
256 #[derive(serde::Deserialize)]
257 struct Template {
258 spec: TemplateSpec,
259 }
260 #[derive(serde::Deserialize)]
261 struct TemplateSpec {
262 #[serde(default)]
263 containers: Vec<Container>,
264 }
265 #[derive(serde::Deserialize)]
266 struct Container {
267 #[serde(default)]
268 args: Vec<String>,
269 }
270
271 let deployment: Deployment = serde_json::from_str(raw)
272 .map_err(|e| ClusterDomainError::ParsingKubeDNSDeployment(e.to_string()))?;
273
274 const FLAGS: [&str; 2] = ["--domain=", "--cluster-domain="];
275
276 for container in &deployment.spec.template.spec.containers {
277 for arg in &container.args {
278 for flag in FLAGS {
279 if let Some(zone) = arg.strip_prefix(flag) {
280 return Ok(zone.trim_end_matches('.').to_string());
281 }
282 }
283 }
284 }
285
286 Err(ClusterDomainError::NoKubeDNSDomainArg)
287}
288
289pub fn parse_kubelet_config_z(raw: &str) -> Result<String, ClusterDomainError> {
291 #[derive(serde::Deserialize, Default)]
292 struct Configz {
293 #[serde(default)]
294 kubeletconfig: KubeletConfig,
295 }
296 #[derive(serde::Deserialize, Default)]
297 struct KubeletConfig {
298 #[serde(default, rename = "clusterDomain")]
299 cluster_domain: String,
300 }
301
302 let configz: Configz = serde_json::from_str(raw)
303 .map_err(|e| ClusterDomainError::ParsingKubeletConfigZ(e.to_string()))?;
304
305 if configz.kubeletconfig.cluster_domain.is_empty() {
306 return Err(ClusterDomainError::NoKubeletClusterDomain);
307 }
308
309 Ok(configz.kubeletconfig.cluster_domain)
310}
311
312pub fn first_node_name(backend: &dyn DnsReader) -> Result<String, ClusterDomainError> {
314 let out = backend
315 .get("nodes", "", "")
316 .map_err(|e| ClusterDomainError::ListingNodes(e.to_string()))?;
317
318 #[derive(serde::Deserialize)]
319 struct NodeList {
320 #[serde(default)]
321 items: Vec<Node>,
322 }
323 #[derive(serde::Deserialize)]
324 struct Node {
325 metadata: Metadata,
326 }
327 #[derive(serde::Deserialize)]
328 struct Metadata {
329 #[serde(default)]
330 name: String,
331 }
332
333 let list: NodeList = serde_json::from_str(&out)
334 .map_err(|e| ClusterDomainError::ParsingNodeList(e.to_string()))?;
335
336 list.items
337 .into_iter()
338 .next()
339 .map(|n| n.metadata.name)
340 .ok_or(ClusterDomainError::NoNodes)
341}