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#[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#[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 pub name: String,
73 #[serde(rename = "type")]
75 pub ty: Option<RecordType>,
76 pub content: StringOrService,
78 pub ttl: Option<i64>,
80 pub proxied: Option<bool>,
82 pub comment: Option<String>,
84 pub tags: Option<Vec<String>>,
86 pub zone: ZoneNameOrId,
88}
89
90impl CloudflareDNSRecordSpec {
91 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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
111pub struct CloudflareDNSRecordStatus {
112 pub record_id: String,
114 pub zone_id: String,
116 pub pending: bool,
119 pub conditions: Option<Vec<Condition>>,
121}
122
123#[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 pub name: String,
144 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}