Skip to main content

xianyun_cluster/clusterdomain/
mod.rs

1//! Port of `internal/clusterdomain/clusterdomain.go`: discovers the DNS zone
2//! a cluster serves, through the sources its driver names in
3//! `driver::Driver::domain_sources`.
4
5pub mod resolve;
6
7use crate::driver::{DnsReader, DomainSource, Driver, DriverError};
8use thiserror::Error;
9
10/// Port of `clusterdomain.Result`: the zone a cluster's DNS is confirmed to serve.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct DiscoverResult {
13    pub zone: String,
14    pub source: String,
15}
16
17/// Port of `clusterdomain.ErrNoZoneFound` and the source-specific sentinels
18/// it wraps.
19#[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
49/// Port of the private `kubernetesPluginPattern` regex: matches a
50/// `kubernetes <zone> ...` directive at the start of a Corefile line.
51fn 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
65/// Port of `clusterdomain.resolvConfSearchZone`.
66fn 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
76/// Port of `clusterdomain.corefileZone`.
77fn 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
82/// Port of `clusterdomain.kubeDNSDomainArg`.
83fn 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
88/// Port of `clusterdomain.kubeletConfigZoneDomain`.
89fn 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
95/// Port of `clusterdomain.ResolvConfSearch`: reads a probe pod's
96/// `/etc/resolv.conf` search list; needs only the right to run a pod.
97pub const RESOLV_CONF_SEARCH: DomainSource = DomainSource {
98    name: "pod /etc/resolv.conf search list",
99    read: resolv_conf_search_zone,
100};
101
102/// Port of `clusterdomain.CoreDNSCorefile`: reads the `kubernetes` plugin
103/// zone from the Corefile in `kube-system`.
104pub const COREDNS_COREFILE: DomainSource = DomainSource {
105    name: "CoreDNS Corefile",
106    read: corefile_zone,
107};
108
109/// Port of `clusterdomain.KubeDNSDomainArg`: reads the `--domain` argument
110/// of a kube-dns Deployment.
111pub const KUBE_DNS_DOMAIN_ARG: DomainSource = DomainSource {
112    name: "kube-dns Deployment --domain argument",
113    read: kube_dns_domain_arg,
114};
115
116/// Port of `clusterdomain.KubeletConfigz`: reads the kubelet's
117/// `clusterDomain` through nodes/proxy; usually admin-only.
118pub const KUBELET_CONFIGZ: DomainSource = DomainSource {
119    name: "kubelet configz",
120    read: kubelet_config_zone_domain,
121};
122
123/// Port of `clusterdomain.Defaults`: the chain for a driver with no opinion of its own.
124pub fn defaults() -> Vec<DomainSource> {
125    vec![
126        RESOLV_CONF_SEARCH,
127        COREDNS_COREFILE,
128        KUBE_DNS_DOMAIN_ARG,
129        KUBELET_CONFIGZ,
130    ]
131}
132
133/// Port of `clusterdomain.SourcesFor`.
134pub 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
144/// Port of `clusterdomain.Discover`.
145pub 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
172/// Port of `clusterdomain.refusedOrAbsent`, generalized to any
173/// [`ClusterDomainError`].
174pub 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
182/// Shared implementation used by both [`refused_or_absent`] and
183/// [`discover`], which classifies a raw [`DriverError`] rather than a
184/// wrapped [`ClusterDomainError`].
185fn refused_or_absent_driver_error(err: &DriverError) -> &'static str {
186    if is_forbidden(Some(err)) {
187        "refused"
188    } else {
189        "absent"
190    }
191}
192
193/// Port of `clusterdomain.isForbidden`. Takes `Option` since the Go
194/// original is called with a possibly-nil `error`.
195pub fn is_forbidden(err: Option<&DriverError>) -> bool {
196    match err {
197        Some(err) => err.to_string().contains("Forbidden"),
198        None => false,
199    }
200}
201
202/// Port of `clusterdomain.parseResolvConf`.
203pub 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
220/// Port of `clusterdomain.parseCorefile`.
221pub 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
246/// Port of `clusterdomain.parseKubeDNSArgs`.
247pub 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
289/// Port of `clusterdomain.parseKubeletConfigZ`.
290pub 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
312/// Port of `clusterdomain.firstNodeName`.
313pub 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}