Add domain ownership of reservations and apply knowledge
This commit is contained in:
+85
-48
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user