142 lines
5.0 KiB
Rust
142 lines
5.0 KiB
Rust
//! 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::<Vec<_>>();
|
|
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<std::path::PathBuf, BackupError>,
|
|
install: impl FnOnce(&SavedStore, &SavedBytes, BackupComplete) -> Result<(), PersistenceError>,
|
|
) -> Result<std::path::PathBuf, AppError> {
|
|
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(),
|
|
}
|
|
}
|