lazaret/crates/lazaret-prepared-lower/tests/prepared_lower/publication.rs

497 lines
16 KiB
Rust
Raw Normal View History

use super::*;
#[test]
fn high_expansion_layer_is_disk_spooled_and_published() {
let fixture = Fixture::new();
let payload = vec![0u8; 32 * 1024 * 1024];
let tar = layer(&[Entry {
path: "expanded",
kind: EntryType::Regular,
data: &payload,
target: None,
mode: 0o600,
}]);
let compressed = compress(&tar, OCI_GZIP);
assert!(compressed.len() * 100 < payload.len());
let request = fixture.request(30, &[(compressed, OCI_GZIP)]);
let artifact = fixture.store.prepare(&request).unwrap();
assert_eq!(
std::fs::metadata(fixture.artifact_root(&artifact).join("expanded"))
.unwrap()
.len(),
payload.len() as u64
);
assert!(
fixture
.root
.join("stages")
.read_dir()
.unwrap()
.next()
.is_none()
);
}
#[test]
fn concurrent_publication_reuses_one_immutable_artifact() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let first = fixture.request(40, &[(bytes.clone(), OCI_TAR)]);
let mut second = PrepareRequest {
operation_id: PrepareOperationId::new([41; 32]),
..first.clone()
};
second.layout_path = first.layout_path.clone();
let store = Arc::new(fixture.store.clone());
let barrier = Arc::new(Barrier::new(3));
let first_handle = {
let store = Arc::clone(&store);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
store.prepare(&first)
})
};
let second_handle = {
let store = Arc::clone(&store);
let barrier = Arc::clone(&barrier);
thread::spawn(move || {
barrier.wait();
store.prepare(&second)
})
};
barrier.wait();
let first = first_handle.join().unwrap().unwrap();
let second = second_handle.join().unwrap().unwrap();
assert_eq!(first, second);
let snapshot = fixture.catalog.snapshot().unwrap();
assert_eq!(snapshot.prepared_artifacts, vec![first]);
assert!(snapshot.prepare_operations.is_empty());
}
#[test]
fn publication_during_reclaim_preserves_replacement_and_deletes_old_inode() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let first_request = fixture.request(50, &[(bytes, OCI_TAR)]);
let first = fixture.store.prepare(&first_request).unwrap();
let reclaiming = fixture.catalog.begin_reclaim(&first).unwrap();
let replacement_request = PrepareRequest {
operation_id: PrepareOperationId::new([51; 32]),
..first_request
};
let barrier = Arc::new(Barrier::new(3));
let store = Arc::new(fixture.store.clone());
let reclaim_handle = {
let barrier = Arc::clone(&barrier);
let store = Arc::clone(&store);
thread::spawn(move || {
barrier.wait();
store.resume_reclaim(&reclaiming)
})
};
let publish_handle = {
let barrier = Arc::clone(&barrier);
let store = Arc::clone(&store);
thread::spawn(move || {
barrier.wait();
store.prepare(&replacement_request)
})
};
barrier.wait();
let reclaim = reclaim_handle.join().unwrap().unwrap();
let replacement = publish_handle.join().unwrap().unwrap();
assert!(matches!(
reclaim,
ReclaimOutcome::Removed | ReclaimOutcome::ReplacementPreserved
));
assert_ne!(replacement.inode, first.inode);
assert_eq!(
fixture.catalog.snapshot().unwrap().prepared_artifacts,
vec![replacement]
);
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
}
#[test]
fn replacement_winning_before_reclaimer_resumes_still_cleans_old_quarantine() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let first_request = fixture.request(52, &[(bytes, OCI_TAR)]);
let first = fixture.store.prepare(&first_request).unwrap();
let reclaiming = fixture.catalog.begin_reclaim(&first).unwrap();
let replacement_request = PrepareRequest {
operation_id: PrepareOperationId::new([53; 32]),
..first_request
};
let replacement = fixture.store.prepare(&replacement_request).unwrap();
assert_ne!(replacement.inode, first.inode);
assert_eq!(
fixture.store.resume_reclaim(&reclaiming).unwrap(),
ReclaimOutcome::ReplacementPreserved
);
assert_eq!(
fixture.catalog.snapshot().unwrap().prepared_artifacts,
vec![replacement.clone()]
);
assert!(fixture.artifact_root(&replacement).exists());
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
}
#[test]
fn replacement_reclaim_start_does_not_orphan_prior_quarantine() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let first_request = fixture.request(55, &[(bytes, OCI_TAR)]);
let first = fixture.store.prepare(&first_request).unwrap();
let first_reclaiming = fixture.catalog.begin_reclaim(&first).unwrap();
let replacement_request = PrepareRequest {
operation_id: PrepareOperationId::new([56; 32]),
..first_request
};
let replacement = fixture.store.prepare(&replacement_request).unwrap();
let replacement_reclaiming = fixture.catalog.begin_reclaim(&replacement).unwrap();
assert_eq!(
fixture.store.resume_reclaim(&first_reclaiming).unwrap(),
ReclaimOutcome::ReplacementPreserved
);
assert!(fixture.artifact_root(&replacement).is_dir());
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
assert_eq!(
fixture
.store
.resume_reclaim(&replacement_reclaiming)
.unwrap(),
ReclaimOutcome::Removed
);
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.prepared_artifacts
.is_empty()
);
assert!(!fixture.artifact_root(&replacement).exists());
assert!(
fixture
.root
.join("quarantine")
.read_dir()
.unwrap()
.next()
.is_none()
);
}
#[test]
fn reclaim_refuses_preexisting_foreign_quarantine_path() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let artifact = fixture
.store
.prepare(&fixture.request(54, &[(bytes, OCI_TAR)]))
.unwrap();
let foreign = fixture.root.join("quarantine").join(format!(
"{}-{}-{}",
hex(artifact.prepared_artifact_id.as_bytes()),
artifact.device.get(),
artifact.inode.get()
));
std::fs::create_dir(&foreign).unwrap();
assert!(fixture.store.reclaim(&artifact).is_err());
assert!(foreign.is_dir());
assert!(fixture.artifact_root(&artifact).is_dir());
}
#[test]
fn recovery_deletes_only_exact_owned_stage_and_preserves_foreign_stage() {
let fixture = Fixture::new();
let operation_id = PrepareOperationId::new([60; 32]);
let stage = Path::new("stages").join(hex(operation_id.as_bytes()));
let intent = PrepareOperation {
operation_id,
expected_artifact_id: PreparedArtifactId::new([61; 32]),
source_identity: SourceIdentity::new(b"source".to_vec()),
selected_manifest_digest: SelectedManifestDigest::new([62; 32]),
policy_id: PolicyId::new([63; 32]),
id_map_digest: IdMapDigest::new([64; 32]),
stage_path: HostPath::new(stage.as_os_str().as_encoded_bytes().to_vec()),
stage_state: PrepareStageState::Intent,
device: None,
inode: None,
};
fixture.catalog.record_prepare(&intent).unwrap();
std::fs::create_dir(fixture.root.join(&stage)).unwrap();
std::fs::write(fixture.root.join(&stage).join("foreign"), b"keep").unwrap();
2026-07-19 03:18:50 +09:00
assert!(matches!(
fixture.store.recover_prepare(&intent),
Err(PreparedLowerError::Recovery { .. })
));
assert!(fixture.root.join(&stage).join("foreign").exists());
std::fs::remove_dir_all(fixture.root.join(&stage)).unwrap();
assert_eq!(
fixture.store.recover_prepare(&intent).unwrap(),
PrepareRecovery::ClearedUnmaterializedIntent
);
let owned_id = PrepareOperationId::new([65; 32]);
let owned_stage = Path::new("stages").join(hex(owned_id.as_bytes()));
let owned_intent = PrepareOperation {
operation_id: owned_id,
stage_path: HostPath::new(owned_stage.as_os_str().as_encoded_bytes().to_vec()),
..intent
};
fixture.catalog.record_prepare(&owned_intent).unwrap();
std::fs::create_dir(fixture.root.join(&owned_stage)).unwrap();
std::fs::write(fixture.root.join(&owned_stage).join("partial"), b"partial").unwrap();
let metadata = std::fs::metadata(fixture.root.join(&owned_stage)).unwrap();
let owned = PrepareOperation {
stage_state: PrepareStageState::Owned,
device: Some(DeviceId::new(metadata.dev())),
inode: Some(InodeId::new(metadata.ino())),
..owned_intent.clone()
};
fixture
.catalog
.bind_prepare_stage(&owned_intent, &owned)
.unwrap();
assert_eq!(
fixture.store.recover_prepare(&owned).unwrap(),
PrepareRecovery::RemovedOwnedStage
);
assert!(!fixture.root.join(&owned_stage).exists());
}
#[test]
fn reclaim_refuses_forged_identity_and_leases() {
let fixture = Fixture::new();
let bytes = layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"same",
target: None,
mode: 0o644,
}]);
let artifact = fixture
.store
.prepare(&fixture.request(70, &[(bytes, OCI_TAR)]))
.unwrap();
let lease = lazaret_core::records::ArtifactLease {
prepared_artifact_id: artifact.prepared_artifact_id,
domain_id: lazaret_core::identity::DomainId::new([71; 32]),
generation_id: lazaret_core::identity::GenerationId::new(1),
};
fixture.catalog.acquire_lease(&lease).unwrap();
assert!(matches!(
fixture.store.reclaim(&artifact),
2026-07-19 03:18:50 +09:00
Err(error @ PreparedLowerError::Catalog { .. })
if matches!(error.downcast_ref::<CatalogError>(), Some(CatalogError::LeasesPresent))
));
fixture.catalog.release_lease(&lease).unwrap();
let mut forged = artifact.clone();
forged.inode = InodeId::new(artifact.inode.get() + 1);
assert!(fixture.store.reclaim(&forged).is_err());
assert!(fixture.artifact_root(&artifact).exists());
}
#[test]
fn selected_gc_is_idempotent_only_at_the_durable_reclaim_boundary() {
let fixture = Fixture::new();
let artifact = fixture
.store
.prepare(&fixture.request(
71,
&[(
layer(&[Entry {
path: "payload",
kind: EntryType::Regular,
data: b"gc",
target: None,
mode: 0o644,
}]),
OCI_TAR,
)],
))
.unwrap();
let id = artifact.prepared_artifact_id;
2026-07-19 03:18:50 +09:00
assert!(matches!(
fixture.store.gc_artifacts(&[id, id]),
Err(PreparedLowerError::Reclaim { .. })
));
let lease = lazaret_core::records::ArtifactLease {
prepared_artifact_id: id,
domain_id: lazaret_core::identity::DomainId::new([72; 32]),
generation_id: lazaret_core::identity::GenerationId::new(1),
};
fixture.catalog.acquire_lease(&lease).unwrap();
assert!(fixture.store.gc_artifacts(&[id]).is_err());
fixture.catalog.release_lease(&lease).unwrap();
let result = fixture.store.gc_artifacts(&[id]).unwrap();
assert_eq!(result[0].prepared_artifact_id, id);
assert_eq!(result[0].outcome, ReclaimOutcome::Removed);
assert!(fixture.store.gc_artifacts(&[id]).is_err());
}
#[test]
fn destination_layout_is_materialized_and_canonical_in_artifact_identity() {
let fixture = Fixture::new();
let bytes = layer_with_owner(&[], 0, 0);
let mut first = fixture.request(72, &[(bytes.clone(), OCI_TAR)]);
first.id_map = id_map_with_container_root();
first.destinations = vec![
PreparedDestination {
path: "/workspace".into(),
kind: PreparedDestinationKind::Directory,
},
PreparedDestination {
path: "/run/lazaret/bin/entry".into(),
kind: PreparedDestinationKind::RegularFile,
},
PreparedDestination {
path: "/workspace".into(),
kind: PreparedDestinationKind::Directory,
},
];
let artifact = fixture.store.prepare(&first).unwrap();
let root = fixture.artifact_root(&artifact);
assert!(root.join("workspace").is_dir());
assert!(root.join("run/lazaret/bin/entry").is_file());
let mut reordered = fixture.request(73, &[(bytes.clone(), OCI_TAR)]);
reordered.id_map = id_map_with_container_root();
reordered.destinations = first.destinations.iter().rev().cloned().collect();
assert_eq!(fixture.store.prepare(&reordered).unwrap(), artifact);
let mut unequal = fixture.request(74, &[(bytes, OCI_TAR)]);
unequal.id_map = id_map_with_container_root();
unequal.destinations = vec![PreparedDestination {
path: "/opt/other".into(),
kind: PreparedDestinationKind::Directory,
}];
assert_ne!(
fixture
.store
.prepare(&unequal)
.unwrap()
.prepared_artifact_id,
artifact.prepared_artifact_id
);
}
#[test]
fn destination_type_conflict_refuses_before_publication() {
let fixture = Fixture::new();
let bytes = layer_with_owner(
&[Entry {
path: "opt/lazaret-state",
kind: EntryType::Regular,
data: b"conflict",
target: None,
mode: 0o644,
}],
0,
0,
);
let mut request = fixture.request(75, &[(bytes, OCI_TAR)]);
request.id_map = id_map_with_container_root();
request.destinations = vec![PreparedDestination {
path: "/opt/lazaret-state".into(),
kind: PreparedDestinationKind::Directory,
}];
assert!(fixture.store.prepare(&request).is_err());
assert!(
fixture
.catalog
.snapshot()
.unwrap()
.prepared_artifacts
.is_empty()
);
}
fn id_map_with_container_root() -> IdMap {
IdMap {
uid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::geteuid().as_raw(),
size: 1,
}],
gid: vec![IdMapRange {
container_id: 0,
namespace_id: rustix::process::getegid().as_raw(),
size: 1,
}],
}
}
pub(super) fn hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes.iter().fold(
String::with_capacity(bytes.len() * 2),
|mut output, byte| {
write!(output, "{byte:02x}").expect("writing to String cannot fail");
output
},
)
}