Compose selected State Anchor paths over prepared roots
This commit is contained in:
parent
71b0266cf2
commit
4dd2b150d2
5 changed files with 1575 additions and 163 deletions
|
|
@ -32,6 +32,18 @@ pub struct IdMap {
|
|||
pub gid: Vec<IdMapRange>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum PreparedDestinationKind {
|
||||
Directory,
|
||||
RegularFile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct PreparedDestination {
|
||||
pub path: PathBuf,
|
||||
pub kind: PreparedDestinationKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PreparationLimits {
|
||||
pub max_layers: usize,
|
||||
|
|
@ -189,11 +201,91 @@ pub(crate) fn apply_layout(
|
|||
bail!("layer {index} diff-ID mismatch");
|
||||
}
|
||||
validate_tar(&spool, id_map, limits, &mut usage)?;
|
||||
state.apply(&spool, rootfs)?;
|
||||
state.apply(&spool, rootfs, id_map)?;
|
||||
}
|
||||
state.finish(rootfs)
|
||||
}
|
||||
|
||||
pub(crate) fn materialize_destinations(
|
||||
root: &File,
|
||||
destinations: &[PreparedDestination],
|
||||
id_map: &IdMap,
|
||||
) -> Result<()> {
|
||||
if destinations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let uid = map_owner(0, &id_map.uid, "uid")?;
|
||||
let gid = map_owner(0, &id_map.gid, "gid")?;
|
||||
for destination in destinations {
|
||||
let relative = destination
|
||||
.path
|
||||
.strip_prefix("/")
|
||||
.context("prepared destination is not absolute")?;
|
||||
let relative = normalize_relative(relative)?;
|
||||
let (parent, leaf) = materialize_parent(root, &relative, uid, gid)?;
|
||||
let expected = match destination.kind {
|
||||
PreparedDestinationKind::Directory => FileType::Directory,
|
||||
PreparedDestinationKind::RegularFile => FileType::RegularFile,
|
||||
};
|
||||
match path_type(&parent, Path::new(&leaf))? {
|
||||
Some(actual) if actual == expected => {}
|
||||
Some(_) => bail!("prepared destination conflicts with the image layout"),
|
||||
None => match destination.kind {
|
||||
PreparedDestinationKind::Directory => {
|
||||
mkdirat(&parent, &leaf, Mode::from_raw_mode(0o755))?;
|
||||
set_materialized_metadata(open_component(&parent, &leaf)?, uid, gid)?;
|
||||
}
|
||||
PreparedDestinationKind::RegularFile => {
|
||||
let file = openat2(
|
||||
&parent,
|
||||
&leaf,
|
||||
OFlags::WRONLY
|
||||
| OFlags::CREATE
|
||||
| OFlags::EXCL
|
||||
| OFlags::CLOEXEC
|
||||
| OFlags::NOFOLLOW,
|
||||
Mode::from_raw_mode(0o755),
|
||||
secure_resolve(),
|
||||
)?;
|
||||
set_materialized_metadata(file, uid, gid)?;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn materialize_parent(root: &File, path: &Path, uid: u32, gid: u32) -> Result<(OwnedFd, OsString)> {
|
||||
let leaf = path
|
||||
.file_name()
|
||||
.context("prepared destination has no final component")?
|
||||
.to_os_string();
|
||||
let mut current = dup(root)?;
|
||||
for component in path.parent().into_iter().flat_map(Path::components) {
|
||||
let Component::Normal(name) = component else {
|
||||
bail!("prepared destination is not normalized");
|
||||
};
|
||||
match mkdirat(¤t, name, Mode::from_raw_mode(0o755)) {
|
||||
Ok(()) => set_materialized_metadata(open_component(¤t, name)?, uid, gid)?,
|
||||
Err(Errno::EXIST) => {}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
current = open_component(¤t, name)?;
|
||||
}
|
||||
Ok((current, leaf))
|
||||
}
|
||||
|
||||
fn set_materialized_metadata(descriptor: impl AsFd, uid: u32, gid: u32) -> Result<()> {
|
||||
fchown(
|
||||
&descriptor,
|
||||
Some(Uid::from_raw(uid)),
|
||||
Some(Gid::from_raw(gid)),
|
||||
)?;
|
||||
fchmod(&descriptor, Mode::from_raw_mode(0o755))?;
|
||||
futimens(&descriptor, ×tamps(0))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_json<T: serde::de::DeserializeOwned>(
|
||||
layout: &LayoutSource<'_>,
|
||||
path: &Path,
|
||||
|
|
@ -315,7 +407,8 @@ fn validate_tar(
|
|||
let mut paths = HashSet::new();
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = normalize_relative(&entry.path()?)?;
|
||||
let entry_type = entry.header().entry_type();
|
||||
let path = normalize_layer_path(&entry.path()?, entry_type)?;
|
||||
if !paths.insert(path.clone()) {
|
||||
bail!("policy {POLICY_NAME} rejects duplicate layer path");
|
||||
}
|
||||
|
|
@ -353,9 +446,8 @@ fn validate_tar(
|
|||
bail!("policy {POLICY_NAME} rejects PAX metadata");
|
||||
}
|
||||
}
|
||||
validate_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
||||
validate_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
||||
let entry_type = entry.header().entry_type();
|
||||
map_owner(entry.header().uid()?, &id_map.uid, "uid")?;
|
||||
map_owner(entry.header().gid()?, &id_map.gid, "gid")?;
|
||||
if !matches!(
|
||||
entry_type,
|
||||
EntryType::Regular
|
||||
|
|
@ -407,17 +499,35 @@ fn normalize_relative(path: &Path) -> Result<PathBuf> {
|
|||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn validate_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<()> {
|
||||
fn normalize_layer_path(path: &Path, entry_type: EntryType) -> Result<PathBuf> {
|
||||
match normalize_relative(path) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(_)
|
||||
if entry_type == EntryType::Directory
|
||||
&& path.components().next().is_some()
|
||||
&& path
|
||||
.components()
|
||||
.all(|component| component == Component::CurDir) =>
|
||||
{
|
||||
Ok(PathBuf::from("."))
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_owner(value: u64, ranges: &[IdMapRange], name: &str) -> Result<u32> {
|
||||
let Ok(value) = u32::try_from(value) else {
|
||||
bail!("{name} {value} is outside the target ID map");
|
||||
};
|
||||
if !ranges
|
||||
.iter()
|
||||
.any(|range| value >= range.container_id && value - range.container_id < range.size)
|
||||
{
|
||||
bail!("{name} {value} is outside the target ID map");
|
||||
for range in ranges {
|
||||
if value >= range.container_id && value - range.container_id < range.size {
|
||||
return range
|
||||
.namespace_id
|
||||
.checked_add(value - range.container_id)
|
||||
.context("mapped owner overflows");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
bail!("{name} {value} is outside the target ID map")
|
||||
}
|
||||
|
||||
fn validate_id_map(id_map: &IdMap) -> Result<()> {
|
||||
|
|
@ -483,7 +593,7 @@ struct LayerState {
|
|||
}
|
||||
|
||||
impl LayerState {
|
||||
fn apply(&mut self, tar_source: &File, root: &File) -> Result<()> {
|
||||
fn apply(&mut self, tar_source: &File, root: &File, id_map: &IdMap) -> Result<()> {
|
||||
apply_whiteouts(tar_source, root, &mut self.directories)?;
|
||||
let mut archive = tar::Archive::new(rewound(tar_source)?);
|
||||
let mut hardlinks = Vec::new();
|
||||
|
|
@ -497,7 +607,7 @@ impl LayerState {
|
|||
if name == ".wh..wh..opq" || name.starts_with(".wh.") {
|
||||
continue;
|
||||
}
|
||||
let metadata = EntryMetadata::from_header(path.clone(), entry.header())?;
|
||||
let metadata = EntryMetadata::from_header(path.clone(), entry.header(), id_map)?;
|
||||
match entry.header().entry_type() {
|
||||
EntryType::Regular | EntryType::Continuous => {
|
||||
forget_directory_tree(&mut self.directories, &path);
|
||||
|
|
@ -557,11 +667,11 @@ struct EntryMetadata {
|
|||
}
|
||||
|
||||
impl EntryMetadata {
|
||||
fn from_header(path: PathBuf, header: &Header) -> Result<Self> {
|
||||
fn from_header(path: PathBuf, header: &Header, id_map: &IdMap) -> Result<Self> {
|
||||
Ok(Self {
|
||||
path,
|
||||
uid: u32::try_from(header.uid()?).context("entry uid exceeds u32")?,
|
||||
gid: u32::try_from(header.gid()?).context("entry gid exceeds u32")?,
|
||||
uid: map_owner(header.uid()?, &id_map.uid, "uid")?,
|
||||
gid: map_owner(header.gid()?, &id_map.gid, "gid")?,
|
||||
mode: header.mode()?,
|
||||
mtime: header.mtime()?,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ mod layer;
|
|||
mod store;
|
||||
mod transport;
|
||||
|
||||
pub use layer::{IdMap, IdMapRange, PreparationLimits};
|
||||
pub use layer::{
|
||||
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
||||
};
|
||||
pub use store::{PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome};
|
||||
pub use transport::{OciRegistryTransport, Platform, RegistryCredentials, TransportedImage};
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ use rustix::fs::{
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::layer::{
|
||||
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, apply_layout, remove_children,
|
||||
IdMap, LayoutSource, POLICY_NAME, PreparationLimits, PreparedDestination,
|
||||
PreparedDestinationKind, apply_layout, materialize_destinations, remove_children,
|
||||
verify_layout,
|
||||
};
|
||||
use crate::transport::TransportedImage;
|
||||
|
|
@ -38,6 +39,7 @@ pub struct PrepareRequest {
|
|||
pub source_identity: Vec<u8>,
|
||||
pub selected_manifest_digest: [u8; 32],
|
||||
pub id_map: IdMap,
|
||||
pub destinations: Vec<PreparedDestination>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
|
@ -115,6 +117,22 @@ impl PreparedLowerStore {
|
|||
self
|
||||
}
|
||||
|
||||
/// Opens the exact published artifact beneath the retained prepared-lower root.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
|
||||
pub fn open_artifact_authority(&self, artifact: &PreparedArtifact) -> Result<File> {
|
||||
let catalog_artifact = self
|
||||
.catalog_artifact(artifact.prepared_artifact_id)?
|
||||
.context("prepared artifact is absent from the catalog")?;
|
||||
if catalog_artifact != *artifact {
|
||||
bail!("prepared artifact does not match the catalog");
|
||||
}
|
||||
self.require_artifact_identity(artifact)?;
|
||||
self.open_relative(&artifact_path(artifact.prepared_artifact_id))?
|
||||
.context("prepared artifact path is absent")
|
||||
}
|
||||
|
||||
/// Materializes and atomically publishes one verified immutable prepared lower.
|
||||
///
|
||||
/// # Errors
|
||||
|
|
@ -134,6 +152,7 @@ impl PreparedLowerStore {
|
|||
operation_id: PrepareOperationId,
|
||||
image: &TransportedImage,
|
||||
id_map: IdMap,
|
||||
destinations: Vec<PreparedDestination>,
|
||||
) -> Result<PreparedArtifact> {
|
||||
let layout = image
|
||||
.retained_layout()
|
||||
|
|
@ -144,6 +163,7 @@ impl PreparedLowerStore {
|
|||
source_identity: image.source_identity.clone(),
|
||||
selected_manifest_digest: image.selected_manifest_digest,
|
||||
id_map,
|
||||
destinations,
|
||||
};
|
||||
self.prepare_from_source(&request, &LayoutSource::Retained(layout.root()))
|
||||
}
|
||||
|
|
@ -194,6 +214,7 @@ impl PreparedLowerStore {
|
|||
return Err(error.into());
|
||||
}
|
||||
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
|
||||
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
|
||||
.and_then(|()| sync_tree(&stage))
|
||||
{
|
||||
if self
|
||||
|
|
@ -494,7 +515,13 @@ impl Identities {
|
|||
fn new(request: &PrepareRequest) -> Result<Self> {
|
||||
let source = SourceIdentity::new(request.source_identity.clone());
|
||||
let manifest = SelectedManifestDigest::new(request.selected_manifest_digest);
|
||||
let policy = PolicyId::new(hash_parts(&[b"lazaret-policy-v1", POLICY_NAME.as_bytes()]));
|
||||
let destinations = canonical_destinations(&request.destinations)?;
|
||||
let destination_bytes = destination_policy_bytes(&destinations);
|
||||
let policy = PolicyId::new(hash_parts(&[
|
||||
b"lazaret-policy-v2",
|
||||
POLICY_NAME.as_bytes(),
|
||||
&destination_bytes,
|
||||
]));
|
||||
let id_map_bytes = postcard::to_stdvec(&request.id_map)?;
|
||||
let id_map = IdMapDigest::new(hash_parts(&[b"lazaret-id-map-v1", &id_map_bytes]));
|
||||
let artifact_id = PreparedArtifactId::new(hash_parts(&[
|
||||
|
|
@ -555,6 +582,42 @@ impl Identities {
|
|||
}
|
||||
}
|
||||
|
||||
fn canonical_destinations(
|
||||
destinations: &[PreparedDestination],
|
||||
) -> Result<Vec<PreparedDestination>> {
|
||||
let mut destinations = destinations.to_vec();
|
||||
for destination in &destinations {
|
||||
let mut components = destination.path.components();
|
||||
if components.next() != Some(std::path::Component::RootDir)
|
||||
|| components.any(|component| !matches!(component, std::path::Component::Normal(_)))
|
||||
{
|
||||
bail!("prepared destination is not an absolute normalized path");
|
||||
}
|
||||
}
|
||||
destinations.sort();
|
||||
destinations.dedup();
|
||||
for pair in destinations.windows(2) {
|
||||
if pair[0].path == pair[1].path && pair[0].kind != pair[1].kind {
|
||||
bail!("prepared destination has conflicting types");
|
||||
}
|
||||
}
|
||||
Ok(destinations)
|
||||
}
|
||||
|
||||
fn destination_policy_bytes(destinations: &[PreparedDestination]) -> Vec<u8> {
|
||||
let mut bytes = b"lazaret-prepared-layout-policy-v1\0".to_vec();
|
||||
for destination in destinations {
|
||||
bytes.extend_from_slice(destination.path.as_os_str().as_bytes());
|
||||
bytes.push(0);
|
||||
bytes.push(match destination.kind {
|
||||
PreparedDestinationKind::Directory => b'd',
|
||||
PreparedDestinationKind::RegularFile => b'f',
|
||||
});
|
||||
bytes.push(0);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn artifact_from_operation(
|
||||
operation: &PrepareOperation,
|
||||
path: &Path,
|
||||
|
|
|
|||
|
|
@ -223,8 +223,10 @@ impl OciRegistryTransport {
|
|||
}
|
||||
verify_platform(&config_json, platform)?;
|
||||
validate_manifest(&manifest)?;
|
||||
let layout = LayoutWriter::create(destination)?;
|
||||
layout.write_verified_blob(&manifest.config.digest, config_json.as_bytes())?;
|
||||
let layout = LayoutWriter::create(destination).context("creating private OCI layout")?;
|
||||
layout
|
||||
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
|
||||
.context("writing verified OCI config")?;
|
||||
for descriptor in &manifest.layers {
|
||||
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
|
||||
if size > MAX_LAYER_BYTES {
|
||||
|
|
@ -235,23 +237,33 @@ impl OciRegistryTransport {
|
|||
layout.require_existing_blob(&descriptor.digest, size)?;
|
||||
continue;
|
||||
}
|
||||
let (file, temporary) = layout.create_blob(digest_component)?;
|
||||
let (file, temporary) = layout
|
||||
.create_blob(digest_component)
|
||||
.with_context(|| format!("creating OCI layer {}", descriptor.digest))?;
|
||||
let file = tokio::fs::File::from_std(file);
|
||||
if let Err(error) = client.pull_blob(&reference, descriptor, file).await {
|
||||
layout.remove_temporary_blob(&temporary);
|
||||
return Err(error)
|
||||
.with_context(|| format!("pulling OCI layer {}", descriptor.digest));
|
||||
}
|
||||
let observed = layout.temporary_blob_size(&temporary)?;
|
||||
let observed = layout
|
||||
.temporary_blob_size(&temporary)
|
||||
.with_context(|| format!("measuring OCI layer {}", descriptor.digest))?;
|
||||
if observed != size {
|
||||
bail!("OCI layer {} size mismatch", descriptor.digest);
|
||||
}
|
||||
layout.publish_blob(&temporary, digest_component)?;
|
||||
layout
|
||||
.publish_blob(&temporary, digest_component)
|
||||
.with_context(|| format!("publishing OCI layer {}", descriptor.digest))?;
|
||||
}
|
||||
let local_manifest = serde_json::to_vec(&manifest)?;
|
||||
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
|
||||
layout.write_verified_blob(&local_digest, &local_manifest)?;
|
||||
layout.write_layout_index(&local_digest, local_manifest.len())?;
|
||||
layout
|
||||
.write_verified_blob(&local_digest, &local_manifest)
|
||||
.context("writing verified OCI manifest")?;
|
||||
layout
|
||||
.write_layout_index(&local_digest, local_manifest.len())
|
||||
.context("writing OCI layout index")?;
|
||||
let selected_manifest_digest = parse_sha256(&local_digest)?;
|
||||
let source_identity = format!(
|
||||
"oci-v1\0{}\0{}\0{manifest_digest}",
|
||||
|
|
@ -406,13 +418,19 @@ impl LayoutWriter {
|
|||
bail!("OCI descriptor digest mismatch for {digest}");
|
||||
}
|
||||
let component = digest_component(digest)?;
|
||||
if self.blob_exists(component)? {
|
||||
if self
|
||||
.blob_exists(component)
|
||||
.context("checking verified OCI blob destination")?
|
||||
{
|
||||
return self.require_existing_blob(digest, bytes.len() as u64);
|
||||
}
|
||||
let (mut file, temporary) = self.create_blob(component)?;
|
||||
file.write_all(bytes)?;
|
||||
file.sync_all()?;
|
||||
let (mut file, temporary) = self
|
||||
.create_blob(component)
|
||||
.context("creating verified OCI blob")?;
|
||||
file.write_all(bytes).context("writing verified OCI blob")?;
|
||||
file.sync_all().context("syncing verified OCI blob")?;
|
||||
self.publish_blob(&temporary, component)
|
||||
.context("publishing verified OCI blob")
|
||||
}
|
||||
|
||||
fn blob_exists(&self, component: &str) -> Result<bool> {
|
||||
|
|
@ -552,11 +570,16 @@ fn open_directory(parent: &File, path: &Path) -> std::io::Result<File> {
|
|||
}
|
||||
|
||||
fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Result<File> {
|
||||
let mode = if flags.contains(OFlags::CREATE) {
|
||||
Mode::from_raw_mode(0o600)
|
||||
} else {
|
||||
Mode::empty()
|
||||
};
|
||||
openat2(
|
||||
parent,
|
||||
path,
|
||||
flags | OFlags::NOFOLLOW | OFlags::CLOEXEC,
|
||||
Mode::from_raw_mode(0o600),
|
||||
mode,
|
||||
confined_resolve(),
|
||||
)
|
||||
.map(File::from)
|
||||
|
|
@ -566,7 +589,11 @@ fn open_relative_file(parent: &File, path: &Path, flags: OFlags) -> std::io::Res
|
|||
fn write_relative_atomic(root: &File, path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
let name = path.file_name().context("layout file has no name")?;
|
||||
let parent_path = path.parent().unwrap_or(Path::new("."));
|
||||
let parent = open_directory(root, parent_path)?;
|
||||
let parent = if parent_path.as_os_str().is_empty() {
|
||||
File::from(rustix::io::dup(root)?)
|
||||
} else {
|
||||
open_directory(root, parent_path)?
|
||||
};
|
||||
let temporary = format!(".{}.partial", name.to_string_lossy());
|
||||
let mut file = open_relative_file(
|
||||
&parent,
|
||||
|
|
@ -709,6 +736,25 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn atomic_layout_write_accepts_a_root_level_destination() {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let root = File::open(tree.path()).unwrap();
|
||||
write_relative_atomic(&root, Path::new("oci-layout"), b"layout").unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read(tree.path().join("oci-layout")).unwrap(),
|
||||
b"layout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confined_read_open_uses_an_empty_creation_mode() {
|
||||
let tree = tempfile::tempdir().unwrap();
|
||||
let root = File::open(tree.path()).unwrap();
|
||||
let error = open_relative_file(&root, Path::new("absent"), OFlags::RDONLY).unwrap_err();
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preexisting_transport_parents_require_exact_private_mode() {
|
||||
for name in ["image-transports", "blobs", "sha256"] {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ use lazaret_core::identity::{
|
|||
};
|
||||
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
||||
use lazaret_prepared_lower::{
|
||||
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedLowerStore, ReclaimOutcome,
|
||||
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedDestination,
|
||||
PreparedDestinationKind, PreparedLowerStore, ReclaimOutcome,
|
||||
};
|
||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||
use serde_json::json;
|
||||
|
|
@ -59,6 +60,7 @@ impl Fixture {
|
|||
source_identity: b"oci-v1\0registry.invalid\0fixture\0sha256:source".to_vec(),
|
||||
selected_manifest_digest: manifest,
|
||||
id_map: current_id_map(),
|
||||
destinations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +87,55 @@ fn preexisting_fixed_children_require_exact_private_mode() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn published_artifact_authority_is_revalidated_beneath_the_retained_root() {
|
||||
let fixture = Fixture::new();
|
||||
let request = fixture.request(
|
||||
1,
|
||||
&[(
|
||||
layer(&[Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"content",
|
||||
target: None,
|
||||
mode: 0o644,
|
||||
}]),
|
||||
OCI_TAR,
|
||||
)],
|
||||
);
|
||||
let artifact = fixture.store.prepare(&request).unwrap();
|
||||
|
||||
let authority = fixture.store.open_artifact_authority(&artifact).unwrap();
|
||||
let metadata = authority.metadata().unwrap();
|
||||
assert_eq!(metadata.dev(), artifact.device.get());
|
||||
assert_eq!(metadata.ino(), artifact.inode.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaced_published_artifact_is_refused() {
|
||||
let fixture = Fixture::new();
|
||||
let request = fixture.request(
|
||||
1,
|
||||
&[(
|
||||
layer(&[Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"content",
|
||||
target: None,
|
||||
mode: 0o644,
|
||||
}]),
|
||||
OCI_TAR,
|
||||
)],
|
||||
);
|
||||
let artifact = fixture.store.prepare(&request).unwrap();
|
||||
let artifact_root = fixture.artifact_root(&artifact);
|
||||
let displaced = fixture.root.join("displaced-artifact");
|
||||
std::fs::rename(&artifact_root, &displaced).unwrap();
|
||||
std::fs::create_dir(&artifact_root).unwrap();
|
||||
|
||||
assert!(fixture.store.open_artifact_authority(&artifact).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
fn retained_fixed_child_bind_mount_is_refused_when_mount_setup_is_available() {
|
||||
|
|
@ -155,6 +206,10 @@ struct Entry<'a> {
|
|||
fn layer(entries: &[Entry<'_>]) -> Vec<u8> {
|
||||
let uid = rustix::process::geteuid().as_raw();
|
||||
let gid = rustix::process::getegid().as_raw();
|
||||
layer_with_owner(entries, uid, gid)
|
||||
}
|
||||
|
||||
fn layer_with_owner(entries: &[Entry<'_>], uid: u32, gid: u32) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
{
|
||||
let mut builder = Builder::new(&mut bytes);
|
||||
|
|
@ -514,6 +569,115 @@ fn layer_entry_matching_internal_spool_name_is_preserved() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_directory_layer_entry_is_applied() {
|
||||
let fixture = Fixture::new();
|
||||
let bytes = layer(&[
|
||||
Entry {
|
||||
path: ".",
|
||||
kind: EntryType::Directory,
|
||||
data: b"",
|
||||
target: None,
|
||||
mode: 0o755,
|
||||
},
|
||||
Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"content",
|
||||
target: None,
|
||||
mode: 0o644,
|
||||
},
|
||||
]);
|
||||
let request = fixture.request(5, &[(bytes, OCI_TAR)]);
|
||||
|
||||
let artifact = fixture.store.prepare(&request).unwrap();
|
||||
let root = fixture.artifact_root(&artifact);
|
||||
assert_eq!(std::fs::read(root.join("payload")).unwrap(), b"content");
|
||||
assert_eq!(
|
||||
root.metadata().unwrap().permissions().mode() & 0o7777,
|
||||
0o755
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn container_root_owner_maps_to_invoking_host_ids() {
|
||||
let fixture = Fixture::new();
|
||||
let bytes = layer_with_owner(
|
||||
&[Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"content",
|
||||
target: None,
|
||||
mode: 0o640,
|
||||
}],
|
||||
0,
|
||||
0,
|
||||
);
|
||||
let mut request = fixture.request(6, &[(bytes, OCI_TAR)]);
|
||||
request.id_map = 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,
|
||||
}],
|
||||
};
|
||||
|
||||
let artifact = fixture.store.prepare(&request).unwrap();
|
||||
let metadata = fixture
|
||||
.artifact_root(&artifact)
|
||||
.join("payload")
|
||||
.metadata()
|
||||
.unwrap();
|
||||
assert_eq!(metadata.uid(), rustix::process::geteuid().as_raw());
|
||||
assert_eq!(metadata.gid(), rustix::process::getegid().as_raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmapped_nonzero_owners_refuse_before_publication() {
|
||||
for (operation, uid, gid) in [(7, 1, 0), (8, 0, 1)] {
|
||||
let fixture = Fixture::new();
|
||||
let bytes = layer_with_owner(
|
||||
&[Entry {
|
||||
path: "payload",
|
||||
kind: EntryType::Regular,
|
||||
data: b"content",
|
||||
target: None,
|
||||
mode: 0o640,
|
||||
}],
|
||||
uid,
|
||||
gid,
|
||||
);
|
||||
let mut request = fixture.request(operation, &[(bytes, OCI_TAR)]);
|
||||
request.id_map = 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,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(fixture.store.prepare(&request).is_err());
|
||||
assert!(
|
||||
fixture
|
||||
.catalog
|
||||
.snapshot()
|
||||
.unwrap()
|
||||
.prepared_artifacts
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_opaque_whiteout_removes_only_lower_layer_entries() {
|
||||
let fixture = Fixture::new();
|
||||
|
|
@ -971,6 +1135,98 @@ fn reclaim_refuses_forged_identity_and_leases() {
|
|||
assert!(fixture.artifact_root(&artifact).exists());
|
||||
}
|
||||
|
||||
#[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,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue