2026-07-17 07:54:46 +09:00
|
|
|
use std::ffi::OsString;
|
|
|
|
|
use std::fmt;
|
|
|
|
|
use std::os::unix::ffi::OsStringExt;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
use lazaret_core::configuration::ResolvedDomainConfiguration;
|
|
|
|
|
use lazaret_core::records::{
|
|
|
|
|
DurableDecision, ParticipantState, SessionPhase, SessionTerminalState,
|
|
|
|
|
};
|
|
|
|
|
pub use lazaret_state_anchor::StateAnchorEntryKind;
|
|
|
|
|
use lazaret_state_anchor::{
|
|
|
|
|
StateAnchorEntry, StateAnchorInspection, StateAnchorLayout, StateAnchorResetScope,
|
2026-07-17 08:05:17 +09:00
|
|
|
require_no_orphan_reset_quarantine, require_state_anchor_absent,
|
2026-07-17 07:54:46 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use crate::domain_store::{HostCoordinatorDomainStore, StateResetIntentV1, StateResetScopeV1};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct StateMaintenanceError(String);
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for StateMaintenanceError {
|
|
|
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
formatter.write_str(&self.0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for StateMaintenanceError {}
|
|
|
|
|
|
|
|
|
|
fn failure(error: impl fmt::Display) -> StateMaintenanceError {
|
|
|
|
|
StateMaintenanceError(error.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct StateAnchorReport {
|
|
|
|
|
pub container_path: String,
|
|
|
|
|
pub changes: Vec<StateAnchorEntry>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
|
pub struct DomainStateReport {
|
|
|
|
|
pub profile: String,
|
|
|
|
|
pub workspace: Vec<u8>,
|
|
|
|
|
pub domain_id: [u8; 32],
|
|
|
|
|
pub anchors: Vec<StateAnchorReport>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 08:05:17 +09:00
|
|
|
/// Returns an empty report only after proving that no physical State Anchor exists.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses changed authority, links, inaccessible paths, or any persistent or runtime anchor.
|
|
|
|
|
pub fn inspect_absent_domain_state(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
) -> Result<DomainStateReport, StateMaintenanceError> {
|
|
|
|
|
require_state_anchor_absent(configuration).map_err(failure)?;
|
|
|
|
|
Ok(report(
|
|
|
|
|
configuration,
|
|
|
|
|
&StateAnchorInspection {
|
|
|
|
|
entries: Vec::new(),
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 07:54:46 +09:00
|
|
|
/// Recovers a durable reset before any maintenance observer or runtime mount can expose state.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses malformed intent, changed authority, unsupported scope, orphan quarantine, or cleanup
|
|
|
|
|
/// failure.
|
|
|
|
|
pub fn recover_state_reset(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
store: &HostCoordinatorDomainStore,
|
|
|
|
|
) -> Result<(), StateMaintenanceError> {
|
|
|
|
|
let Some(intent) = store.state_reset_intent().map_err(failure)? else {
|
|
|
|
|
return require_no_orphan_reset_quarantine(configuration).map_err(failure);
|
|
|
|
|
};
|
|
|
|
|
require_intent(configuration, &intent)?;
|
|
|
|
|
let layout = StateAnchorLayout::create(configuration).map_err(failure)?;
|
|
|
|
|
complete_intent(&layout, &intent)?;
|
|
|
|
|
store.finish_state_reset(&intent).map_err(failure)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns content-free State Anchor metadata after mandatory recovery and structural checks.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses active or malformed Domain state, changed authority, mounts, links in fixed paths, and
|
|
|
|
|
/// unsupported upper-layer entries.
|
|
|
|
|
pub fn inspect_domain_state(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
store: &HostCoordinatorDomainStore,
|
|
|
|
|
) -> Result<DomainStateReport, StateMaintenanceError> {
|
|
|
|
|
recover_state_reset(configuration, store)?;
|
|
|
|
|
require_inactive(store)?;
|
|
|
|
|
let layout = StateAnchorLayout::create(configuration).map_err(failure)?;
|
|
|
|
|
let inspection = layout.inspect_state().map_err(failure)?;
|
|
|
|
|
Ok(report(configuration, &inspection))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Commits and completes one all-or-selected State Anchor reset generation.
|
|
|
|
|
///
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses active state, an arbitrary target, malformed durable state, changed authority, mounts,
|
|
|
|
|
/// links in fixed paths, quarantine collisions, and unsupported upper-layer entries.
|
|
|
|
|
pub fn reset_domain_state(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
store: &HostCoordinatorDomainStore,
|
|
|
|
|
scope: StateResetScopeV1,
|
|
|
|
|
) -> Result<DomainStateReport, StateMaintenanceError> {
|
|
|
|
|
recover_state_reset(configuration, store)?;
|
|
|
|
|
require_inactive(store)?;
|
|
|
|
|
validate_scope(configuration, &scope)?;
|
|
|
|
|
let intent = StateResetIntentV1::new(configuration.domain_id(), scope);
|
|
|
|
|
store.begin_state_reset(&intent).map_err(failure)?;
|
|
|
|
|
let layout = StateAnchorLayout::create(configuration).map_err(failure)?;
|
|
|
|
|
complete_intent(&layout, &intent)?;
|
|
|
|
|
store.finish_state_reset(&intent).map_err(failure)?;
|
|
|
|
|
inspect_domain_state(configuration, store)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn complete_intent(
|
|
|
|
|
layout: &StateAnchorLayout<'_>,
|
|
|
|
|
intent: &StateResetIntentV1,
|
|
|
|
|
) -> Result<(), StateMaintenanceError> {
|
|
|
|
|
match intent.scope() {
|
|
|
|
|
StateResetScopeV1::All => layout
|
|
|
|
|
.complete_state_reset(StateAnchorResetScope::All)
|
|
|
|
|
.map_err(failure),
|
|
|
|
|
StateResetScopeV1::Selected(path) => {
|
|
|
|
|
let path = PathBuf::from(OsString::from_vec(path.clone()));
|
|
|
|
|
layout
|
|
|
|
|
.complete_state_reset(StateAnchorResetScope::Selected(&path))
|
|
|
|
|
.map_err(failure)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_intent(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
intent: &StateResetIntentV1,
|
|
|
|
|
) -> Result<(), StateMaintenanceError> {
|
|
|
|
|
if intent.domain_id() != configuration.domain_id() {
|
|
|
|
|
return Err(failure(
|
|
|
|
|
"reset intent Domain differs from resolved authority",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
validate_scope(configuration, intent.scope())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn validate_scope(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
scope: &StateResetScopeV1,
|
|
|
|
|
) -> Result<(), StateMaintenanceError> {
|
|
|
|
|
let StateResetScopeV1::Selected(bytes) = scope else {
|
|
|
|
|
return Ok(());
|
|
|
|
|
};
|
|
|
|
|
let selected = PathBuf::from(OsString::from_vec(bytes.clone()));
|
|
|
|
|
if configuration
|
|
|
|
|
.fingerprint_record()
|
|
|
|
|
.persistent_dirs
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|configured| Path::new(configured) == selected)
|
|
|
|
|
{
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(failure(
|
|
|
|
|
"reset target is not an exact configured persistent container path",
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_inactive(store: &HostCoordinatorDomainStore) -> Result<(), StateMaintenanceError> {
|
|
|
|
|
let snapshot = store.snapshot().map_err(failure)?;
|
|
|
|
|
let inactive = snapshot
|
|
|
|
|
.generations
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|generation| generation.durable_decision == DurableDecision::Clean)
|
|
|
|
|
&& snapshot
|
|
|
|
|
.participants
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|participant| participant.state() == ParticipantState::Clean)
|
|
|
|
|
&& snapshot.sessions.iter().all(|session| {
|
|
|
|
|
matches!(
|
|
|
|
|
session.session_phase,
|
|
|
|
|
SessionPhase::Closed | SessionPhase::Failed
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
&& snapshot
|
|
|
|
|
.session_terminals
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|terminal| terminal.state() == SessionTerminalState::Clean)
|
|
|
|
|
&& snapshot
|
|
|
|
|
.mailboxes
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|mailbox| mailbox.state == ParticipantState::Clean);
|
|
|
|
|
if inactive {
|
|
|
|
|
Ok(())
|
|
|
|
|
} else {
|
|
|
|
|
Err(failure("Domain is active or has unfinished runtime state"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn report(
|
|
|
|
|
configuration: &ResolvedDomainConfiguration,
|
|
|
|
|
inspection: &StateAnchorInspection,
|
|
|
|
|
) -> DomainStateReport {
|
|
|
|
|
let anchors = configuration
|
|
|
|
|
.fingerprint_record()
|
|
|
|
|
.persistent_dirs
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|container_path| {
|
|
|
|
|
let relative = container_path.trim_start_matches('/').as_bytes();
|
|
|
|
|
let path = Path::new(container_path.trim_start_matches('/'));
|
|
|
|
|
let mut whiteout = path
|
|
|
|
|
.parent()
|
|
|
|
|
.unwrap_or(Path::new(""))
|
|
|
|
|
.as_os_str()
|
|
|
|
|
.as_encoded_bytes()
|
|
|
|
|
.to_vec();
|
|
|
|
|
if !whiteout.is_empty() {
|
|
|
|
|
whiteout.push(b'/');
|
|
|
|
|
}
|
|
|
|
|
whiteout.extend_from_slice(b".wh.");
|
|
|
|
|
whiteout.extend_from_slice(
|
|
|
|
|
path.file_name()
|
|
|
|
|
.expect("validated persistent path has a name")
|
|
|
|
|
.as_encoded_bytes(),
|
|
|
|
|
);
|
|
|
|
|
let changes = inspection
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|entry| {
|
|
|
|
|
entry.path == relative
|
|
|
|
|
|| entry.path == whiteout
|
|
|
|
|
|| entry
|
|
|
|
|
.path
|
|
|
|
|
.strip_prefix(relative)
|
|
|
|
|
.is_some_and(|suffix| suffix.first() == Some(&b'/'))
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect();
|
|
|
|
|
StateAnchorReport {
|
|
|
|
|
container_path: container_path.clone(),
|
|
|
|
|
changes,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
DomainStateReport {
|
|
|
|
|
profile: configuration.selected_profile().to_owned(),
|
|
|
|
|
workspace: configuration
|
|
|
|
|
.workspace()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.to_vec(),
|
|
|
|
|
domain_id: *configuration.domain_id().as_bytes(),
|
|
|
|
|
anchors,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::fs::{self, File, OpenOptions};
|
|
|
|
|
use std::os::unix::ffi::OsStringExt;
|
|
|
|
|
use std::os::unix::fs::symlink;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
use lazaret_core::configuration::{
|
|
|
|
|
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_core::identity::{
|
|
|
|
|
ConfigurationFingerprint, CoordinatorId, GenerationId, NetworkSidecarId, SandboxId,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_core::records::{Domain, Generation};
|
|
|
|
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
2026-07-18 22:35:48 +09:00
|
|
|
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
|
2026-07-17 07:54:46 +09:00
|
|
|
use rustix::fs::{FileType, Mode, XattrFlags, fsetxattr, makedev, mknodat};
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
enum EntryCase {
|
|
|
|
|
Directory,
|
|
|
|
|
File,
|
|
|
|
|
Symlink,
|
|
|
|
|
WhiteoutFile,
|
|
|
|
|
WhiteoutDevice,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn every_reset_boundary_recovers_to_new_empty() {
|
|
|
|
|
for entry_case in [
|
|
|
|
|
EntryCase::Directory,
|
|
|
|
|
EntryCase::File,
|
|
|
|
|
EntryCase::Symlink,
|
|
|
|
|
EntryCase::WhiteoutFile,
|
|
|
|
|
EntryCase::WhiteoutDevice,
|
|
|
|
|
] {
|
|
|
|
|
for boundary in 0..9 {
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let configuration = test_configuration(root.path());
|
|
|
|
|
let store = open_store(&configuration);
|
|
|
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
|
|
|
let anchor = domain_root(&configuration).join("state-anchor");
|
|
|
|
|
let upper = anchor.join("upper");
|
|
|
|
|
fs::create_dir(upper.join("unrelated")).unwrap();
|
|
|
|
|
fs::write(upper.join("unrelated/value"), b"unchanged").unwrap();
|
|
|
|
|
create_entry(&upper, entry_case);
|
|
|
|
|
let intent = StateResetIntentV1::new(
|
|
|
|
|
configuration.domain_id(),
|
|
|
|
|
StateResetScopeV1::Selected(b"/usr".to_vec()),
|
|
|
|
|
);
|
|
|
|
|
store.begin_state_reset(&intent).unwrap();
|
|
|
|
|
stage_boundary(&anchor, &upper, entry_case, boundary);
|
|
|
|
|
if boundary == 8 {
|
|
|
|
|
store.finish_state_reset(&intent).unwrap();
|
|
|
|
|
}
|
|
|
|
|
drop(layout);
|
|
|
|
|
|
|
|
|
|
recover_state_reset(&configuration, &store).unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(!upper.join("usr").exists());
|
|
|
|
|
assert!(!upper.join("usr").is_symlink());
|
|
|
|
|
assert!(!upper.join(".wh.usr").exists());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(upper.join("unrelated/value")).unwrap(),
|
|
|
|
|
b"unchanged"
|
|
|
|
|
);
|
|
|
|
|
assert!(store.state_reset_intent().unwrap().is_none());
|
|
|
|
|
assert!(!anchor.join("reset-quarantine").exists());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn corrupt_mismatched_and_orphan_recovery_state_refuses() {
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let configuration = test_configuration(root.path());
|
|
|
|
|
let store = open_store(&configuration);
|
|
|
|
|
let foreign = StateResetIntentV1::new(
|
|
|
|
|
configuration.domain_id(),
|
|
|
|
|
StateResetScopeV1::Selected(b"/etc".to_vec()),
|
|
|
|
|
);
|
|
|
|
|
store.begin_state_reset(&foreign).unwrap();
|
|
|
|
|
assert!(recover_state_reset(&configuration, &store).is_err());
|
|
|
|
|
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let configuration = test_configuration(root.path());
|
|
|
|
|
let store = open_store(&configuration);
|
|
|
|
|
StateAnchorLayout::create(&configuration).unwrap();
|
|
|
|
|
fs::create_dir(domain_root(&configuration).join("state-anchor/reset-quarantine")).unwrap();
|
|
|
|
|
assert!(recover_state_reset(&configuration, &store).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn active_domain_refuses_inspection_and_reset() {
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let configuration = test_configuration(root.path());
|
|
|
|
|
let store = open_store(&configuration);
|
|
|
|
|
store
|
|
|
|
|
.apply(&[crate::domain_store::DomainStoreMutation::PutGeneration(
|
|
|
|
|
Generation {
|
|
|
|
|
domain_id: configuration.domain_id(),
|
|
|
|
|
generation_id: GenerationId::new(1),
|
|
|
|
|
configuration_fingerprint: ConfigurationFingerprint::new([2; 32]),
|
|
|
|
|
coordinator_id: CoordinatorId::new([3; 32]),
|
|
|
|
|
network_sidecar_id: NetworkSidecarId::new([4; 32]),
|
|
|
|
|
sandbox_id: SandboxId::new([5; 32]),
|
|
|
|
|
durable_decision: DurableDecision::None,
|
|
|
|
|
},
|
|
|
|
|
)])
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(inspect_domain_state(&configuration, &store).is_err());
|
|
|
|
|
assert!(reset_domain_state(&configuration, &store, StateResetScopeV1::All).is_err());
|
|
|
|
|
assert!(store.state_reset_intent().unwrap().is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_entry(upper: &Path, entry_case: EntryCase) {
|
|
|
|
|
match entry_case {
|
|
|
|
|
EntryCase::Directory => {
|
|
|
|
|
fs::create_dir(upper.join("usr")).unwrap();
|
|
|
|
|
fs::write(upper.join("usr/value"), b"state").unwrap();
|
|
|
|
|
fs::write(upper.join("usr/.wh..wh..opq"), b"").unwrap();
|
|
|
|
|
let directory = File::open(upper.join("usr")).unwrap();
|
|
|
|
|
fsetxattr(
|
|
|
|
|
directory,
|
|
|
|
|
"user.fuseoverlayfs.opaque",
|
|
|
|
|
b"y",
|
|
|
|
|
XattrFlags::empty(),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
EntryCase::File => fs::write(upper.join("usr"), b"state").unwrap(),
|
|
|
|
|
EntryCase::Symlink => symlink("/foreign", upper.join("usr")).unwrap(),
|
|
|
|
|
EntryCase::WhiteoutFile => fs::write(upper.join(".wh.usr"), b"").unwrap(),
|
|
|
|
|
EntryCase::WhiteoutDevice => mknodat(
|
|
|
|
|
rustix::fs::CWD,
|
|
|
|
|
upper.join("usr"),
|
|
|
|
|
FileType::CharacterDevice,
|
|
|
|
|
Mode::empty(),
|
|
|
|
|
makedev(0, 0),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn stage_boundary(anchor: &Path, upper: &Path, entry_case: EntryCase, boundary: usize) {
|
|
|
|
|
if boundary == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let quarantine = anchor.join("reset-quarantine");
|
|
|
|
|
fs::create_dir(&quarantine).unwrap();
|
|
|
|
|
if boundary == 1 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let (source, destination) = match entry_case {
|
|
|
|
|
EntryCase::WhiteoutFile => (upper.join(".wh.usr"), quarantine.join("whiteout")),
|
|
|
|
|
_ => (upper.join("usr"), quarantine.join("entry")),
|
|
|
|
|
};
|
|
|
|
|
fs::rename(source, &destination).unwrap();
|
|
|
|
|
if boundary <= 3 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
remove_entry(&destination);
|
|
|
|
|
if boundary <= 5 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fs::remove_dir(&quarantine).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn remove_entry(path: &Path) {
|
|
|
|
|
let metadata = fs::symlink_metadata(path).unwrap();
|
|
|
|
|
if metadata.file_type().is_dir() {
|
|
|
|
|
fs::remove_dir_all(path).unwrap();
|
|
|
|
|
} else {
|
|
|
|
|
fs::remove_file(path).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_store(configuration: &ResolvedDomainConfiguration) -> HostCoordinatorDomainStore {
|
|
|
|
|
let root = domain_root(configuration);
|
|
|
|
|
let lock = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.create(true)
|
|
|
|
|
.truncate(false)
|
|
|
|
|
.open(root.join("domain.lock"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
HostCoordinatorDomainStore::open(
|
|
|
|
|
try_exclusive_flock(lock).unwrap(),
|
|
|
|
|
&root.join("domain.redb"),
|
|
|
|
|
&Domain {
|
|
|
|
|
domain_id: configuration.domain_id(),
|
|
|
|
|
profile_name: configuration.selected_profile().to_owned(),
|
|
|
|
|
canonical_workspace: configuration.workspace().canonical_label().clone(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn domain_root(configuration: &ResolvedDomainConfiguration) -> PathBuf {
|
|
|
|
|
PathBuf::from(std::ffi::OsString::from_vec(
|
|
|
|
|
configuration
|
|
|
|
|
.persistent_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes()
|
|
|
|
|
.to_vec(),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn test_configuration(root: &Path) -> ResolvedDomainConfiguration {
|
|
|
|
|
let configuration_directory = root.join("configuration");
|
|
|
|
|
let workspace = root.join("workspace");
|
|
|
|
|
let runtime = root.join("runtime");
|
|
|
|
|
fs::create_dir(&configuration_directory).unwrap();
|
|
|
|
|
fs::create_dir(&workspace).unwrap();
|
|
|
|
|
fs::create_dir(&runtime).unwrap();
|
|
|
|
|
let configuration_path = configuration_directory.join("lazaret.toml");
|
|
|
|
|
fs::write(
|
|
|
|
|
&configuration_path,
|
|
|
|
|
format!(
|
|
|
|
|
"schema = \"lazaret-trusted-host-config-v1\"\n[storage]\ndata_root = \"{}\"\n[network]\ndefault_allow = [\"192.0.2.10/32\"]\n[profiles.default]\nimage = \"example.invalid/state:v1\"\npersistent_dirs = [\"/usr\"]\n[profiles.default.readiness]\ndenied = [{{ address = \"10.20.30.40\", port = 8443 }}]\npermitted = [{{ address = \"192.0.2.10\", port = 443 }}]\ndns = [{{ resolver = \"198.51.100.53\", query = \"example.invalid\" }}]\n",
|
|
|
|
|
root.join("data").display()
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
load_trusted_configuration(ConfigurationLoadRequest {
|
|
|
|
|
explicit_configuration_path: Some(configuration_path),
|
|
|
|
|
selected_profile: "default".into(),
|
|
|
|
|
workspace,
|
|
|
|
|
home: root.join("home"),
|
|
|
|
|
xdg_config_home: None,
|
|
|
|
|
xdg_data_home: None,
|
|
|
|
|
xdg_runtime_dir: runtime,
|
|
|
|
|
environment: BTreeMap::new(),
|
2026-07-18 22:35:48 +09:00
|
|
|
sandbox_layout: default_sandbox_layout().unwrap(),
|
2026-07-17 07:54:46 +09:00
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
}
|