Skip to content

Commit 1e76aca

Browse files
authored
update internal DNS during blueprint execution (#4989)
1 parent 915276d commit 1e76aca

File tree

34 files changed

+1975
-381
lines changed

34 files changed

+1975
-381
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clients/dns-service-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
license = "MPL-2.0"
66

77
[dependencies]
8+
anyhow.workspace = true
89
chrono.workspace = true
910
http.workspace = true
1011
progenitor.workspace = true
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use crate::types::DnsConfigParams;
6+
use crate::types::DnsRecord;
7+
use crate::DnsRecords;
8+
use anyhow::ensure;
9+
use anyhow::Context;
10+
11+
/// Compare the DNS records contained in two sets of DNS configuration
12+
#[derive(Debug)]
13+
pub struct DnsDiff<'a> {
14+
left: &'a DnsRecords,
15+
right: &'a DnsRecords,
16+
}
17+
18+
impl<'a> DnsDiff<'a> {
19+
/// Compare the DNS records contained in two sets of DNS configuration
20+
///
21+
/// Both configurations are expected to contain exactly one zone and they
22+
/// should have the same name.
23+
pub fn new(
24+
left: &'a DnsConfigParams,
25+
right: &'a DnsConfigParams,
26+
) -> Result<DnsDiff<'a>, anyhow::Error> {
27+
let left_zone = left.sole_zone().context("left side of diff")?;
28+
let right_zone = right.sole_zone().context("right side of diff")?;
29+
30+
ensure!(
31+
left_zone.zone_name == right_zone.zone_name,
32+
"cannot compare DNS configuration from zones with different names: \
33+
{:?} vs. {:?}", left_zone.zone_name, right_zone.zone_name,
34+
);
35+
36+
Ok(DnsDiff { left: &left_zone.records, right: &right_zone.records })
37+
}
38+
39+
/// Iterate over the names that are present in the `right` config but
40+
/// absent in the `left` one (i.e., added between `left` and `right`)
41+
pub fn names_added(&self) -> impl Iterator<Item = (&str, &[DnsRecord])> {
42+
self.right
43+
.iter()
44+
.filter(|(k, _)| !self.left.contains_key(*k))
45+
.map(|(k, v)| (k.as_ref(), v.as_ref()))
46+
}
47+
48+
/// Iterate over the names that are present in the `left` config but
49+
/// absent in the `right` one (i.e., removed between `left` and `right`)
50+
pub fn names_removed(&self) -> impl Iterator<Item = (&str, &[DnsRecord])> {
51+
self.left
52+
.iter()
53+
.filter(|(k, _)| !self.right.contains_key(*k))
54+
.map(|(k, v)| (k.as_ref(), v.as_ref()))
55+
}
56+
57+
/// Iterate over the names whose records changed between `left` and `right`.
58+
pub fn names_changed(
59+
&self,
60+
) -> impl Iterator<Item = (&str, &[DnsRecord], &[DnsRecord])> {
61+
self.left.iter().filter_map(|(k, v1)| match self.right.get(k) {
62+
Some(v2) if v1 != v2 => {
63+
Some((k.as_ref(), v1.as_ref(), v2.as_ref()))
64+
}
65+
_ => None,
66+
})
67+
}
68+
69+
/// Returns true iff there are no differences in the DNS names and records
70+
/// described by the given configurations
71+
pub fn is_empty(&self) -> bool {
72+
self.names_added().next().is_none()
73+
&& self.names_removed().next().is_none()
74+
&& self.names_changed().next().is_none()
75+
}
76+
}
77+
78+
#[cfg(test)]
79+
mod test {
80+
use super::DnsDiff;
81+
use crate::types::DnsConfigParams;
82+
use crate::types::DnsConfigZone;
83+
use crate::types::DnsRecord;
84+
use chrono::Utc;
85+
use std::collections::HashMap;
86+
use std::net::Ipv4Addr;
87+
88+
const ZONE_NAME: &str = "dummy";
89+
90+
fn example() -> DnsConfigParams {
91+
DnsConfigParams {
92+
generation: 4,
93+
time_created: Utc::now(),
94+
zones: vec![DnsConfigZone {
95+
zone_name: ZONE_NAME.to_string(),
96+
records: HashMap::from([
97+
(
98+
"ex1".to_string(),
99+
vec![DnsRecord::A(Ipv4Addr::LOCALHOST)],
100+
),
101+
(
102+
"ex2".to_string(),
103+
vec![DnsRecord::A("192.168.1.3".parse().unwrap())],
104+
),
105+
]),
106+
}],
107+
}
108+
}
109+
110+
#[test]
111+
fn diff_invalid() {
112+
let example_empty = DnsConfigParams {
113+
generation: 3,
114+
time_created: Utc::now(),
115+
zones: vec![],
116+
};
117+
118+
// Configs must have at least one zone.
119+
let error = DnsDiff::new(&example_empty, &example_empty)
120+
.expect_err("unexpectedly succeeded comparing two empty configs");
121+
assert!(
122+
format!("{:#}", error).contains("expected exactly one DNS zone")
123+
);
124+
125+
let example = example();
126+
let error = DnsDiff::new(&example_empty, &example)
127+
.expect_err("unexpectedly succeeded comparing an empty config");
128+
assert!(
129+
format!("{:#}", error).contains("expected exactly one DNS zone")
130+
);
131+
132+
// Configs must not have more than one zone.
133+
let example_multiple = DnsConfigParams {
134+
generation: 3,
135+
time_created: Utc::now(),
136+
zones: vec![
137+
DnsConfigZone {
138+
zone_name: ZONE_NAME.to_string(),
139+
records: HashMap::new(),
140+
},
141+
DnsConfigZone {
142+
zone_name: "two".to_string(),
143+
records: HashMap::new(),
144+
},
145+
],
146+
};
147+
let error = DnsDiff::new(&example_multiple, &example).expect_err(
148+
"unexpectedly succeeded comparing config with multiple zones",
149+
);
150+
assert!(
151+
format!("{:#}", error).contains("expected exactly one DNS zone")
152+
);
153+
154+
// Cannot compare different zone names
155+
let example_different_zone = DnsConfigParams {
156+
generation: 3,
157+
time_created: Utc::now(),
158+
zones: vec![DnsConfigZone {
159+
zone_name: format!("{}-other", ZONE_NAME),
160+
records: HashMap::new(),
161+
}],
162+
};
163+
let error = DnsDiff::new(&example_different_zone, &example).expect_err(
164+
"unexpectedly succeeded comparing configs with \
165+
different zone names",
166+
);
167+
assert_eq!(
168+
format!("{:#}", error),
169+
"cannot compare DNS configuration from zones with different \
170+
names: \"dummy-other\" vs. \"dummy\""
171+
);
172+
}
173+
174+
#[test]
175+
fn diff_equivalent() {
176+
let example = example();
177+
let diff = DnsDiff::new(&example, &example).unwrap();
178+
assert!(diff.is_empty());
179+
assert_eq!(diff.names_removed().count(), 0);
180+
assert_eq!(diff.names_added().count(), 0);
181+
assert_eq!(diff.names_changed().count(), 0);
182+
}
183+
184+
#[test]
185+
fn diff_different() {
186+
let example = example();
187+
let example2 = DnsConfigParams {
188+
generation: 4,
189+
time_created: Utc::now(),
190+
zones: vec![DnsConfigZone {
191+
zone_name: ZONE_NAME.to_string(),
192+
records: HashMap::from([
193+
(
194+
"ex2".to_string(),
195+
vec![DnsRecord::A("192.168.1.4".parse().unwrap())],
196+
),
197+
(
198+
"ex3".to_string(),
199+
vec![DnsRecord::A(std::net::Ipv4Addr::LOCALHOST)],
200+
),
201+
]),
202+
}],
203+
};
204+
205+
let diff = DnsDiff::new(&example, &example2).unwrap();
206+
assert!(!diff.is_empty());
207+
208+
let removed = diff.names_removed().collect::<Vec<_>>();
209+
assert_eq!(removed.len(), 1);
210+
assert_eq!(removed[0].0, "ex1");
211+
assert_eq!(removed[0].1, vec![DnsRecord::A(Ipv4Addr::LOCALHOST)]);
212+
213+
let added = diff.names_added().collect::<Vec<_>>();
214+
assert_eq!(added.len(), 1);
215+
assert_eq!(added[0].0, "ex3");
216+
assert_eq!(added[0].1, vec![DnsRecord::A(Ipv4Addr::LOCALHOST)]);
217+
218+
let changed = diff.names_changed().collect::<Vec<_>>();
219+
assert_eq!(changed.len(), 1);
220+
assert_eq!(changed[0].0, "ex2");
221+
assert_eq!(
222+
changed[0].1,
223+
vec![DnsRecord::A("192.168.1.3".parse().unwrap())]
224+
);
225+
assert_eq!(
226+
changed[0].2,
227+
vec![DnsRecord::A("192.168.1.4".parse().unwrap())]
228+
);
229+
}
230+
}

