Add domain ownership of reservations and apply knowledge

This commit is contained in:
2026-10-29 23:03:13 +02:00
parent 8413b382b1
commit 2766c6254a
5 changed files with 349 additions and 100 deletions
+85 -48
View File
@@ -81,7 +81,6 @@ pub struct Application {
document: ConfigDocument,
states: ClientStates,
vault: Option<VaultBundle>,
draft_unsupported_reservations: BTreeSet<IpAddr>,
historical_apply: HistoricalApplyStatus,
verified_saved_projection: Option<SavedPeerProjection>,
active_restart_attempt: Option<u64>,
@@ -477,7 +476,11 @@ impl fmt::Debug for Application {
)
.field(
"pending_reservation_count",
&self.draft_unsupported_reservations.len(),
&self
.states
.address_reservations()
.pending_host_routes()
.count(),
)
.field("initial_issue_count", &self.initial_issues.len())
.finish()
@@ -511,7 +514,7 @@ impl Application {
saved: SavedBytes,
mut states: ClientStates,
) -> Result<Self, AppError> {
states.mark_runtime_unknown();
states.begin_runtime_session();
let document = ConfigDocument::parse(&saved.server_config)?;
let vault = saved
.vault
@@ -531,7 +534,6 @@ impl Application {
document,
states,
vault,
draft_unsupported_reservations: BTreeSet::new(),
historical_apply,
verified_saved_projection: None,
active_restart_attempt: None,
@@ -633,13 +635,7 @@ impl Application {
.into_iter()
.collect::<Vec<_>>();
occupied.extend(document_host_addresses(&self.document));
let reserved = self
.states
.reserved_addresses()
.iter()
.copied()
.chain(self.draft_unsupported_reservations.iter().copied())
.collect::<Vec<_>>();
let reserved = self.states.address_reservations().all().collect::<Vec<_>>();
let availability = AddressAvailability {
server: &server_addresses,
occupied: &occupied,
@@ -1268,36 +1264,24 @@ impl Application {
.peers()
.iter()
.any(|peer| peer.identity == Some(identity));
let mut additional_reservations = BTreeSet::new();
let mut next_states = self.states.clone();
if saved_peer_exists && self.states.saved().get(&identity).is_none() {
let peer = saved_document
.peers()
.iter()
.find(|peer| peer.identity == Some(identity))
.ok_or(AppError::DeletedPeerAddressUnknown)?;
for network in &peer.allowed_ips {
match network {
ipnet::IpNet::V4(network) if network.prefix_len() == 32 => {
additional_reservations.insert(IpAddr::V4(network.addr()));
}
ipnet::IpNet::V6(network) if network.prefix_len() == 128 => {
additional_reservations.insert(IpAddr::V6(network.addr()));
}
_ => return Err(AppError::DeletedPeerAddressUnknown),
}
}
if peer.allowed_ips.is_empty() || additional_reservations.is_empty() {
return Err(AppError::DeletedPeerAddressUnknown);
}
next_states
.stage_exact_host_routes(peer.allowed_ips.iter().copied())
.map_err(|_| AppError::DeletedPeerAddressUnknown)?;
}
let edited = self.document.delete_client(&identity)?;
let removed_record = match self.vault.as_mut() {
Some(vault) => vault.remove_record(identity)?,
None => false,
};
self.states.draft_mut().remove(&identity);
self.draft_unsupported_reservations
.extend(additional_reservations);
next_states.draft_mut().remove(&identity);
self.states = next_states;
self.document = edited;
self.invalidate_active_issue_authorizations();
self.initial_issues.remove(&identity);
@@ -1365,11 +1349,10 @@ impl Application {
.map(VaultBundle::from_toml)
.transpose()?;
self.states.discard_draft();
self.draft_unsupported_reservations.clear();
if let Some(bytes) = &self.saved.internal_state {
PersistedState::from_toml(bytes)?.apply_to(&mut self.states)?;
} else {
self.states.mark_runtime_unknown();
self.states.runtime_knowledge_unknown();
}
self.historical_apply = historical_apply_from_saved(&self.saved)?;
self.initial_issues
@@ -1457,7 +1440,12 @@ impl Application {
};
self.states.draft() != self.states.saved()
|| self.document.render() != self.saved.server_config
|| !self.draft_unsupported_reservations.is_empty()
|| self
.states
.address_reservations()
.pending_host_routes()
.next()
.is_some()
|| vault_differs
}
@@ -1708,7 +1696,7 @@ impl Application {
);
}
self.states.mark_runtime_unknown();
self.states.runtime_knowledge_unknown();
let deadline = RestartDeadline::new(now(), settings.restart_timeout);
let execution = service(&settings.systemd_unit, deadline);
let job = execution.outcome;
@@ -1954,7 +1942,6 @@ impl Application {
self.vault = vault;
self.states = states;
self.initial_issues.clear();
self.draft_unsupported_reservations.clear();
self.historical_apply = HistoricalApplyStatus::Unknown;
self.verified_saved_projection = None;
self.active_restart_attempt = None;
@@ -1980,7 +1967,7 @@ impl Application {
return Err(AppError::SavedGenerationChanged);
}
let mut next_states = self.states.clone();
next_states.mark_technically_applied_after_verification(observed_management_snapshot)?;
next_states.prepare_technically_verified_result(observed_management_snapshot)?;
let mut persisted = match &installed.internal_state {
Some(bytes) => PersistedState::from_toml(bytes)?,
None => PersistedState::new(
@@ -2070,17 +2057,6 @@ impl Application {
let mut next_states = self.states.clone();
next_states.commit_draft();
if !self.draft_unsupported_reservations.is_empty() {
let reservations = next_states
.reserved_addresses()
.iter()
.copied()
.chain(self.draft_unsupported_reservations.iter().copied())
.map(|address| address.to_string())
.collect::<Vec<_>>();
let persisted = PersistedState::new(reservations, None)?;
persisted.apply_to(&mut next_states)?;
}
let reservations = next_states
.reserved_addresses()
.iter()
@@ -2133,7 +2109,6 @@ impl Application {
self.saved = proposed;
self.states = next_states;
self.draft_unsupported_reservations.clear();
self.document = ConfigDocument::parse(&self.saved.server_config)?;
self.invalidate_active_issue_authorizations();
promote_matching_issue_map(
@@ -3105,7 +3080,7 @@ restart_timeout_seconds = 30
.expect("saved generation verified");
let mut runtime_unknown = app.states().clone();
runtime_unknown.mark_runtime_unknown();
runtime_unknown.runtime_knowledge_unknown();
let reopened = Application::open(app.store.clone(), runtime_unknown)
.expect("application reopens with unknown runtime");
let destination = reopened
@@ -3681,6 +3656,56 @@ restart_timeout_seconds = 30
assert!(String::from_utf8_lossy(&saved.server_config).contains("# admin note\n"));
}
#[test]
fn unchanged_commit_preserves_verified_apply_knowledge() {
let root = tempfile::tempdir().expect("temp directory");
let (store, states, _, _) = setup(root.path(), false);
let mut app = Application::open(store, states).expect("application opens");
let observed = app.states().saved().clone();
test_mark_verified(&mut app, observed, "2026-10-29T12:00:00Z".to_owned())
.expect("synthetic verification is published");
assert_eq!(app.states().applied(), &ApplyStatus::Verified);
app.commit().expect("unchanged commit completes");
assert_eq!(app.states().applied(), &ApplyStatus::Verified);
assert_eq!(app.states().applied_snapshot(), Some(app.states().saved()));
}
#[test]
fn unchanged_commit_preserves_unavailable_apply_knowledge() {
let root = tempfile::tempdir().expect("temp directory");
let (store, states, _, _) = setup(root.path(), false);
let mut app = Application::open(store, states).expect("application opens");
app.states.runtime_knowledge_unavailable();
app.commit().expect("unchanged commit completes");
assert_eq!(app.states().applied(), &ApplyStatus::Unavailable);
}
#[test]
fn changed_saved_snapshot_demotes_verified_apply_knowledge_on_commit() {
let root = tempfile::tempdir().expect("temp directory");
let (store, states, key, _) = setup(root.path(), false);
let mut app = Application::open(store, states).expect("application opens");
let observed = app.states().saved().clone();
test_mark_verified(
&mut app,
observed.clone(),
"2026-10-29T12:00:00Z".to_owned(),
)
.expect("synthetic verification is published");
app.rename_client(key, "changed saved peer")
.expect("peer change is staged");
app.commit().expect("changed commit completes");
assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert_eq!(app.states().applied_snapshot(), Some(&observed));
assert_ne!(app.states().saved(), &observed);
}
#[test]
fn discard_restores_draft_without_writing_installed_bytes() {
let root = tempfile::tempdir().expect("temp directory");
@@ -4976,6 +5001,18 @@ restart_timeout_seconds = 30
assert_eq!(reopened.states().applied(), &ApplyStatus::Unknown);
}
#[test]
fn open_does_not_treat_caller_seeded_verified_state_as_runtime_evidence() {
let root = tempfile::tempdir().expect("temp directory");
let (store, seeded, _, _) = setup(root.path(), false);
assert_eq!(seeded.applied(), &ApplyStatus::Verified);
let app = Application::open(store, seeded).expect("application opens");
assert_eq!(app.states().applied(), &ApplyStatus::Unknown);
assert!(app.states().applied_snapshot().is_some());
assert!(!app.saved_generation_is_verified_applied(app.saved_bytes(), app.document()));
}
#[test]
fn failed_verified_history_write_does_not_advance_memory_or_reopen_as_verified() {
let root = tempfile::tempdir().expect("temp directory");