external_dns_sdk/
lib.rs

1#[cfg(feature = "client")]
2mod client;
3
4#[cfg(feature = "client")]
5pub use client::{Client, Error};
6
7#[cfg(feature = "provider")]
8mod provider;
9use kubizone_common::{DomainName, Type};
10#[cfg(feature = "provider")]
11pub use provider::{serve, Provider};
12
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18struct DomainFilter {
19    #[serde(default)]
20    pub filters: Vec<String>,
21}
22
23/// Uniquely identifiable parts of an Endpoint.
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
25#[serde(rename_all = "camelCase")]
26pub struct EndpointIdent {
27    pub dns_name: DomainName,
28    pub record_type: Type,
29}
30
31/// Domain and record type with one or more "targets" (values).
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase")]
34pub struct Endpoint {
35    /// Uniquely identifiable record name.
36    #[serde(flatten)]
37    pub identity: EndpointIdent,
38
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub set_identifier: Option<String>,
41
42    /// One or more "targets", that is the R-data values returned
43    /// when this record is queried.
44    pub targets: Vec<String>,
45
46    /// Time-To-Live.
47    #[serde(default, rename = "recordTTL", skip_serializing_if = "Option::is_none")]
48    pub record_ttl: Option<i64>,
49
50    /// One or more labels associated with the record, if
51    /// supported by the underlying provider.
52    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53    pub labels: HashMap<String, String>,
54
55    /// Provider-specific properties associated with the endpoint.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub provider_specific: Vec<ProviderSpecificProperty>,
58}
59
60/// Provider-specific properties associated with an [`Endpoint`]
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62#[serde(rename_all = "camelCase")]
63pub struct ProviderSpecificProperty {
64    /// Name of the property.
65    pub name: String,
66
67    /// Value of the property.
68    pub value: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73struct Changes {
74    pub create: Vec<Endpoint>,
75    pub update_old: Vec<Endpoint>,
76    pub update_new: Vec<Endpoint>,
77    pub delete: Vec<Endpoint>,
78}
79
80/// Change to apply to the record set held by the provider.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Change {
83    /// Update the `old` endpoint to match the `new` one.
84    Update {
85        /// Existing endpoint which should be updated.
86        old: Endpoint,
87        new: Endpoint,
88    },
89
90    /// Delete the contained endpoint.
91    Delete(Endpoint),
92
93    /// Create a new endpoint.
94    Create(Endpoint),
95}
96
97impl From<Changes> for Vec<Change> {
98    fn from(changes: Changes) -> Self {
99        let mut out = Vec::new();
100
101        for endpoint in changes.delete {
102            out.push(Change::Delete(endpoint));
103        }
104
105        for old in changes.update_old {
106            if let Some(new) = changes
107                .update_new
108                .iter()
109                .find(|new| new.identity == old.identity)
110                .cloned()
111            {
112                out.push(Change::Update { old, new })
113            }
114        }
115
116        for endpoint in changes.create {
117            out.push(Change::Create(endpoint))
118        }
119
120        out
121    }
122}
123
124impl From<Vec<Change>> for Changes {
125    fn from(value: Vec<Change>) -> Self {
126        let mut out = Changes {
127            create: vec![],
128            update_old: vec![],
129            update_new: vec![],
130            delete: vec![],
131        };
132
133        for change in value {
134            match change {
135                Change::Update { old, new } => {
136                    out.update_old.push(old);
137                    out.update_new.push(new);
138                }
139                Change::Delete(endpoint) => out.delete.push(endpoint),
140                Change::Create(endpoint) => out.create.push(endpoint),
141            }
142        }
143
144        out
145    }
146}
147
148/// Utility trait for computing a differential from two lists of endpoints.
149pub trait EndpointDiff {
150    /// Compare current (self) and desired (other) state, and compute a list of
151    /// changes to move from one state to the other.
152    ///
153    /// * Endpoints contained in `self` but not in `other` will yield a [`Change::Delete`].
154    /// * Endpoints contained in `other` but not in `self` will yield a [`Change::Create`].
155    /// * Endpoints contained in both `self` and `other` will yield a [`Change::Update`],
156    ///     *if* the entries are not identical.
157    fn difference(self, other: Self) -> Vec<Change>;
158}
159
160impl EndpointDiff for Vec<Endpoint> {
161    fn difference(self, other: Self) -> Vec<Change> {
162        let old: HashMap<EndpointIdent, Endpoint> = HashMap::from_iter(
163            self.into_iter()
164                .map(|endpoint| (endpoint.identity.clone(), endpoint)),
165        );
166        let new: HashMap<EndpointIdent, Endpoint> = HashMap::from_iter(
167            other
168                .into_iter()
169                .map(|endpoint| (endpoint.identity.clone(), endpoint)),
170        );
171
172        let old_keys: HashSet<_> = old.keys().collect();
173        let new_keys: HashSet<_> = new.keys().collect();
174
175        let creates = new_keys
176            .difference(&old_keys)
177            .filter_map(|identity| new.get(identity))
178            .cloned()
179            .map(Change::Create);
180
181        let deletes = old_keys
182            .difference(&new_keys)
183            .filter_map(|identity| old.get(identity))
184            .cloned()
185            .map(Change::Delete);
186
187        let updates = old_keys.intersection(&new_keys).filter_map(|identity| {
188            let old = old.get(identity)?.clone();
189            let new = new.get(identity)?.clone();
190
191            if old == new {
192                return None;
193            }
194
195            Some(Change::Update { old, new })
196        });
197
198        deletes.into_iter().chain(updates).chain(creates).collect()
199    }
200}
201
202#[cfg(test)]
203#[test]
204fn difference_calculation() {
205    let a = vec![
206        Endpoint {
207            identity: EndpointIdent {
208                dns_name: DomainName::try_from("update.org.").unwrap(),
209                record_type: Type::A,
210            },
211            set_identifier: None,
212            targets: vec!["192.168.0.1".to_string()],
213            record_ttl: Some(300),
214            labels: HashMap::default(),
215            provider_specific: Vec::new(),
216        },
217        Endpoint {
218            identity: EndpointIdent {
219                dns_name: DomainName::try_from("delete.org.").unwrap(),
220                record_type: Type::A,
221            },
222            set_identifier: None,
223            targets: vec!["192.168.0.1".to_string()],
224            record_ttl: Some(300),
225            labels: HashMap::default(),
226            provider_specific: Vec::new(),
227        },
228    ];
229
230    let b = vec![
231        Endpoint {
232            identity: EndpointIdent {
233                dns_name: DomainName::try_from("update.org.").unwrap(),
234                record_type: Type::A,
235            },
236            set_identifier: None,
237            targets: vec!["192.168.0.2".to_string()],
238            record_ttl: Some(300),
239            labels: HashMap::default(),
240            provider_specific: Vec::new(),
241        },
242        Endpoint {
243            identity: EndpointIdent {
244                dns_name: DomainName::try_from("create.org.").unwrap(),
245                record_type: Type::A,
246            },
247            set_identifier: None,
248            targets: vec!["192.168.0.1".to_string()],
249            record_ttl: Some(300),
250            labels: HashMap::default(),
251            provider_specific: Vec::new(),
252        },
253    ];
254
255    let changes = a.difference(b);
256
257    assert_eq!(
258        changes,
259        vec![
260            Change::Delete(Endpoint {
261                identity: EndpointIdent {
262                    dns_name: DomainName::try_from("delete.org.").unwrap(),
263                    record_type: Type::A,
264                },
265                set_identifier: None,
266                targets: vec!["192.168.0.1".to_string()],
267                record_ttl: Some(300),
268                labels: HashMap::default(),
269                provider_specific: Vec::new(),
270            }),
271            Change::Update {
272                old: Endpoint {
273                    identity: EndpointIdent {
274                        dns_name: DomainName::try_from("update.org.").unwrap(),
275                        record_type: Type::A,
276                    },
277                    set_identifier: None,
278                    targets: vec!["192.168.0.1".to_string()],
279                    record_ttl: Some(300),
280                    labels: HashMap::default(),
281                    provider_specific: Vec::new(),
282                },
283                new: Endpoint {
284                    identity: EndpointIdent {
285                        dns_name: DomainName::try_from("update.org.").unwrap(),
286                        record_type: Type::A,
287                    },
288                    set_identifier: None,
289                    targets: vec!["192.168.0.2".to_string()],
290                    record_ttl: Some(300),
291                    labels: HashMap::default(),
292                    provider_specific: Vec::new(),
293                }
294            },
295            Change::Create(Endpoint {
296                identity: EndpointIdent {
297                    dns_name: DomainName::try_from("create.org.").unwrap(),
298                    record_type: Type::A,
299                },
300                set_identifier: None,
301                targets: vec!["192.168.0.1".to_string()],
302                record_ttl: Some(300),
303                labels: HashMap::default(),
304                provider_specific: Vec::new(),
305            })
306        ]
307    )
308}