clients/dns-service-client/src/lib.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
mod diff;
6+
7+
use crate::Error as DnsConfigError;
8+
use anyhow::ensure;
9+
pub use diff::DnsDiff;
10+
use std::collections::HashMap;
11+
512
progenitor::generate_api!(
613
spec = "../../openapi/dns-server.json",
714
inner_type = slog::Logger,
8-
derives = [schemars::JsonSchema, Eq, PartialEq],
15+
derives = [schemars::JsonSchema, Clone, Eq, PartialEq],
916
pre_hook = (|log: &slog::Logger, request: &reqwest::Request| {
1017
slog::debug!(log, "client request";
1118
"method" => %request.method(),
@@ -22,8 +29,6 @@ pub const ERROR_CODE_UPDATE_IN_PROGRESS: &'static str = "UpdateInProgress";
2229
pub const ERROR_CODE_BAD_UPDATE_GENERATION: &'static str =
2330
"BadUpdateGeneration";
2431

25-
use crate::Error as DnsConfigError;
26-
2732
/// Returns whether an error from this client should be retried
2833
pub fn is_retryable(error: &DnsConfigError<crate::types::Error>) -> bool {
2934
let response_value = match error {
@@ -84,3 +89,23 @@ pub fn is_retryable(error: &DnsConfigError<crate::types::Error>) -> bool {
8489

8590
false
8691
}
92+
93+
type DnsRecords = HashMap<String, Vec<types::DnsRecord>>;
94+
95+
impl types::DnsConfigParams {
96+
/// Given a high-level DNS configuration, return a reference to its sole
97+
/// DNS zone.
98+
///
99+
/// # Errors
100+
///
101+
/// Returns an error if there are 0 or more than one zones in this
102+
/// configuration.
103+
pub fn sole_zone(&self) -> Result<&types::DnsConfigZone, anyhow::Error> {
104+
ensure!(
105+
self.zones.len() == 1,
106+
"expected exactly one DNS zone, but found {}",
107+
self.zones.len()
108+
);
109+
Ok(&self.zones[0])
110+
}
111+
}

common/src/api/external/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,12 @@ impl From<&Generation> for i64 {
679679
}
680680
}
681681

682+
impl From<Generation> for u64 {
683+
fn from(g: Generation) -> Self {
684+
g.0
685+
}
686+
}
687+
682688
impl From<u32> for Generation {
683689
fn from(value: u32) -> Self {
684690
Generation(u64::from(value))

dev-tools/omdb/src/bin/omdb/nexus.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ async fn cmd_nexus_blueprints_diff(
870870
let b2 = client.blueprint_view(&args.blueprint2_id).await.with_context(
871871
|| format!("fetching blueprint {}", args.blueprint2_id),
872872
)?;
873-
println!("{}", b1.diff(&b2));
873+
println!("{}", b1.diff_sleds(&b2));
874874
Ok(())
875875
}
876876

0 commit comments

Comments
 (0)