cloudflare_dns_operator/
resources.rs

1use k8s_openapi::{
2    api::core::v1::{
3        ConfigMap,
4        Secret,
5    },
6    apimachinery::pkg::apis::meta::v1::Condition,
7};
8use kube::CustomResource;
9use schemars::JsonSchema;
10use serde::{
11    Deserialize,
12    Serialize,
13};
14
15/// Supported DNS record types.
16///
17/// See https://developers.cloudflare.com/dns/manage-dns-records/reference/dns-record-types/#dns-record-types
18#[allow(clippy::upper_case_acronyms)]
19#[derive(Default, Debug, PartialEq, Serialize, Deserialize, Clone, Copy, JsonSchema)]
20pub enum RecordType {
21    #[default]
22    #[serde(rename = "A")]
23    A,
24    #[serde(rename = "AAAA")]
25    AAAA,
26    #[serde(rename = "CNAME")]
27    CNAME,
28    #[serde(rename = "MX")]
29    MX,
30    #[serde(rename = "TXT")]
31    TXT,
32    #[serde(rename = "SRV")]
33    SRV,
34    #[serde(rename = "LOC")]
35    LOC,
36    #[serde(rename = "SPF")]
37    SPF,
38    #[serde(rename = "NS")]
39    NS,
40}
41
42impl std::str::FromStr for RecordType {
43    type Err = eyre::Report;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        match s {
47            "A" => Ok(RecordType::A),
48            "AAAA" => Ok(RecordType::AAAA),
49            "CNAME" => Ok(RecordType::CNAME),
50            "MX" => Ok(RecordType::MX),
51            "TXT" => Ok(RecordType::TXT),
52            "SRV" => Ok(RecordType::SRV),
53            "LOC" => Ok(RecordType::LOC),
54            "SPF" => Ok(RecordType::SPF),
55            "NS" => Ok(RecordType::NS),
56            s => Err(eyre::eyre!("Invalid RecordType: {s:?}")),
57        }
58    }
59}
60
61/// [CustomResource] definition for a Cloudflare DNS record.
62#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, PartialEq, JsonSchema)]
63#[kube(
64    group = "dns.cloudflare.com",
65    version = "v1alpha1",
66    kind = "CloudflareDNSRecord",
67    status = "CloudflareDNSRecordStatus",
68    namespaced
69)]
70pub struct CloudflareDNSRecordSpec {
71    /// The name of the record (e.g example.com)
72    pub name: String,
73    /// The type of the record (e.g A, CNAME, MX, TXT, SRV, LOC, SPF, NS). Defaults to A.
74    #[serde(rename = "type")]
75    pub ty: Option<RecordType>,
76    /// The content of the record such as an IP address or a service reference.
77    pub content: StringOrService,
78    /// TTL in seconds
79    pub ttl: Option<i64>,
80    /// Whether the record is proxied by Cloudflare
81    pub proxied: Option<bool>,
82    /// Arbitrary comment
83    pub comment: Option<String>,
84    /// Tags to apply to the record
85    pub tags: Option<Vec<String>>,
86    /// The cloudflare zone ID to create the record in
87    pub zone: ZoneNameOrId,
88}
89
90impl CloudflareDNSRecordSpec {
91    /// If set directly to a value, return that, otherwise look up the service and return the IP.
92    pub async fn lookup_content(&self, client: &kube::Client, ns: &str) -> eyre::Result<Option<String>> {
93        match &self.content {
94            StringOrService::Value(value) => Ok(Some(value.clone())),
95            StringOrService::Service(selector) => {
96                let ns = selector.namespace.as_deref().unwrap_or(ns);
97                let name = selector.name.as_str();
98                let record_type = self.ty;
99                let Some(ip) = crate::services::public_ip_from_service(client, name, ns, record_type).await? else {
100                    error!("no public ip found for service {ns}/{name}");
101                    return Ok(None);
102                };
103                Ok(Some(ip.to_string()))
104            }
105        }
106    }
107}
108
109/// Status of a Cloudflare DNS record.
110#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
111pub struct CloudflareDNSRecordStatus {
112    /// The ID of the cloudflare record
113    pub record_id: String,
114    /// The zone ID of the record
115    pub zone_id: String,
116    /// Whether we are able to resolve the DNS record (false) or not (true). If no dns check is performed, this field
117    /// will default to true.
118    pub pending: bool,
119    /// Status conditions
120    pub conditions: Option<Vec<Condition>>,
121}
122
123/// A Cloudflare DNS Zone. Can either be a name (such as example.com) or id.
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
125pub enum ZoneNameOrId {
126    #[serde(rename = "name")]
127    Name(ValueOrReference),
128    #[serde(rename = "id")]
129    Id(ValueOrReference),
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
133pub enum StringOrService {
134    #[serde(rename = "value")]
135    Value(String),
136    #[serde(rename = "service")]
137    Service(ServiceSelector),
138}
139
140#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
141pub struct ServiceSelector {
142    /// Service name
143    pub name: String,
144    /// Namespace, default is the same namespace as the referent.
145    pub namespace: Option<String>,
146}
147
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
149pub enum ValueOrReference {
150    #[serde(rename = "value")]
151    Value(String),
152    #[serde(rename = "from")]
153    Reference(Reference),
154}
155
156#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
157pub enum Reference {
158    #[serde(rename = "configMap")]
159    ConfigMap(k8s_openapi::api::core::v1::ConfigMapKeySelector),
160
161    #[serde(rename = "secret")]
162    Secret(k8s_openapi::api::core::v1::SecretKeySelector),
163}
164
165impl ValueOrReference {
166    pub async fn lookup(&self, client: &kube::Client, ns: &str) -> eyre::Result<Option<String>> {
167        match self {
168            ValueOrReference::Value(value) => Ok(Some(value.clone())),
169            ValueOrReference::Reference(reference) => reference.lookup(client, ns).await,
170        }
171    }
172}
173
174impl Reference {
175    async fn lookup(&self, client: &kube::Client, ns: &str) -> eyre::Result<Option<String>> {
176        match self {
177            Reference::ConfigMap(selector) => {
178                trace!(name = %selector.name, %ns, key = %selector.key, "configmap reference lookup");
179                let config_map = kube::api::Api::<ConfigMap>::namespaced(client.clone(), ns)
180                    .get(&selector.name)
181                    .await?;
182                let value = config_map.data.and_then(|data| data.get(&selector.key).cloned());
183                trace!(value = ?value, "configmap reference lookup result");
184                Ok(value)
185            }
186            Reference::Secret(selector) => {
187                trace!(name = %selector.name, %ns, key = %selector.key, "secret reference lookup");
188                let secret = kube::api::Api::<Secret>::namespaced(client.clone(), ns)
189                    .get(&selector.name)
190                    .await?;
191                let result = secret
192                    .string_data
193                    .and_then(|data| data.get(&selector.key).cloned())
194                    .or_else(|| {
195                        secret.data.and_then(|data| {
196                            data.get(&selector.key).and_then(|bytes| {
197                                use base64::prelude::*;
198                                if let Ok(decoded) = String::from_utf8(bytes.0.clone()) {
199                                    trace!("secret reference lookup result string");
200                                    return Some(decoded);
201                                }
202                                if let Some(decoded) = BASE64_STANDARD.decode(&bytes.0).ok().and_then(|decoded| String::from_utf8(decoded).ok()) {
203                                    return Some(decoded);
204                                };
205                                error!(name = %selector.name, %ns, "unable to decode secret reference value as utf8 or base64");
206                                None
207                            })
208                        })
209                    });
210                Ok(result)
211            }
212        }
213    }
214}