//! Saved-state Commit scenario. //! //! The ordering is deliberately explicit: prepare and validate the complete //! candidate, archive the installed generation, install it through the //! journaled store, then publish only already-prepared in-memory values. use std::net::IpAddr; use crate::backup::{BackupError, BackupReason, HistoricalApplyStatus, SavedBytes}; use crate::domain::ClientStates; use crate::persistence::{BackupComplete, PersistedState, PersistenceError, SavedStore}; use crate::vault::VaultBundle; use crate::wireguard::ConfigDocument; use super::{AppError, Application, historical_apply_from_saved}; struct PreparedCommit { proposed: SavedBytes, document: ConfigDocument, states: ClientStates, } impl Application { /// Prepare a candidate without changing the live session or installed /// files. All parsing, serialization and store validation finish here. fn prepare_commit( &self, installed: &SavedBytes, ) -> Result<(SavedBytes, ConfigDocument, ClientStates), AppError> { if let Some(existing_bytes) = &installed.vault { let existing_text = std::str::from_utf8(existing_bytes).map_err(|_| AppError::InvalidSavedState)?; let existing = VaultBundle::from_toml(existing_text)?; if self .draft .vault .as_ref() .is_none_or(|vault| existing.recipient() != vault.recipient()) { return Err(AppError::VaultIdentityChange); } } let mut states = self.draft.states.clone(); states.commit_draft(); let reservations = states .reserved_addresses() .iter() .map(IpAddr::to_string) .collect::>(); let internal_state = Some(PersistedState::new(reservations, None)?.to_toml()?); let vault = self .draft .vault .as_ref() .map(VaultBundle::to_toml) .transpose()? .map(String::into_bytes); let proposed = SavedBytes { settings: installed.settings.clone(), server_config: self.draft.document.render().to_vec(), internal_state, vault, }; self.store.validate_saved(&proposed)?; // Reuse this parsed document after install; publication must not have a // fallible parse step after the filesystem has already advanced. let document = ConfigDocument::parse(&proposed.server_config)?; Ok((proposed, document, states)) } } pub(super) fn commit( application: &mut Application, create_backup: impl FnOnce( &std::path::Path, BackupReason, HistoricalApplyStatus, &SavedBytes, ) -> Result, install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>, ) -> Result { let installed = application.store.read_saved()?; let historical_apply = historical_apply_from_saved(&installed)?; let (proposed, document, states) = application.prepare_commit(&installed)?; let prepared = PreparedCommit { proposed, document, states, }; // Passing BackupComplete is the only path to installation, and this point // is reached only after the full archive has been published successfully. let archive = create_backup( application.store.archives_directory(), BackupReason::PreCommit, historical_apply, &installed, )?; if let Err(error) = install(&application.store, &prepared.proposed, BackupComplete(())) { retain_first_issue_rights_after_failed_install(application); return Err(error.into()); } application.saved = prepared.proposed; application.draft.states = prepared.states; application.draft.document = prepared.document; application .first_delivery .invalidate_active_authorizations(); application.first_delivery.promote_matching( &application.draft.document, application.draft.vault.as_ref(), ); application.historical_apply = HistoricalApplyStatus::Unknown; application.verified_saved_projection = None; application.active_restart_attempt = None; Ok(archive) } fn retain_first_issue_rights_after_failed_install(application: &mut Application) { match application.store.read_saved() { Ok(current) => { let document = ConfigDocument::parse(¤t.server_config).ok(); let vault = current .vault .as_deref() .and_then(|bytes| std::str::from_utf8(bytes).ok()) .and_then(|text| VaultBundle::from_toml(text).ok()); if let Some(document) = document { application .first_delivery .retain_matching(&document, vault.as_ref()); } else { application.first_delivery.clear(); } } Err(_) => application.first_delivery.clear(), } }