2026-07-19 05:12:57 +09:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::fs::{self, File, OpenOptions};
|
|
|
|
|
use std::os::unix::fs::symlink;
|
|
|
|
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
|
|
|
use std::os::unix::net::UnixListener;
|
|
|
|
|
use std::path::PathBuf;
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
use lazaret_core::configuration::{ConfigurationLoadRequest, load_trusted_configuration};
|
|
|
|
|
use lazaret_core::identity::{
|
|
|
|
|
BootId, BundleDigest, ContainerId, CoordinatorId, DeviceId, HostPidLocator, IdMapDigest,
|
|
|
|
|
InodeId, MountId, NetworkSidecarId, PidfsDeviceId, PidfsInodeId, PidfsProcessAuthorityV1,
|
|
|
|
|
PolicyId, PrepareOperationId, PreparedArtifactId, ProcessStartTimeTicks, SandboxId,
|
|
|
|
|
SelectedManifestDigest, SourceIdentity,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_core::records::{
|
|
|
|
|
ArtifactState, DurableDecision, GitRoutingOwnership, HelperOwnership, PrepareOperation,
|
|
|
|
|
PrepareStageState, PreparedArtifact, PreparedLowerOwnership, PublicationOwnership,
|
|
|
|
|
SandboxOwnership, StateAnchorOwnership,
|
|
|
|
|
};
|
|
|
|
|
use lazaret_linux::filesystem::try_exclusive_flock;
|
|
|
|
|
use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
use super::*;
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn process(value: u64) -> PidfsProcessIdentityV1 {
|
|
|
|
|
PidfsProcessIdentityV1::new(
|
|
|
|
|
PidfsProcessAuthorityV1 {
|
|
|
|
|
boot_id: BootId::new([1; 16]),
|
|
|
|
|
pidfs_st_dev: PidfsDeviceId::new(value),
|
|
|
|
|
pidfs_st_ino: PidfsInodeId::new(value),
|
|
|
|
|
},
|
|
|
|
|
HostPidLocator::new(u32::try_from(value).unwrap()),
|
|
|
|
|
ProcessStartTimeTicks::new(value),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn generation() -> Generation {
|
|
|
|
|
Generation {
|
|
|
|
|
domain_id: DomainId::new([1; 32]),
|
|
|
|
|
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::Preparing,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn participant(role: ParticipantRole, ownership: ParticipantOwnership) -> Participant {
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
GenerationId::new(1),
|
|
|
|
|
role,
|
|
|
|
|
ParticipantState::Intent,
|
|
|
|
|
Some(ownership),
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn inventory_rejects_default_partial_and_helper_or_fuse_omission() {
|
|
|
|
|
let generation = generation();
|
|
|
|
|
let empty = runtime_recovery_ownership(&[], &generation).unwrap();
|
|
|
|
|
assert!(prove_runtime_owners_absent(&empty).is_err());
|
|
|
|
|
|
|
|
|
|
let helper = participant(
|
|
|
|
|
ParticipantRole::Helper,
|
|
|
|
|
ParticipantOwnership::Helper(HelperOwnership {
|
|
|
|
|
network_sidecar_id: None,
|
|
|
|
|
connectivity_helper: PersistedHostProcessOwnership::staged(process(4)),
|
|
|
|
|
runtime_supervisor: PersistedHostProcessOwnership::committed(process(2)),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
assert!(runtime_recovery_ownership(&[helper], &generation).is_err());
|
|
|
|
|
|
|
|
|
|
let incomplete_fuse = participant(
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
|
|
|
|
lower_artifact_id: None,
|
|
|
|
|
upper_path: None,
|
|
|
|
|
work_path: None,
|
|
|
|
|
merged_path: Some(HostPath::new(b"/tmp/missing".to_vec())),
|
|
|
|
|
daemon: PersistedHostProcessOwnership::unassigned(),
|
|
|
|
|
user_namespace: Some(NamespaceIdentity {
|
|
|
|
|
device: DeviceId::new(6),
|
|
|
|
|
inode: InodeId::new(7),
|
|
|
|
|
}),
|
|
|
|
|
mount_namespace: Some(NamespaceIdentity {
|
|
|
|
|
device: DeviceId::new(8),
|
|
|
|
|
inode: InodeId::new(9),
|
|
|
|
|
}),
|
|
|
|
|
mount: Some(MountIdentity {
|
|
|
|
|
mount_id: MountId::new(10),
|
|
|
|
|
device: DeviceId::new(11),
|
|
|
|
|
inode: InodeId::new(12),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let prepared = Participant::bare_intent(GenerationId::new(1), ParticipantRole::PreparedLower);
|
|
|
|
|
assert!(runtime_recovery_ownership(&[prepared.clone(), incomplete_fuse], &generation).is_err());
|
|
|
|
|
|
|
|
|
|
let staged_state = participant(
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
|
|
|
|
lower_artifact_id: None,
|
|
|
|
|
upper_path: None,
|
|
|
|
|
work_path: None,
|
|
|
|
|
merged_path: None,
|
|
|
|
|
daemon: PersistedHostProcessOwnership::staged(process(5)),
|
|
|
|
|
user_namespace: None,
|
|
|
|
|
mount_namespace: None,
|
|
|
|
|
mount: None,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let mut opened = 0;
|
|
|
|
|
let partial = runtime_recovery_ownership(&[prepared, staged_state], &generation);
|
|
|
|
|
assert!(partial.is_err());
|
|
|
|
|
if let Ok(partial) = partial {
|
|
|
|
|
let _ = seal_runtime_owners(&partial, |_| {
|
|
|
|
|
opened += 1;
|
|
|
|
|
Err(io::Error::from_raw_os_error(
|
|
|
|
|
rustix::io::Errno::SRCH.raw_os_error(),
|
|
|
|
|
))
|
|
|
|
|
});
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
assert_eq!(opened, 0);
|
|
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
#[test]
|
|
|
|
|
fn complete_boot_mismatch_inventory_proves_only_after_every_holder_and_mount() {
|
|
|
|
|
let generation = generation();
|
|
|
|
|
let prepared = Participant::bare_intent(GenerationId::new(1), ParticipantRole::PreparedLower);
|
|
|
|
|
let state = participant(
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantOwnership::StateAnchor(StateAnchorOwnership {
|
|
|
|
|
lower_artifact_id: None,
|
|
|
|
|
upper_path: None,
|
|
|
|
|
work_path: None,
|
|
|
|
|
merged_path: None,
|
|
|
|
|
daemon: PersistedHostProcessOwnership::staged(process(5)),
|
|
|
|
|
user_namespace: None,
|
|
|
|
|
mount_namespace: None,
|
|
|
|
|
mount: None,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let helper = participant(
|
|
|
|
|
ParticipantRole::Helper,
|
|
|
|
|
ParticipantOwnership::Helper(HelperOwnership {
|
|
|
|
|
network_sidecar_id: None,
|
|
|
|
|
connectivity_helper: PersistedHostProcessOwnership::staged(process(4)),
|
|
|
|
|
runtime_supervisor: PersistedHostProcessOwnership::committed(process(2)),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let network = Participant::bare_intent(GenerationId::new(1), ParticipantRole::Network);
|
|
|
|
|
let sandbox = participant(
|
|
|
|
|
ParticipantRole::Sandbox,
|
|
|
|
|
ParticipantOwnership::Sandbox(SandboxOwnership {
|
|
|
|
|
container_id: Some(ContainerId::new([3; 32])),
|
|
|
|
|
runtime_root: Some(HostPath::new(b"/runtime".to_vec())),
|
|
|
|
|
runtime_root_identity: Some(MountIdentity {
|
|
|
|
|
mount_id: MountId::new(13),
|
|
|
|
|
device: DeviceId::new(14),
|
|
|
|
|
inode: InodeId::new(15),
|
2026-07-19 03:02:44 +09:00
|
|
|
}),
|
2026-07-19 05:12:57 +09:00
|
|
|
bundle_digest: Some(BundleDigest::new([4; 32])),
|
|
|
|
|
host_coordinator: PersistedHostProcessOwnership::committed(process(1)),
|
|
|
|
|
runtime_supervisor: PersistedHostProcessOwnership::staged(process(2)),
|
|
|
|
|
attached_runsc: PersistedHostProcessOwnership::staged(process(3)),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
let inventory =
|
|
|
|
|
runtime_recovery_ownership(&[prepared, state, helper, network, sandbox], &generation)
|
2026-07-19 03:02:44 +09:00
|
|
|
.unwrap();
|
2026-07-19 05:12:57 +09:00
|
|
|
assert_eq!(
|
|
|
|
|
prove_runtime_owners_absent(&inventory).unwrap().ownership(),
|
|
|
|
|
&inventory
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn recovery_preflight_recovers_full_runtime_then_is_idempotent() {
|
|
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let mut boundaries = Vec::new();
|
|
|
|
|
fixture
|
|
|
|
|
.recover_with(|boundary, generation| {
|
|
|
|
|
boundaries.push((boundary, generation));
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(
|
|
|
|
|
boundaries
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|(boundary, _)| { *boundary == RecoveryBoundary::StateAnchor })
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fixture.store.snapshot().unwrap().generations[0].durable_decision,
|
|
|
|
|
DurableDecision::Clean
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
fixture
|
|
|
|
|
.catalog
|
|
|
|
|
.snapshot()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.artifact_leases
|
|
|
|
|
.is_empty()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
fixture
|
|
|
|
|
.catalog
|
|
|
|
|
.snapshot()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.prepared_artifacts
|
|
|
|
|
.is_empty()
|
|
|
|
|
);
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
assert!(
|
|
|
|
|
!runtime_domain
|
|
|
|
|
.join("runsc")
|
|
|
|
|
.join(hex_lower(&[6; 32]))
|
|
|
|
|
.exists()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
recover_before_cold_start(
|
|
|
|
|
&fixture.configuration,
|
|
|
|
|
&fixture.store,
|
|
|
|
|
&fixture.catalog,
|
|
|
|
|
&fixture.prepared,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
enum PublicationFixtureBoundary {
|
|
|
|
|
TrulyAbsent,
|
|
|
|
|
AfterStagedIdentity,
|
|
|
|
|
AfterStagedSocket,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bare_publication_intent_requires_every_fixed_label_to_be_absent() {
|
|
|
|
|
let absent = RecoveryFixture::new();
|
|
|
|
|
absent.prepare_bare_publication(PublicationFixtureBoundary::TrulyAbsent);
|
|
|
|
|
recover_publication(&absent.configuration, &absent.store, GenerationId::new(2)).unwrap();
|
|
|
|
|
assert_eq!(absent.publication(2).state(), ParticipantState::Clean);
|
|
|
|
|
|
|
|
|
|
let identity = RecoveryFixture::new();
|
|
|
|
|
identity.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedIdentity);
|
|
|
|
|
assert!(
|
|
|
|
|
recover_publication(
|
|
|
|
|
&identity.configuration,
|
|
|
|
|
&identity.store,
|
|
|
|
|
GenerationId::new(2)
|
|
|
|
|
)
|
|
|
|
|
.is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
identity
|
|
|
|
|
.runtime_domain()
|
|
|
|
|
.join(STAGED_PUBLICATION_IDENTITY_NAME)
|
|
|
|
|
.is_file()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(identity.publication(2).state(), ParticipantState::Intent);
|
|
|
|
|
assert!(identity.publication(2).ownership().is_none());
|
|
|
|
|
|
|
|
|
|
let socket = RecoveryFixture::new();
|
|
|
|
|
socket.prepare_bare_publication(PublicationFixtureBoundary::AfterStagedSocket);
|
|
|
|
|
assert!(
|
|
|
|
|
recover_publication(&socket.configuration, &socket.store, GenerationId::new(2)).is_err()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
socket
|
|
|
|
|
.runtime_domain()
|
|
|
|
|
.join(STAGED_PUBLICATION_IDENTITY_NAME)
|
|
|
|
|
.is_file()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
fs::symlink_metadata(socket.runtime_domain().join(STAGED_CONTROL_SOCKET_NAME))
|
|
|
|
|
.unwrap()
|
|
|
|
|
.file_type()
|
|
|
|
|
.is_socket()
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(socket.publication(2).state(), ParticipantState::Intent);
|
|
|
|
|
assert!(socket.publication(2).ownership().is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn recovery_preflight_retries_every_recovery_boundary() {
|
|
|
|
|
for failed in [
|
|
|
|
|
RecoveryBoundary::Snapshot,
|
|
|
|
|
RecoveryBoundary::Publication,
|
|
|
|
|
RecoveryBoundary::Sandbox,
|
|
|
|
|
RecoveryBoundary::HelperAndNetwork,
|
|
|
|
|
RecoveryBoundary::GitRouting,
|
|
|
|
|
RecoveryBoundary::StateAnchor,
|
|
|
|
|
RecoveryBoundary::PreparedLower,
|
|
|
|
|
RecoveryBoundary::PrepareOperations,
|
|
|
|
|
RecoveryBoundary::ArtifactGc,
|
|
|
|
|
RecoveryBoundary::FinishGeneration,
|
|
|
|
|
] {
|
|
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let mut injected = false;
|
2026-07-19 03:02:44 +09:00
|
|
|
assert!(
|
2026-07-19 05:12:57 +09:00
|
|
|
fixture
|
|
|
|
|
.recover_with(|boundary, _| {
|
|
|
|
|
if boundary == failed && !injected {
|
|
|
|
|
injected = true;
|
|
|
|
|
Err("injected recovery boundary".into())
|
|
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
},)
|
|
|
|
|
.is_err()
|
2026-07-19 03:02:44 +09:00
|
|
|
);
|
2026-07-19 05:12:57 +09:00
|
|
|
assert!(injected);
|
|
|
|
|
fixture.recover_with(|_, _| Ok(())).unwrap();
|
2026-07-19 03:02:44 +09:00
|
|
|
assert_eq!(
|
|
|
|
|
fixture.store.snapshot().unwrap().generations[0].durable_decision,
|
|
|
|
|
DurableDecision::Clean
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
#[test]
|
|
|
|
|
fn recovery_preflight_opens_every_exact_owner_once() {
|
|
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let mut opened = BTreeMap::new();
|
|
|
|
|
recover_before_cold_start_with_opener(
|
|
|
|
|
&fixture.configuration,
|
|
|
|
|
&fixture.store,
|
|
|
|
|
&fixture.catalog,
|
|
|
|
|
&fixture.prepared,
|
|
|
|
|
|_, _| Ok(()),
|
|
|
|
|
|identity| {
|
|
|
|
|
*opened.entry(identity.host_pid().get()).or_insert(0) += 1;
|
|
|
|
|
Ok(PidfsRecoveryMatch::BootMismatch)
|
|
|
|
|
},
|
|
|
|
|
|root, container| {
|
|
|
|
|
unlinkat(root, container, AtFlags::REMOVEDIR).map_err(|error| error.to_string())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(opened.len(), 6);
|
|
|
|
|
assert!(opened.values().all(|count| *count == 1));
|
|
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
#[test]
|
|
|
|
|
fn publication_replacement_and_runsc_root_replacement_refuse_untouched() {
|
|
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
let socket = runtime_domain.join(CONTROL_SOCKET_NAME);
|
|
|
|
|
let old_socket = runtime_domain.join("old-control.sock");
|
|
|
|
|
fs::rename(&socket, &old_socket).unwrap();
|
|
|
|
|
let root = File::open(runtime_domain).unwrap();
|
|
|
|
|
drop(
|
|
|
|
|
UnixListener::bind(format!(
|
|
|
|
|
"/proc/self/fd/{}/{}",
|
|
|
|
|
root.as_raw_fd(),
|
|
|
|
|
CONTROL_SOCKET_NAME
|
|
|
|
|
))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
|
|
|
|
|
assert!(socket.exists());
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
let identity = runtime_domain.join(PUBLICATION_IDENTITY_NAME);
|
|
|
|
|
fs::rename(&identity, runtime_domain.join("old-identity")).unwrap();
|
|
|
|
|
symlink("old-identity", &identity).unwrap();
|
|
|
|
|
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
|
|
|
|
|
assert!(
|
|
|
|
|
fs::symlink_metadata(&identity)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.file_type()
|
|
|
|
|
.is_symlink()
|
|
|
|
|
);
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
let identity = runtime_domain.join(PUBLICATION_IDENTITY_NAME);
|
|
|
|
|
fs::rename(&identity, runtime_domain.join("old-identity")).unwrap();
|
|
|
|
|
fs::write(&identity, b"replacement").unwrap();
|
|
|
|
|
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
|
|
|
|
|
assert_eq!(fs::read(&identity).unwrap(), b"replacement");
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
let runsc = runtime_domain.join("runsc");
|
|
|
|
|
let old_runsc = runtime_domain.join("old-runsc");
|
|
|
|
|
fs::rename(&runsc, &old_runsc).unwrap();
|
|
|
|
|
fs::create_dir(&runsc).unwrap();
|
|
|
|
|
let mut deletes = 0;
|
|
|
|
|
let result = recover_before_cold_start_with_opener(
|
|
|
|
|
&fixture.configuration,
|
|
|
|
|
&fixture.store,
|
|
|
|
|
&fixture.catalog,
|
|
|
|
|
&fixture.prepared,
|
|
|
|
|
|_, _| Ok(()),
|
|
|
|
|
|_| Ok(PidfsRecoveryMatch::BootMismatch),
|
|
|
|
|
|_, _| {
|
|
|
|
|
deletes += 1;
|
|
|
|
|
Ok(())
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert_eq!(deletes, 0);
|
|
|
|
|
assert!(runsc.exists());
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
let fixture = RecoveryFixture::new();
|
|
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
fixture
|
|
|
|
|
.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
|
|
|
|
let runsc = runtime_domain.join("runsc");
|
|
|
|
|
fs::rename(&runsc, runtime_domain.join("old-runsc")).unwrap();
|
|
|
|
|
symlink("old-runsc", &runsc).unwrap();
|
|
|
|
|
assert!(fixture.recover_with(|_, _| Ok(())).is_err());
|
|
|
|
|
assert!(
|
|
|
|
|
fs::symlink_metadata(&runsc)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.file_type()
|
|
|
|
|
.is_symlink()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct RecoveryFixture {
|
|
|
|
|
_tree: tempfile::TempDir,
|
|
|
|
|
configuration: ResolvedDomainConfiguration,
|
|
|
|
|
store: HostCoordinatorDomainStore,
|
|
|
|
|
catalog: Catalog,
|
|
|
|
|
prepared: PreparedLowerStore,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RecoveryFixture {
|
|
|
|
|
fn recover_with<Failpoint>(&self, failpoint: Failpoint) -> Result<(), String>
|
|
|
|
|
where
|
|
|
|
|
Failpoint: FnMut(RecoveryBoundary, GenerationId) -> Result<(), String>,
|
|
|
|
|
{
|
2026-07-19 03:02:44 +09:00
|
|
|
recover_before_cold_start_with_opener(
|
2026-07-19 05:12:57 +09:00
|
|
|
&self.configuration,
|
|
|
|
|
&self.store,
|
|
|
|
|
&self.catalog,
|
|
|
|
|
&self.prepared,
|
|
|
|
|
failpoint,
|
|
|
|
|
match_recovery_process,
|
2026-07-19 03:02:44 +09:00
|
|
|
|root, container| {
|
|
|
|
|
unlinkat(root, container, AtFlags::REMOVEDIR).map_err(|error| error.to_string())
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
#[allow(clippy::too_many_lines)]
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
let tree = tempfile::tempdir().unwrap();
|
|
|
|
|
let configuration = test_configuration(tree.path());
|
|
|
|
|
let catalog = Catalog::under_storage_root(configuration.storage_root()).unwrap();
|
|
|
|
|
catalog.initialize().unwrap();
|
|
|
|
|
let prepared =
|
|
|
|
|
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())
|
|
|
|
|
.unwrap();
|
|
|
|
|
let artifact_name = hex_lower(&[21; 32]);
|
|
|
|
|
let artifact_path = tree
|
|
|
|
|
.path()
|
|
|
|
|
.join("data/prepared-lower/artifacts")
|
|
|
|
|
.join(&artifact_name);
|
|
|
|
|
fs::create_dir(&artifact_path).unwrap();
|
|
|
|
|
let metadata = artifact_path.metadata().unwrap();
|
|
|
|
|
let artifact = PreparedArtifact {
|
|
|
|
|
prepared_artifact_id: PreparedArtifactId::new([21; 32]),
|
|
|
|
|
source_identity: SourceIdentity::new(vec![22]),
|
|
|
|
|
selected_manifest_digest: SelectedManifestDigest::new([23; 32]),
|
|
|
|
|
policy_id: PolicyId::new([24; 32]),
|
|
|
|
|
id_map_digest: IdMapDigest::new([25; 32]),
|
|
|
|
|
path: HostPath::new(format!("artifacts/{artifact_name}").into_bytes()),
|
|
|
|
|
device: DeviceId::new(metadata.dev()),
|
|
|
|
|
inode: InodeId::new(metadata.ino()),
|
|
|
|
|
state: ArtifactState::Available,
|
|
|
|
|
};
|
|
|
|
|
publish_artifact(&catalog, &artifact);
|
|
|
|
|
let domain = lazaret_core::records::Domain {
|
|
|
|
|
domain_id: configuration.domain_id(),
|
|
|
|
|
profile_name: configuration.selected_profile().to_owned(),
|
|
|
|
|
canonical_workspace: configuration.workspace().canonical_label().clone(),
|
|
|
|
|
};
|
|
|
|
|
let lock = OpenOptions::new()
|
|
|
|
|
.create(true)
|
|
|
|
|
.read(true)
|
|
|
|
|
.truncate(false)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(tree.path().join("domain.lock"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let store = HostCoordinatorDomainStore::open(
|
|
|
|
|
try_exclusive_flock(lock).unwrap(),
|
|
|
|
|
&tree.path().join("domain.redb"),
|
|
|
|
|
&domain,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let generation = Generation {
|
|
|
|
|
domain_id: domain.domain_id,
|
|
|
|
|
generation_id: GenerationId::new(1),
|
|
|
|
|
configuration_fingerprint: configuration.configuration_fingerprint(),
|
|
|
|
|
coordinator_id: CoordinatorId::new([3; 32]),
|
|
|
|
|
network_sidecar_id: NetworkSidecarId::new([4; 32]),
|
|
|
|
|
sandbox_id: SandboxId::new([5; 32]),
|
|
|
|
|
durable_decision: DurableDecision::None,
|
|
|
|
|
};
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
|
|
|
|
.unwrap();
|
|
|
|
|
let mut generation = generation;
|
|
|
|
|
generation.durable_decision = DurableDecision::Preparing;
|
|
|
|
|
store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
|
|
|
|
.unwrap();
|
|
|
|
|
let layout = StateAnchorLayout::create(&configuration).unwrap();
|
|
|
|
|
let mut anchor = layout.ownership_intent(artifact.prepared_artifact_id);
|
|
|
|
|
let mut staged_anchor = anchor.clone();
|
|
|
|
|
staged_anchor.daemon = PersistedHostProcessOwnership::staged(process(5));
|
|
|
|
|
anchor.daemon = PersistedHostProcessOwnership::committed(process(5));
|
|
|
|
|
anchor.user_namespace = Some(NamespaceIdentity {
|
|
|
|
|
device: DeviceId::new(30),
|
|
|
|
|
inode: InodeId::new(31),
|
|
|
|
|
});
|
|
|
|
|
anchor.mount_namespace = Some(NamespaceIdentity {
|
|
|
|
|
device: DeviceId::new(32),
|
|
|
|
|
inode: InodeId::new(33),
|
|
|
|
|
});
|
|
|
|
|
anchor.mount = Some(MountIdentity {
|
|
|
|
|
mount_id: MountId::new(u64::MAX),
|
|
|
|
|
device: DeviceId::new(34),
|
|
|
|
|
inode: InodeId::new(35),
|
|
|
|
|
});
|
|
|
|
|
let prepared_ownership = PreparedLowerOwnership {
|
|
|
|
|
prepared_artifact_id: Some(artifact.prepared_artifact_id),
|
|
|
|
|
source_identity: Some(artifact.source_identity.clone()),
|
|
|
|
|
lease_state: Some(ParticipantState::LeaseHeld),
|
|
|
|
|
path: Some(artifact.path.clone()),
|
|
|
|
|
device: Some(artifact.device),
|
|
|
|
|
inode: Some(artifact.inode),
|
|
|
|
|
released_state_anchor: None,
|
|
|
|
|
};
|
|
|
|
|
let helper = HelperOwnership {
|
|
|
|
|
network_sidecar_id: None,
|
|
|
|
|
connectivity_helper: PersistedHostProcessOwnership::staged(process(4)),
|
|
|
|
|
runtime_supervisor: PersistedHostProcessOwnership::committed(process(2)),
|
|
|
|
|
};
|
2026-07-19 03:02:44 +09:00
|
|
|
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
2026-07-19 05:12:57 +09:00
|
|
|
configuration
|
2026-07-19 03:02:44 +09:00
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
));
|
2026-07-19 05:12:57 +09:00
|
|
|
let runsc_root = runtime_domain.join("runsc");
|
|
|
|
|
fs::create_dir(&runsc_root).unwrap();
|
|
|
|
|
let container_id = ContainerId::new([6; 32]);
|
|
|
|
|
fs::create_dir(runsc_root.join(hex_lower(container_id.as_bytes()))).unwrap();
|
|
|
|
|
let sandbox = SandboxOwnership {
|
|
|
|
|
container_id: Some(container_id),
|
|
|
|
|
runtime_root: Some(HostPath::new(runsc_root.as_os_str().as_bytes().to_vec())),
|
|
|
|
|
runtime_root_identity: Some(current_mount_identity(&runsc_root).unwrap()),
|
|
|
|
|
bundle_digest: Some(BundleDigest::new([7; 32])),
|
|
|
|
|
host_coordinator: PersistedHostProcessOwnership::committed(process(1)),
|
|
|
|
|
runtime_supervisor: PersistedHostProcessOwnership::staged(process(2)),
|
|
|
|
|
attached_runsc: PersistedHostProcessOwnership::staged(process(3)),
|
|
|
|
|
};
|
|
|
|
|
let publication_socket = runtime_domain.join(CONTROL_SOCKET_NAME);
|
|
|
|
|
let publication_root = File::open(runtime_domain).unwrap();
|
2026-07-19 03:02:44 +09:00
|
|
|
drop(
|
|
|
|
|
UnixListener::bind(format!(
|
|
|
|
|
"/proc/self/fd/{}/{}",
|
2026-07-19 05:12:57 +09:00
|
|
|
publication_root.as_raw_fd(),
|
2026-07-19 03:02:44 +09:00
|
|
|
CONTROL_SOCKET_NAME
|
|
|
|
|
))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
2026-07-19 05:12:57 +09:00
|
|
|
let publication_identity = runtime_domain.join(PUBLICATION_IDENTITY_NAME);
|
|
|
|
|
fs::write(&publication_identity, b"identity").unwrap();
|
|
|
|
|
let publication = PublicationOwnership {
|
|
|
|
|
staged_socket_path: Some(HostPath::new(
|
|
|
|
|
STAGED_CONTROL_SOCKET_NAME.as_bytes().to_vec(),
|
|
|
|
|
)),
|
|
|
|
|
staged_identity_path: Some(HostPath::new(
|
|
|
|
|
STAGED_PUBLICATION_IDENTITY_NAME.as_bytes().to_vec(),
|
|
|
|
|
)),
|
|
|
|
|
socket_path: Some(HostPath::new(CONTROL_SOCKET_NAME.as_bytes().to_vec())),
|
|
|
|
|
identity_path: Some(HostPath::new(PUBLICATION_IDENTITY_NAME.as_bytes().to_vec())),
|
|
|
|
|
socket_identity: Some(path_identity(runtime_domain, &publication_socket)),
|
|
|
|
|
identity_file_identity: Some(path_identity(runtime_domain, &publication_identity)),
|
|
|
|
|
};
|
|
|
|
|
let git_path = runtime_domain.join("git-routing-fixture");
|
|
|
|
|
fs::create_dir(&git_path).unwrap();
|
|
|
|
|
let git_routing = GitRoutingOwnership {
|
|
|
|
|
mountpoint: Some(HostPath::new(git_path.as_os_str().as_bytes().to_vec())),
|
|
|
|
|
host_root: Some(HostPath::new(b"/git-host".to_vec())),
|
|
|
|
|
isolated_root: Some(HostPath::new(b"/git-isolated".to_vec())),
|
|
|
|
|
daemon: PersistedHostProcessOwnership::staged(process(6)),
|
|
|
|
|
mount: Some(MountIdentity {
|
|
|
|
|
mount_id: MountId::new(u64::MAX - 1),
|
|
|
|
|
device: DeviceId::new(40),
|
|
|
|
|
inode: InodeId::new(41),
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
store
|
|
|
|
|
.apply(&[
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::PreparedLower,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::PreparedLower,
|
|
|
|
|
ParticipantState::Intent,
|
|
|
|
|
Some(ParticipantOwnership::PreparedLower(
|
|
|
|
|
PreparedLowerOwnership {
|
|
|
|
|
lease_state: Some(ParticipantState::Intent),
|
|
|
|
|
..prepared_ownership.clone()
|
|
|
|
|
},
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
.unwrap();
|
|
|
|
|
let lease = ArtifactLease {
|
|
|
|
|
prepared_artifact_id: artifact.prepared_artifact_id,
|
|
|
|
|
domain_id: domain.domain_id,
|
|
|
|
|
generation_id: generation.generation_id,
|
|
|
|
|
};
|
|
|
|
|
catalog.acquire_lease_checked(&artifact, &lease).unwrap();
|
|
|
|
|
store
|
|
|
|
|
.apply(&[
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::PreparedLower,
|
|
|
|
|
ParticipantState::LeaseHeld,
|
|
|
|
|
Some(ParticipantOwnership::PreparedLower(prepared_ownership)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantState::Intent,
|
|
|
|
|
Some(ParticipantOwnership::StateAnchor(staged_anchor)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantState::Intent,
|
|
|
|
|
Some(ParticipantOwnership::StateAnchor(anchor.clone())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::StateAnchor,
|
|
|
|
|
ParticipantState::Prepared,
|
|
|
|
|
Some(ParticipantOwnership::StateAnchor(anchor)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Helper,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(participant(
|
|
|
|
|
ParticipantRole::Helper,
|
|
|
|
|
ParticipantOwnership::Helper(helper),
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Network,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Sandbox,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(participant(
|
|
|
|
|
ParticipantRole::Sandbox,
|
|
|
|
|
ParticipantOwnership::Sandbox(sandbox),
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::GitRouting,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(participant(
|
|
|
|
|
ParticipantRole::GitRouting,
|
|
|
|
|
ParticipantOwnership::GitRouting(git_routing),
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Publication,
|
|
|
|
|
)),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Publication,
|
|
|
|
|
ParticipantState::Intent,
|
|
|
|
|
Some(ParticipantOwnership::Publication(publication.clone())),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
DomainStoreMutation::PutParticipant(
|
|
|
|
|
Participant::snapshot(
|
|
|
|
|
generation.generation_id,
|
|
|
|
|
ParticipantRole::Publication,
|
|
|
|
|
ParticipantState::Committed,
|
|
|
|
|
Some(ParticipantOwnership::Publication(publication)),
|
|
|
|
|
)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
.unwrap();
|
|
|
|
|
Self {
|
|
|
|
|
_tree: tree,
|
|
|
|
|
configuration,
|
|
|
|
|
store,
|
|
|
|
|
catalog,
|
|
|
|
|
prepared,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn runtime_domain(&self) -> PathBuf {
|
|
|
|
|
Path::new(std::ffi::OsStr::from_bytes(
|
|
|
|
|
self.configuration
|
|
|
|
|
.runtime_domain_root()
|
|
|
|
|
.canonical_label()
|
|
|
|
|
.as_bytes(),
|
|
|
|
|
))
|
|
|
|
|
.to_path_buf()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn prepare_bare_publication(&self, boundary: PublicationFixtureBoundary) {
|
|
|
|
|
let runtime_domain = self.runtime_domain();
|
|
|
|
|
fs::remove_file(runtime_domain.join(CONTROL_SOCKET_NAME)).unwrap();
|
|
|
|
|
fs::remove_file(runtime_domain.join(PUBLICATION_IDENTITY_NAME)).unwrap();
|
|
|
|
|
let generation = Generation {
|
|
|
|
|
domain_id: self.configuration.domain_id(),
|
|
|
|
|
generation_id: GenerationId::new(2),
|
|
|
|
|
configuration_fingerprint: self.configuration.configuration_fingerprint(),
|
|
|
|
|
coordinator_id: CoordinatorId::new([13; 32]),
|
|
|
|
|
network_sidecar_id: NetworkSidecarId::new([14; 32]),
|
|
|
|
|
sandbox_id: SandboxId::new([15; 32]),
|
|
|
|
|
durable_decision: DurableDecision::None,
|
|
|
|
|
};
|
|
|
|
|
self.store
|
|
|
|
|
.apply(&[DomainStoreMutation::PutGeneration(generation.clone())])
|
|
|
|
|
.unwrap();
|
|
|
|
|
self.store
|
|
|
|
|
.apply(&[
|
|
|
|
|
DomainStoreMutation::PutGeneration(Generation {
|
|
|
|
|
durable_decision: DurableDecision::Preparing,
|
|
|
|
|
..generation
|
|
|
|
|
}),
|
|
|
|
|
DomainStoreMutation::PutParticipant(Participant::bare_intent(
|
|
|
|
|
GenerationId::new(2),
|
|
|
|
|
ParticipantRole::Publication,
|
|
|
|
|
)),
|
|
|
|
|
])
|
|
|
|
|
.unwrap();
|
|
|
|
|
if !matches!(boundary, PublicationFixtureBoundary::TrulyAbsent) {
|
|
|
|
|
fs::write(
|
|
|
|
|
runtime_domain.join(STAGED_PUBLICATION_IDENTITY_NAME),
|
|
|
|
|
b"identity",
|
2026-07-19 03:02:44 +09:00
|
|
|
)
|
|
|
|
|
.unwrap();
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
|
|
|
|
if matches!(boundary, PublicationFixtureBoundary::AfterStagedSocket) {
|
|
|
|
|
let root = File::open(runtime_domain).unwrap();
|
2026-07-19 03:02:44 +09:00
|
|
|
drop(
|
|
|
|
|
UnixListener::bind(format!(
|
|
|
|
|
"/proc/self/fd/{}/{}",
|
2026-07-19 05:12:57 +09:00
|
|
|
root.as_raw_fd(),
|
|
|
|
|
STAGED_CONTROL_SOCKET_NAME
|
2026-07-19 03:02:44 +09:00
|
|
|
))
|
|
|
|
|
.unwrap(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn publication(&self, generation: u64) -> Participant {
|
|
|
|
|
role(
|
|
|
|
|
&self.store,
|
|
|
|
|
GenerationId::new(generation),
|
|
|
|
|
ParticipantRole::Publication,
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.unwrap()
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn path_identity(root: &Path, path: &Path) -> MountIdentity {
|
|
|
|
|
let root_identity = current_mount_identity(root).unwrap();
|
|
|
|
|
let metadata = fs::symlink_metadata(path).unwrap();
|
|
|
|
|
MountIdentity {
|
|
|
|
|
mount_id: root_identity.mount_id,
|
|
|
|
|
device: DeviceId::new(metadata.dev()),
|
|
|
|
|
inode: InodeId::new(metadata.ino()),
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
2026-07-19 05:12:57 +09:00
|
|
|
}
|
2026-07-19 03:02:44 +09:00
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
fn publish_artifact(catalog: &Catalog, artifact: &PreparedArtifact) {
|
|
|
|
|
let intent = PrepareOperation {
|
|
|
|
|
operation_id: PrepareOperationId::new([12; 32]),
|
|
|
|
|
expected_artifact_id: artifact.prepared_artifact_id,
|
|
|
|
|
source_identity: artifact.source_identity.clone(),
|
|
|
|
|
selected_manifest_digest: artifact.selected_manifest_digest,
|
|
|
|
|
policy_id: artifact.policy_id,
|
|
|
|
|
id_map_digest: artifact.id_map_digest,
|
|
|
|
|
stage_path: HostPath::new(b"stages/fixture".to_vec()),
|
|
|
|
|
stage_state: PrepareStageState::Intent,
|
|
|
|
|
device: None,
|
|
|
|
|
inode: None,
|
|
|
|
|
};
|
|
|
|
|
catalog.record_prepare(&intent).unwrap();
|
|
|
|
|
let owned = PrepareOperation {
|
|
|
|
|
stage_state: PrepareStageState::Owned,
|
|
|
|
|
device: Some(artifact.device),
|
|
|
|
|
inode: Some(artifact.inode),
|
|
|
|
|
..intent.clone()
|
|
|
|
|
};
|
|
|
|
|
catalog.bind_prepare_stage(&intent, &owned).unwrap();
|
|
|
|
|
catalog.publish(&owned, artifact).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
2026-07-19 03:02:44 +09:00
|
|
|
&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/factory:v1\"\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();
|
2026-07-19 05:12:57 +09:00
|
|
|
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(),
|
|
|
|
|
sandbox_layout: default_sandbox_layout().unwrap(),
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|