Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
1 changed files with 18648 additions and 18513 deletions
907
crates/lazaret-host-coordinator/src/recovery/tests.rs
Normal file
907
crates/lazaret-host-coordinator/src/recovery/tests.rs
Normal file
|
|
@ -0,0 +1,907 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
use super::*;
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
))
|
||||
});
|
||||
}
|
||||
assert_eq!(opened, 0);
|
||||
}
|
||||
|
||||
#[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),
|
||||
}),
|
||||
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)
|
||||
.unwrap();
|
||||
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;
|
||||
assert!(
|
||||
fixture
|
||||
.recover_with(|boundary, _| {
|
||||
if boundary == failed && !injected {
|
||||
injected = true;
|
||||
Err("injected recovery boundary".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},)
|
||||
.is_err()
|
||||
);
|
||||
assert!(injected);
|
||||
fixture.recover_with(|_, _| Ok(())).unwrap();
|
||||
assert_eq!(
|
||||
fixture.store.snapshot().unwrap().generations[0].durable_decision,
|
||||
DurableDecision::Clean
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
||||
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()
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
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());
|
||||
|
||||
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>,
|
||||
{
|
||||
recover_before_cold_start_with_opener(
|
||||
&self.configuration,
|
||||
&self.store,
|
||||
&self.catalog,
|
||||
&self.prepared,
|
||||
failpoint,
|
||||
match_recovery_process,
|
||||
|root, container| {
|
||||
unlinkat(root, container, AtFlags::REMOVEDIR).map_err(|error| error.to_string())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[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)),
|
||||
};
|
||||
let runtime_domain = Path::new(std::ffi::OsStr::from_bytes(
|
||||
configuration
|
||||
.runtime_domain_root()
|
||||
.canonical_label()
|
||||
.as_bytes(),
|
||||
));
|
||||
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();
|
||||
drop(
|
||||
UnixListener::bind(format!(
|
||||
"/proc/self/fd/{}/{}",
|
||||
publication_root.as_raw_fd(),
|
||||
CONTROL_SOCKET_NAME
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if matches!(boundary, PublicationFixtureBoundary::AfterStagedSocket) {
|
||||
let root = File::open(runtime_domain).unwrap();
|
||||
drop(
|
||||
UnixListener::bind(format!(
|
||||
"/proc/self/fd/{}/{}",
|
||||
root.as_raw_fd(),
|
||||
STAGED_CONTROL_SOCKET_NAME
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn publication(&self, generation: u64) -> Participant {
|
||||
role(
|
||||
&self.store,
|
||||
GenerationId::new(generation),
|
||||
ParticipantRole::Publication,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
&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();
|
||||
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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue