Implement readable saved-state Commit and restore scenarios
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! 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, promote_matching_issue_map,
|
||||
retain_matching_issue_map,
|
||||
};
|
||||
|
||||
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.invalidate_active_issue_authorizations();
|
||||
promote_matching_issue_map(
|
||||
&mut application.initial_issues,
|
||||
&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 {
|
||||
retain_matching_issue_map(
|
||||
&mut application.initial_issues,
|
||||
&document,
|
||||
vault.as_ref(),
|
||||
);
|
||||
} else {
|
||||
application.initial_issues.clear();
|
||||
}
|
||||
}
|
||||
Err(_) => application.initial_issues.clear(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user