Expose typed Prepared Lower failures
This commit is contained in:
parent
ee9201b057
commit
30cd36dcdd
9 changed files with 694 additions and 249 deletions
|
|
@ -17,7 +17,7 @@ use lazaret_linux::filesystem::namespace_identity;
|
||||||
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
use lazaret_linux::platform::{PlatformObservation, check_platform};
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
|
IdMap, IdMapRange, OciRegistryTransport, Platform, PreparedDestination,
|
||||||
PreparedDestinationKind, PreparedLowerStore, RegistryCredentials,
|
PreparedDestinationKind, PreparedLowerError, PreparedLowerStore, RegistryCredentials,
|
||||||
};
|
};
|
||||||
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
use lazaret_runtime_supervisor::network::{DnsProbeTarget, NetworkProbeRequest, TcpProbeTarget};
|
||||||
use lazaret_runtime_supervisor::sandbox::{
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
|
|
@ -36,18 +36,37 @@ use crate::sandbox_init::PRIVATE_SANDBOX_INIT_ROLE;
|
||||||
const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RuntimeSelectionError(String);
|
pub enum RuntimeSelectionError {
|
||||||
|
PreparedLower(PreparedLowerError),
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for RuntimeSelectionError {
|
impl fmt::Display for RuntimeSelectionError {
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
formatter.write_str(&self.0)
|
match self {
|
||||||
|
Self::PreparedLower(error) => error.fmt(formatter),
|
||||||
|
Self::Other(message) => formatter.write_str(message),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for RuntimeSelectionError {}
|
impl std::error::Error for RuntimeSelectionError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::PreparedLower(error) => Some(error),
|
||||||
|
Self::Other(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PreparedLowerError> for RuntimeSelectionError {
|
||||||
|
fn from(error: PreparedLowerError) -> Self {
|
||||||
|
Self::PreparedLower(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn failure(error: impl fmt::Display) -> RuntimeSelectionError {
|
fn failure(error: impl fmt::Display) -> RuntimeSelectionError {
|
||||||
RuntimeSelectionError(error.to_string())
|
RuntimeSelectionError::Other(error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InstalledProductPaths<'a> {
|
pub struct InstalledProductPaths<'a> {
|
||||||
|
|
@ -149,8 +168,7 @@ async fn select_runtime_for_platform(
|
||||||
Err(error) => return Err(failure(error)),
|
Err(error) => return Err(failure(error)),
|
||||||
}
|
}
|
||||||
let prepared =
|
let prepared =
|
||||||
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())
|
PreparedLowerStore::under_storage_root(configuration.storage_root(), catalog.clone())?;
|
||||||
.map_err(failure)?;
|
|
||||||
recover_before_cold_start(configuration, &store, &catalog, &prepared).map_err(failure)?;
|
recover_before_cold_start(configuration, &store, &catalog, &prepared).map_err(failure)?;
|
||||||
let image = OciRegistryTransport
|
let image = OciRegistryTransport
|
||||||
.pull_confined(
|
.pull_confined(
|
||||||
|
|
@ -161,7 +179,7 @@ async fn select_runtime_for_platform(
|
||||||
random_id()?,
|
random_id()?,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| failure(format!("pulling configured anonymous OCI image: {error:#}")))?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
let artifact = prepared
|
let artifact = prepared
|
||||||
.prepare_transported(
|
.prepare_transported(
|
||||||
PrepareOperationId::new(random_id()?),
|
PrepareOperationId::new(random_id()?),
|
||||||
|
|
@ -169,10 +187,10 @@ async fn select_runtime_for_platform(
|
||||||
rootless_id_map(),
|
rootless_id_map(),
|
||||||
prepared_destinations(configuration),
|
prepared_destinations(configuration),
|
||||||
)
|
)
|
||||||
.map_err(failure)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
let artifact_authority = prepared
|
let artifact_authority = prepared
|
||||||
.open_artifact_authority(&artifact)
|
.open_artifact_authority(&artifact)
|
||||||
.map_err(failure)?;
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
|
let artifact_label = PathBuf::from(std::ffi::OsString::from_vec(
|
||||||
configuration
|
configuration
|
||||||
.storage_root()
|
.storage_root()
|
||||||
|
|
@ -184,7 +202,9 @@ async fn select_runtime_for_platform(
|
||||||
.join(PathBuf::from(std::ffi::OsString::from_vec(
|
.join(PathBuf::from(std::ffi::OsString::from_vec(
|
||||||
artifact.path.as_bytes().to_vec(),
|
artifact.path.as_bytes().to_vec(),
|
||||||
)));
|
)));
|
||||||
image.cleanup().map_err(failure)?;
|
image
|
||||||
|
.cleanup()
|
||||||
|
.map_err(RuntimeSelectionError::PreparedLower)?;
|
||||||
|
|
||||||
let next_generation = next_generation(&store)?;
|
let next_generation = next_generation(&store)?;
|
||||||
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
|
let (coordinator_id, network_sidecar_id, sandbox_id) = distinct_generation_ids()?;
|
||||||
|
|
|
||||||
161
crates/lazaret-prepared-lower/src/error.rs
Normal file
161
crates/lazaret-prepared-lower/src/error.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
pub struct PreparedLowerSource(anyhow::Error);
|
||||||
|
|
||||||
|
impl fmt::Debug for PreparedLowerSource {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(formatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PreparedLowerSource {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
if formatter.alternate() {
|
||||||
|
write!(formatter, "{:#}", self.0)
|
||||||
|
} else {
|
||||||
|
self.0.fmt(formatter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PreparedLowerSource {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
Some(self.0.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! prepared_lower_errors {
|
||||||
|
($($variant:ident, $constructor:ident => $label:literal),+ $(,)?) => {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PreparedLowerError {
|
||||||
|
$($variant { source: PreparedLowerSource },)+
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreparedLowerError {
|
||||||
|
$(pub(crate) fn $constructor(source: impl Into<anyhow::Error>) -> Self {
|
||||||
|
Self::$variant {
|
||||||
|
source: PreparedLowerSource(source.into()),
|
||||||
|
}
|
||||||
|
})+
|
||||||
|
|
||||||
|
fn source_error(&self) -> &PreparedLowerSource {
|
||||||
|
match self {
|
||||||
|
$(Self::$variant { source } => source,)+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn downcast_ref<E>(&self) -> Option<&E>
|
||||||
|
where
|
||||||
|
E: fmt::Display + fmt::Debug + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.source_error().0.downcast_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PreparedLowerError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let label = match self {
|
||||||
|
$(Self::$variant { .. } => $label,)+
|
||||||
|
};
|
||||||
|
if formatter.alternate() {
|
||||||
|
write!(formatter, "{label}: {:#}", self.source_error())
|
||||||
|
} else {
|
||||||
|
write!(formatter, "{label}: {}", self.source_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared_lower_errors! {
|
||||||
|
Transport, transport => "OCI transport failed",
|
||||||
|
Authentication, authentication => "OCI authentication failed",
|
||||||
|
ManifestSelection, manifest_selection => "OCI manifest selection failed",
|
||||||
|
PlatformSelection, platform_selection => "OCI platform selection failed",
|
||||||
|
LayerValidation, layer_validation => "prepared layer validation failed",
|
||||||
|
Materialization, materialization => "prepared lower materialization failed",
|
||||||
|
Publication, publication => "prepared lower publication failed",
|
||||||
|
Catalog, catalog => "prepared lower catalog operation failed",
|
||||||
|
Locking, locking => "prepared lower lock acquisition failed",
|
||||||
|
Recovery, recovery => "prepared lower recovery failed",
|
||||||
|
Reclaim, reclaim => "prepared lower reclaim failed",
|
||||||
|
Confinement, confinement => "prepared lower confinement failed",
|
||||||
|
OperatingSystem, operating_system => "prepared lower operating-system operation failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PreparedLowerError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
Some(self.source_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PreparedLowerResult<T> = Result<T, PreparedLowerError>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::error::Error as _;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use lazaret_core::records::{PrepareOperation, PreparedArtifact};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
OciRegistryTransport, Platform, PrepareRequest, PreparedLowerStore, RegistryCredentials,
|
||||||
|
TransportedImage,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_public_failure_category_retains_its_source() {
|
||||||
|
let errors = [
|
||||||
|
PreparedLowerError::transport(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::authentication(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::manifest_selection(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::platform_selection(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::layer_validation(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::materialization(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::publication(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::catalog(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::locking(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::recovery(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::reclaim(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::confinement(anyhow::anyhow!("source")),
|
||||||
|
PreparedLowerError::operating_system(anyhow::anyhow!("source")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for error in errors {
|
||||||
|
assert!(error.source().is_some());
|
||||||
|
assert!(format!("{error:#}").contains("source"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exported_operations_have_typed_results() {
|
||||||
|
fn prepare(
|
||||||
|
store: &PreparedLowerStore,
|
||||||
|
request: &PrepareRequest,
|
||||||
|
) -> PreparedLowerResult<PreparedArtifact> {
|
||||||
|
store.prepare(request)
|
||||||
|
}
|
||||||
|
fn recover(
|
||||||
|
store: &PreparedLowerStore,
|
||||||
|
operation: &PrepareOperation,
|
||||||
|
) -> PreparedLowerResult<crate::PrepareRecovery> {
|
||||||
|
store.recover_prepare(operation)
|
||||||
|
}
|
||||||
|
async fn pull(
|
||||||
|
transport: &OciRegistryTransport,
|
||||||
|
reference: &str,
|
||||||
|
credentials: &RegistryCredentials,
|
||||||
|
platform: &Platform,
|
||||||
|
layout: &Path,
|
||||||
|
) -> PreparedLowerResult<TransportedImage> {
|
||||||
|
transport
|
||||||
|
.pull(reference, credentials, platform, layout)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = prepare;
|
||||||
|
let _ = recover;
|
||||||
|
let _ = pull;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
#![doc = "OCI transport, preparation, publication, and exact-owned reclaim for prepared lowers."]
|
#![doc = "OCI transport, preparation, publication, and exact-owned reclaim for prepared lowers."]
|
||||||
|
|
||||||
|
mod error;
|
||||||
mod layer;
|
mod layer;
|
||||||
mod store;
|
mod store;
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
|
pub use error::{PreparedLowerError, PreparedLowerResult, PreparedLowerSource};
|
||||||
pub use layer::{
|
pub use layer::{
|
||||||
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
IdMap, IdMapRange, PreparationLimits, PreparedDestination, PreparedDestinationKind,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ use crate::layer::{
|
||||||
verify_layout,
|
verify_layout,
|
||||||
};
|
};
|
||||||
use crate::transport::TransportedImage;
|
use crate::transport::TransportedImage;
|
||||||
|
use crate::{PreparedLowerError, PreparedLowerResult};
|
||||||
|
|
||||||
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
|
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
|
||||||
const LOCK_RETRY: Duration = Duration::from_millis(2);
|
const LOCK_RETRY: Duration = Duration::from_millis(2);
|
||||||
|
|
@ -74,7 +75,11 @@ impl PreparedLowerStore {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns an error if the root cannot be opened or any fixed path is not a real directory.
|
/// Returns an error if the root cannot be opened or any fixed path is not a real directory.
|
||||||
pub fn initialize(root: impl Into<PathBuf>, catalog: Catalog) -> Result<Self> {
|
pub fn initialize(root: impl Into<PathBuf>, catalog: Catalog) -> PreparedLowerResult<Self> {
|
||||||
|
Self::initialize_inner(root, catalog).map_err(PreparedLowerError::confinement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_inner(root: impl Into<PathBuf>, catalog: Catalog) -> Result<Self> {
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
let root = File::open(&root).context("opening Storage Root")?;
|
let root = File::open(&root).context("opening Storage Root")?;
|
||||||
Self::initialize_retained(root, catalog)
|
Self::initialize_retained(root, catalog)
|
||||||
|
|
@ -84,7 +89,15 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error if Storage Root authority drifts or a fixed child is not a real directory.
|
/// Returns an error if Storage Root authority drifts or a fixed child is not a real directory.
|
||||||
pub fn under_storage_root(storage_root: &ConfinedPathPlan, catalog: Catalog) -> Result<Self> {
|
pub fn under_storage_root(
|
||||||
|
storage_root: &ConfinedPathPlan,
|
||||||
|
catalog: Catalog,
|
||||||
|
) -> PreparedLowerResult<Self> {
|
||||||
|
Self::under_storage_root_inner(storage_root, catalog)
|
||||||
|
.map_err(PreparedLowerError::confinement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn under_storage_root_inner(storage_root: &ConfinedPathPlan, catalog: Catalog) -> Result<Self> {
|
||||||
let storage_root = File::from(
|
let storage_root = File::from(
|
||||||
storage_root
|
storage_root
|
||||||
.open_authority()
|
.open_authority()
|
||||||
|
|
@ -127,7 +140,15 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
|
/// Refuses a non-canonical artifact record, catalog drift, or physical identity drift.
|
||||||
pub fn open_artifact_authority(&self, artifact: &PreparedArtifact) -> Result<File> {
|
pub fn open_artifact_authority(
|
||||||
|
&self,
|
||||||
|
artifact: &PreparedArtifact,
|
||||||
|
) -> PreparedLowerResult<File> {
|
||||||
|
self.open_artifact_authority_inner(artifact)
|
||||||
|
.map_err(PreparedLowerError::confinement)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_artifact_authority_inner(&self, artifact: &PreparedArtifact) -> Result<File> {
|
||||||
let catalog_artifact = self
|
let catalog_artifact = self
|
||||||
.catalog_artifact(artifact.prepared_artifact_id)?
|
.catalog_artifact(artifact.prepared_artifact_id)?
|
||||||
.context("prepared artifact is absent from the catalog")?;
|
.context("prepared artifact is absent from the catalog")?;
|
||||||
|
|
@ -145,7 +166,7 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// Refuses corrupt or unsupported OCI input, forged physical ownership, catalog conflicts,
|
/// Refuses corrupt or unsupported OCI input, forged physical ownership, catalog conflicts,
|
||||||
/// resource limits, and publication failures.
|
/// resource limits, and publication failures.
|
||||||
pub fn prepare(&self, request: &PrepareRequest) -> Result<PreparedArtifact> {
|
pub fn prepare(&self, request: &PrepareRequest) -> PreparedLowerResult<PreparedArtifact> {
|
||||||
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path))
|
self.prepare_from_source(request, &LayoutSource::Ambient(&request.layout_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,10 +180,21 @@ impl PreparedLowerStore {
|
||||||
image: &TransportedImage,
|
image: &TransportedImage,
|
||||||
id_map: IdMap,
|
id_map: IdMap,
|
||||||
destinations: Vec<PreparedDestination>,
|
destinations: Vec<PreparedDestination>,
|
||||||
) -> Result<PreparedArtifact> {
|
) -> PreparedLowerResult<PreparedArtifact> {
|
||||||
|
self.prepare_transported_inner(operation_id, image, id_map, destinations)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_transported_inner(
|
||||||
|
&self,
|
||||||
|
operation_id: PrepareOperationId,
|
||||||
|
image: &TransportedImage,
|
||||||
|
id_map: IdMap,
|
||||||
|
destinations: Vec<PreparedDestination>,
|
||||||
|
) -> PreparedLowerResult<PreparedArtifact> {
|
||||||
let layout = image
|
let layout = image
|
||||||
.retained_layout()
|
.retained_layout()
|
||||||
.context("transported image has no retained layout authority")?;
|
.context("transported image has no retained layout authority")
|
||||||
|
.map_err(PreparedLowerError::confinement)?;
|
||||||
let request = PrepareRequest {
|
let request = PrepareRequest {
|
||||||
operation_id,
|
operation_id,
|
||||||
layout_path: image.layout_path.clone(),
|
layout_path: image.layout_path.clone(),
|
||||||
|
|
@ -178,32 +210,46 @@ impl PreparedLowerStore {
|
||||||
&self,
|
&self,
|
||||||
request: &PrepareRequest,
|
request: &PrepareRequest,
|
||||||
layout: &LayoutSource<'_>,
|
layout: &LayoutSource<'_>,
|
||||||
) -> Result<PreparedArtifact> {
|
) -> PreparedLowerResult<PreparedArtifact> {
|
||||||
if request.source_identity.is_empty() || request.source_identity.len() > 4096 {
|
if request.source_identity.is_empty() || request.source_identity.len() > 4096 {
|
||||||
bail!("source identity is empty or exceeds 4096 bytes");
|
return Err(PreparedLowerError::layer_validation(anyhow::anyhow!(
|
||||||
|
"source identity is empty or exceeds 4096 bytes"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let verified = verify_layout(layout, self.limits)?;
|
let verified =
|
||||||
|
verify_layout(layout, self.limits).map_err(PreparedLowerError::layer_validation)?;
|
||||||
if request.selected_manifest_digest != verified.manifest_digest {
|
if request.selected_manifest_digest != verified.manifest_digest {
|
||||||
bail!("selected manifest identity does not match verified OCI layout");
|
return Err(PreparedLowerError::manifest_selection(anyhow::anyhow!(
|
||||||
|
"selected manifest identity does not match verified OCI layout"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let identities = Identities::new(request)?;
|
let identities = Identities::new(request).map_err(PreparedLowerError::layer_validation)?;
|
||||||
if let Some(existing) = self.available_artifact(identities.artifact_id)? {
|
if let Some(existing) = self
|
||||||
identities.require_equivalent(&existing)?;
|
.available_artifact(identities.artifact_id)
|
||||||
self.require_artifact_identity(&existing)?;
|
.map_err(PreparedLowerError::catalog)?
|
||||||
|
{
|
||||||
|
identities
|
||||||
|
.require_equivalent(&existing)
|
||||||
|
.map_err(PreparedLowerError::catalog)?;
|
||||||
|
self.require_artifact_identity(&existing)
|
||||||
|
.map_err(PreparedLowerError::confinement)?;
|
||||||
return Ok(existing);
|
return Ok(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
let stage_relative = stage_path(request.operation_id);
|
let stage_relative = stage_path(request.operation_id);
|
||||||
let intent = identities.intent(request.operation_id, &stage_relative);
|
let intent = identities.intent(request.operation_id, &stage_relative);
|
||||||
self.catalog.record_prepare(&intent)?;
|
self.catalog
|
||||||
|
.record_prepare(&intent)
|
||||||
|
.map_err(PreparedLowerError::catalog)?;
|
||||||
let stage = match self.create_stage(&stage_relative) {
|
let stage = match self.create_stage(&stage_relative) {
|
||||||
Ok(stage) => stage,
|
Ok(stage) => stage,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = self.catalog.remove_prepare(&intent);
|
let _ = self.catalog.remove_prepare(&intent);
|
||||||
return Err(error);
|
return Err(PreparedLowerError::materialization(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (device, inode) = file_identity(&stage)?;
|
let (device, inode) =
|
||||||
|
file_identity(&stage).map_err(PreparedLowerError::operating_system)?;
|
||||||
let owned = PrepareOperation {
|
let owned = PrepareOperation {
|
||||||
stage_state: PrepareStageState::Owned,
|
stage_state: PrepareStageState::Owned,
|
||||||
device: Some(device),
|
device: Some(device),
|
||||||
|
|
@ -217,7 +263,7 @@ impl PreparedLowerStore {
|
||||||
{
|
{
|
||||||
let _ = self.catalog.remove_prepare(&intent);
|
let _ = self.catalog.remove_prepare(&intent);
|
||||||
}
|
}
|
||||||
return Err(error.into());
|
return Err(PreparedLowerError::catalog(error));
|
||||||
}
|
}
|
||||||
if let Err(error) = apply_layout(layout, &stage, &verified, &request.id_map, self.limits)
|
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(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
|
||||||
|
|
@ -229,9 +275,11 @@ impl PreparedLowerStore {
|
||||||
{
|
{
|
||||||
let _ = self.catalog.remove_prepare(&owned);
|
let _ = self.catalog.remove_prepare(&owned);
|
||||||
}
|
}
|
||||||
return Err(error);
|
return Err(PreparedLowerError::materialization(error));
|
||||||
}
|
}
|
||||||
self.publish_owned(&owned, &identities)
|
let physical_lock = self.physical_lock()?;
|
||||||
|
self.publish_owned(&owned, &identities, &physical_lock)
|
||||||
|
.map_err(PreparedLowerError::publication)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recovers one exact durable prepare operation without adopting foreign paths.
|
/// Recovers one exact durable prepare operation without adopting foreign paths.
|
||||||
|
|
@ -240,7 +288,15 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// Refuses unexpected paths, missing ownership evidence, identity mismatch, and catalog or
|
/// Refuses unexpected paths, missing ownership evidence, identity mismatch, and catalog or
|
||||||
/// filesystem failures.
|
/// filesystem failures.
|
||||||
pub fn recover_prepare(&self, operation: &PrepareOperation) -> Result<PrepareRecovery> {
|
pub fn recover_prepare(
|
||||||
|
&self,
|
||||||
|
operation: &PrepareOperation,
|
||||||
|
) -> PreparedLowerResult<PrepareRecovery> {
|
||||||
|
self.recover_prepare_inner(operation)
|
||||||
|
.map_err(PreparedLowerError::recovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recover_prepare_inner(&self, operation: &PrepareOperation) -> Result<PrepareRecovery> {
|
||||||
let expected_stage = stage_path(operation.operation_id);
|
let expected_stage = stage_path(operation.operation_id);
|
||||||
if operation.stage_path.as_bytes() != path_bytes(&expected_stage) {
|
if operation.stage_path.as_bytes() != path_bytes(&expected_stage) {
|
||||||
bail!("prepare operation stage path is not canonical");
|
bail!("prepare operation stage path is not canonical");
|
||||||
|
|
@ -281,12 +337,19 @@ impl PreparedLowerStore {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Stops at the first foreign, mismatched, unreadable, or changed operation without treating
|
/// Stops at the first foreign, mismatched, unreadable, or changed operation without treating
|
||||||
/// it as owned absence.
|
/// it as owned absence.
|
||||||
pub fn recover_preparations(&self) -> Result<Vec<(PrepareOperationId, PrepareRecovery)>> {
|
pub fn recover_preparations(
|
||||||
|
&self,
|
||||||
|
) -> PreparedLowerResult<Vec<(PrepareOperationId, PrepareRecovery)>> {
|
||||||
|
self.recover_preparations_inner()
|
||||||
|
.map_err(PreparedLowerError::recovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recover_preparations_inner(&self) -> Result<Vec<(PrepareOperationId, PrepareRecovery)>> {
|
||||||
let operations = self.catalog.snapshot()?.prepare_operations;
|
let operations = self.catalog.snapshot()?.prepare_operations;
|
||||||
operations
|
operations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|operation| {
|
.map(|operation| {
|
||||||
self.recover_prepare(operation)
|
self.recover_prepare_inner(operation)
|
||||||
.map(|outcome| (operation.operation_id, outcome))
|
.map(|outcome| (operation.operation_id, outcome))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -300,19 +363,32 @@ impl PreparedLowerStore {
|
||||||
pub fn gc_artifacts(
|
pub fn gc_artifacts(
|
||||||
&self,
|
&self,
|
||||||
selected: &[PreparedArtifactId],
|
selected: &[PreparedArtifactId],
|
||||||
) -> Result<Vec<PreparedArtifactGcResult>> {
|
) -> PreparedLowerResult<Vec<PreparedArtifactGcResult>> {
|
||||||
|
self.gc_artifacts_inner(selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gc_artifacts_inner(
|
||||||
|
&self,
|
||||||
|
selected: &[PreparedArtifactId],
|
||||||
|
) -> PreparedLowerResult<Vec<PreparedArtifactGcResult>> {
|
||||||
let mut unique = std::collections::HashSet::new();
|
let mut unique = std::collections::HashSet::new();
|
||||||
if selected.iter().any(|id| !unique.insert(*id)) {
|
if selected.iter().any(|id| !unique.insert(*id)) {
|
||||||
bail!("prepared-artifact GC target is duplicated");
|
return Err(PreparedLowerError::reclaim(anyhow::anyhow!(
|
||||||
|
"prepared-artifact GC target is duplicated"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
let snapshot = self.catalog.snapshot()?;
|
let snapshot = self
|
||||||
|
.catalog
|
||||||
|
.snapshot()
|
||||||
|
.map_err(PreparedLowerError::catalog)?;
|
||||||
let mut results = Vec::with_capacity(selected.len());
|
let mut results = Vec::with_capacity(selected.len());
|
||||||
for id in selected {
|
for id in selected {
|
||||||
let artifact = snapshot
|
let artifact = snapshot
|
||||||
.prepared_artifacts
|
.prepared_artifacts
|
||||||
.iter()
|
.iter()
|
||||||
.find(|artifact| artifact.prepared_artifact_id == *id)
|
.find(|artifact| artifact.prepared_artifact_id == *id)
|
||||||
.context("prepared-artifact GC target is absent")?;
|
.context("prepared-artifact GC target is absent")
|
||||||
|
.map_err(PreparedLowerError::reclaim)?;
|
||||||
let outcome = match artifact.state {
|
let outcome = match artifact.state {
|
||||||
ArtifactState::Available => self.reclaim(artifact)?,
|
ArtifactState::Available => self.reclaim(artifact)?,
|
||||||
ArtifactState::Reclaiming => self.resume_reclaim(artifact)?,
|
ArtifactState::Reclaiming => self.resume_reclaim(artifact)?,
|
||||||
|
|
@ -331,8 +407,11 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
|
/// Refuses leases, forged catalog rows, physical identity mismatch, foreign paths, and
|
||||||
/// filesystem or catalog failures.
|
/// filesystem or catalog failures.
|
||||||
pub fn reclaim(&self, expected: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
pub fn reclaim(&self, expected: &PreparedArtifact) -> PreparedLowerResult<ReclaimOutcome> {
|
||||||
let reclaiming = self.catalog.begin_reclaim(expected)?;
|
let reclaiming = self
|
||||||
|
.catalog
|
||||||
|
.begin_reclaim(expected)
|
||||||
|
.map_err(PreparedLowerError::catalog)?;
|
||||||
self.resume_reclaim(&reclaiming)
|
self.resume_reclaim(&reclaiming)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,7 +421,16 @@ impl PreparedLowerStore {
|
||||||
///
|
///
|
||||||
/// Refuses non-reclaiming state, path/identity mismatch, missing physical evidence, and
|
/// Refuses non-reclaiming state, path/identity mismatch, missing physical evidence, and
|
||||||
/// catalog or filesystem failures.
|
/// catalog or filesystem failures.
|
||||||
pub fn resume_reclaim(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
pub fn resume_reclaim(
|
||||||
|
&self,
|
||||||
|
reclaiming: &PreparedArtifact,
|
||||||
|
) -> PreparedLowerResult<ReclaimOutcome> {
|
||||||
|
let _physical_lock = self.physical_lock()?;
|
||||||
|
self.resume_reclaim_inner(reclaiming)
|
||||||
|
.map_err(PreparedLowerError::reclaim)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resume_reclaim_inner(&self, reclaiming: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
||||||
if reclaiming.state != ArtifactState::Reclaiming {
|
if reclaiming.state != ArtifactState::Reclaiming {
|
||||||
bail!("artifact is not reclaiming");
|
bail!("artifact is not reclaiming");
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +440,6 @@ impl PreparedLowerStore {
|
||||||
}
|
}
|
||||||
let quarantine = quarantine_path(reclaiming);
|
let quarantine = quarantine_path(reclaiming);
|
||||||
{
|
{
|
||||||
let _lock = self.physical_lock()?;
|
|
||||||
if let Some(canonical) = self.open_relative(&expected_artifact)? {
|
if let Some(canonical) = self.open_relative(&expected_artifact)? {
|
||||||
if file_identity(&canonical)? == (reclaiming.device, reclaiming.inode) {
|
if file_identity(&canonical)? == (reclaiming.device, reclaiming.inode) {
|
||||||
self.rename_relative_noreplace(&expected_artifact, &quarantine)?;
|
self.rename_relative_noreplace(&expected_artifact, &quarantine)?;
|
||||||
|
|
@ -395,10 +482,10 @@ impl PreparedLowerStore {
|
||||||
&self,
|
&self,
|
||||||
operation: &PrepareOperation,
|
operation: &PrepareOperation,
|
||||||
identities: &Identities,
|
identities: &Identities,
|
||||||
|
_physical_lock: &ExclusiveFileLock,
|
||||||
) -> Result<PreparedArtifact> {
|
) -> Result<PreparedArtifact> {
|
||||||
let stage = stage_path(operation.operation_id);
|
let stage = stage_path(operation.operation_id);
|
||||||
let artifact_path = artifact_path(operation.expected_artifact_id);
|
let artifact_path = artifact_path(operation.expected_artifact_id);
|
||||||
let _lock = self.physical_lock()?;
|
|
||||||
if let Some(existing) = self.open_relative(&artifact_path)? {
|
if let Some(existing) = self.open_relative(&artifact_path)? {
|
||||||
if let Some(row) = self.available_artifact(operation.expected_artifact_id)? {
|
if let Some(row) = self.available_artifact(operation.expected_artifact_id)? {
|
||||||
identities.require_equivalent(&row)?;
|
identities.require_equivalent(&row)?;
|
||||||
|
|
@ -526,20 +613,22 @@ impl PreparedLowerStore {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn physical_lock(&self) -> Result<ExclusiveFileLock> {
|
fn physical_lock(&self) -> PreparedLowerResult<ExclusiveFileLock> {
|
||||||
let deadline = Instant::now() + PHYSICAL_LOCK_WAIT;
|
let deadline = Instant::now() + PHYSICAL_LOCK_WAIT;
|
||||||
loop {
|
loop {
|
||||||
let file = open_lock_at(&self.root)?;
|
let file = open_lock_at(&self.root).map_err(PreparedLowerError::locking)?;
|
||||||
match try_exclusive_flock(file) {
|
match try_exclusive_flock(file) {
|
||||||
Ok(lock) => return Ok(lock),
|
Ok(lock) => return Ok(lock),
|
||||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now >= deadline {
|
if now >= deadline {
|
||||||
bail!("prepared-lower physical lock wait expired");
|
return Err(PreparedLowerError::locking(anyhow::anyhow!(
|
||||||
|
"prepared-lower physical lock wait expired"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
thread::sleep(LOCK_RETRY.min(deadline.saturating_duration_since(now)));
|
thread::sleep(LOCK_RETRY.min(deadline.saturating_duration_since(now)));
|
||||||
}
|
}
|
||||||
Err(error) => return Err(error.into()),
|
Err(error) => return Err(PreparedLowerError::locking(error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use anyhow::{Context, Result, bail};
|
||||||
use lazaret_core::configuration::ConfinedPathPlan;
|
use lazaret_core::configuration::ConfinedPathPlan;
|
||||||
use oci_client::Reference;
|
use oci_client::Reference;
|
||||||
use oci_client::client::{Client, ClientConfig};
|
use oci_client::client::{Client, ClientConfig};
|
||||||
|
use oci_client::errors::{OciDistributionError, OciErrorCode};
|
||||||
use oci_client::manifest::{IMAGE_LAYER_GZIP_MEDIA_TYPE, IMAGE_LAYER_MEDIA_TYPE, OciImageManifest};
|
use oci_client::manifest::{IMAGE_LAYER_GZIP_MEDIA_TYPE, IMAGE_LAYER_MEDIA_TYPE, OciImageManifest};
|
||||||
use oci_client::secrets::RegistryAuth;
|
use oci_client::secrets::RegistryAuth;
|
||||||
use rustix::fs::{
|
use rustix::fs::{
|
||||||
|
|
@ -19,6 +20,8 @@ use rustix::fs::{
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::{PreparedLowerError, PreparedLowerResult};
|
||||||
|
|
||||||
const OCI_LAYER_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
|
const OCI_LAYER_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd";
|
||||||
const MAX_CONFIG_BYTES: usize = 16 * 1024 * 1024;
|
const MAX_CONFIG_BYTES: usize = 16 * 1024 * 1024;
|
||||||
const MAX_LAYER_BYTES: u64 = 32 * 1024 * 1024 * 1024;
|
const MAX_LAYER_BYTES: u64 = 32 * 1024 * 1024 * 1024;
|
||||||
|
|
@ -97,11 +100,15 @@ impl TransportedImage {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Refuses a replacement path and reports any descriptor-relative cleanup failure.
|
/// Refuses a replacement path and reports any descriptor-relative cleanup failure.
|
||||||
pub fn cleanup(mut self) -> Result<()> {
|
pub fn cleanup(mut self) -> PreparedLowerResult<()> {
|
||||||
if let Some(layout) = &mut self.retained_layout {
|
self.cleanup_inner()
|
||||||
cleanup_retained_layout(layout)?;
|
.map_err(PreparedLowerError::publication)
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
|
fn cleanup_inner(&mut self) -> Result<()> {
|
||||||
|
self.retained_layout
|
||||||
|
.as_mut()
|
||||||
|
.map_or(Ok(()), cleanup_retained_layout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +134,7 @@ impl OciRegistryTransport {
|
||||||
credentials: &RegistryCredentials,
|
credentials: &RegistryCredentials,
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
layout_path: &Path,
|
layout_path: &Path,
|
||||||
) -> Result<TransportedImage> {
|
) -> PreparedLowerResult<TransportedImage> {
|
||||||
self.pull_with_config(
|
self.pull_with_config(
|
||||||
reference,
|
reference,
|
||||||
credentials,
|
credentials,
|
||||||
|
|
@ -150,7 +157,7 @@ impl OciRegistryTransport {
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
storage_root: &ConfinedPathPlan,
|
storage_root: &ConfinedPathPlan,
|
||||||
staging_id: [u8; 32],
|
staging_id: [u8; 32],
|
||||||
) -> Result<TransportedImage> {
|
) -> PreparedLowerResult<TransportedImage> {
|
||||||
self.pull_to(
|
self.pull_to(
|
||||||
reference,
|
reference,
|
||||||
credentials,
|
credentials,
|
||||||
|
|
@ -171,7 +178,7 @@ impl OciRegistryTransport {
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
layout_path: &Path,
|
layout_path: &Path,
|
||||||
config: ClientConfig,
|
config: ClientConfig,
|
||||||
) -> Result<TransportedImage> {
|
) -> PreparedLowerResult<TransportedImage> {
|
||||||
self.pull_to(
|
self.pull_to(
|
||||||
reference,
|
reference,
|
||||||
credentials,
|
credentials,
|
||||||
|
|
@ -189,9 +196,11 @@ impl OciRegistryTransport {
|
||||||
platform: &Platform,
|
platform: &Platform,
|
||||||
destination: LayoutDestination<'_>,
|
destination: LayoutDestination<'_>,
|
||||||
mut config: ClientConfig,
|
mut config: ClientConfig,
|
||||||
) -> Result<TransportedImage> {
|
) -> PreparedLowerResult<TransportedImage> {
|
||||||
validate_credentials(credentials)?;
|
validate_credentials(credentials).map_err(PreparedLowerError::authentication)?;
|
||||||
let reference = Reference::from_str(reference).context("invalid OCI image reference")?;
|
let reference = Reference::from_str(reference)
|
||||||
|
.context("invalid OCI image reference")
|
||||||
|
.map_err(PreparedLowerError::transport)?;
|
||||||
let selected_platform = platform.clone();
|
let selected_platform = platform.clone();
|
||||||
config.max_concurrent_download = 1;
|
config.max_concurrent_download = 1;
|
||||||
config.platform_resolver = Some(Box::new(move |entries| {
|
config.platform_resolver = Some(Box::new(move |entries| {
|
||||||
|
|
@ -217,54 +226,37 @@ impl OciRegistryTransport {
|
||||||
let (manifest, manifest_digest, config_json) = client
|
let (manifest, manifest_digest, config_json) = client
|
||||||
.pull_manifest_and_config(&reference, &auth)
|
.pull_manifest_and_config(&reference, &auth)
|
||||||
.await
|
.await
|
||||||
.context("pulling selected OCI manifest and config")?;
|
.map_err(|error| {
|
||||||
|
distribution_error(error, "pulling selected OCI manifest and config")
|
||||||
|
})?;
|
||||||
if config_json.len() > MAX_CONFIG_BYTES {
|
if config_json.len() > MAX_CONFIG_BYTES {
|
||||||
bail!("OCI config exceeds {MAX_CONFIG_BYTES} bytes");
|
return Err(PreparedLowerError::manifest_selection(anyhow::anyhow!(
|
||||||
|
"OCI config exceeds {MAX_CONFIG_BYTES} bytes"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
verify_platform(&config_json, platform)?;
|
verify_platform(&config_json, platform).map_err(PreparedLowerError::platform_selection)?;
|
||||||
validate_manifest(&manifest)?;
|
validate_manifest(&manifest).map_err(PreparedLowerError::manifest_selection)?;
|
||||||
let layout = LayoutWriter::create(destination).context("creating private OCI layout")?;
|
let layout = LayoutWriter::create(destination)
|
||||||
|
.context("creating private OCI layout")
|
||||||
|
.map_err(PreparedLowerError::confinement)?;
|
||||||
layout
|
layout
|
||||||
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
|
.write_verified_blob(&manifest.config.digest, config_json.as_bytes())
|
||||||
.context("writing verified OCI config")?;
|
.context("writing verified OCI config")
|
||||||
for descriptor in &manifest.layers {
|
.map_err(PreparedLowerError::publication)?;
|
||||||
let size = u64::try_from(descriptor.size).context("negative OCI layer size")?;
|
pull_layers(&client, &reference, &manifest, &layout).await?;
|
||||||
if size > MAX_LAYER_BYTES {
|
let local_manifest =
|
||||||
bail!("OCI layer exceeds {MAX_LAYER_BYTES} bytes");
|
serde_json::to_vec(&manifest).map_err(PreparedLowerError::manifest_selection)?;
|
||||||
}
|
|
||||||
let digest_component = digest_component(&descriptor.digest)?;
|
|
||||||
if layout.blob_exists(digest_component)? {
|
|
||||||
layout.require_existing_blob(&descriptor.digest, size)?;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
.with_context(|| format!("measuring OCI layer {}", descriptor.digest))?;
|
|
||||||
if observed != size {
|
|
||||||
bail!("OCI layer {} size mismatch", descriptor.digest);
|
|
||||||
}
|
|
||||||
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)));
|
let local_digest = format!("sha256:{}", hex(&Sha256::digest(&local_manifest)));
|
||||||
layout
|
layout
|
||||||
.write_verified_blob(&local_digest, &local_manifest)
|
.write_verified_blob(&local_digest, &local_manifest)
|
||||||
.context("writing verified OCI manifest")?;
|
.context("writing verified OCI manifest")
|
||||||
|
.map_err(PreparedLowerError::publication)?;
|
||||||
layout
|
layout
|
||||||
.write_layout_index(&local_digest, local_manifest.len())
|
.write_layout_index(&local_digest, local_manifest.len())
|
||||||
.context("writing OCI layout index")?;
|
.context("writing OCI layout index")
|
||||||
let selected_manifest_digest = parse_sha256(&local_digest)?;
|
.map_err(PreparedLowerError::publication)?;
|
||||||
|
let selected_manifest_digest =
|
||||||
|
parse_sha256(&local_digest).map_err(PreparedLowerError::manifest_selection)?;
|
||||||
let source_identity = format!(
|
let source_identity = format!(
|
||||||
"oci-v1\0{}\0{}\0{manifest_digest}",
|
"oci-v1\0{}\0{}\0{manifest_digest}",
|
||||||
reference.registry(),
|
reference.registry(),
|
||||||
|
|
@ -280,6 +272,115 @@ impl OciRegistryTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn pull_layers(
|
||||||
|
client: &Client,
|
||||||
|
reference: &Reference,
|
||||||
|
manifest: &OciImageManifest,
|
||||||
|
layout: &LayoutWriter,
|
||||||
|
) -> PreparedLowerResult<()> {
|
||||||
|
for descriptor in &manifest.layers {
|
||||||
|
let size = u64::try_from(descriptor.size)
|
||||||
|
.context("negative OCI layer size")
|
||||||
|
.map_err(PreparedLowerError::manifest_selection)?;
|
||||||
|
if size > MAX_LAYER_BYTES {
|
||||||
|
return Err(PreparedLowerError::layer_validation(anyhow::anyhow!(
|
||||||
|
"OCI layer exceeds {MAX_LAYER_BYTES} bytes"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let digest_component =
|
||||||
|
digest_component(&descriptor.digest).map_err(PreparedLowerError::manifest_selection)?;
|
||||||
|
if layout
|
||||||
|
.blob_exists(digest_component)
|
||||||
|
.map_err(PreparedLowerError::operating_system)?
|
||||||
|
{
|
||||||
|
layout
|
||||||
|
.require_existing_blob(&descriptor.digest, size)
|
||||||
|
.map_err(PreparedLowerError::layer_validation)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (file, temporary) = layout
|
||||||
|
.create_blob(digest_component)
|
||||||
|
.with_context(|| format!("creating OCI layer {}", descriptor.digest))
|
||||||
|
.map_err(PreparedLowerError::materialization)?;
|
||||||
|
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(distribution_error(
|
||||||
|
error,
|
||||||
|
format!("pulling OCI layer {}", descriptor.digest),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let observed = layout
|
||||||
|
.temporary_blob_size(&temporary)
|
||||||
|
.with_context(|| format!("measuring OCI layer {}", descriptor.digest))
|
||||||
|
.map_err(PreparedLowerError::operating_system)?;
|
||||||
|
if observed != size {
|
||||||
|
return Err(PreparedLowerError::layer_validation(anyhow::anyhow!(
|
||||||
|
"OCI layer {} size mismatch",
|
||||||
|
descriptor.digest
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
layout
|
||||||
|
.publish_blob(&temporary, digest_component)
|
||||||
|
.with_context(|| format!("publishing OCI layer {}", descriptor.digest))
|
||||||
|
.map_err(PreparedLowerError::publication)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distribution_error(
|
||||||
|
error: OciDistributionError,
|
||||||
|
context: impl Into<String>,
|
||||||
|
) -> PreparedLowerError {
|
||||||
|
let authentication = matches!(
|
||||||
|
&error,
|
||||||
|
OciDistributionError::AuthenticationFailure(_)
|
||||||
|
| OciDistributionError::RegistryTokenDecodeError(_)
|
||||||
|
| OciDistributionError::UnauthorizedError { .. }
|
||||||
|
| OciDistributionError::ServerError {
|
||||||
|
code: 401 | 403,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) || matches!(
|
||||||
|
&error,
|
||||||
|
OciDistributionError::RegistryError { envelope, .. }
|
||||||
|
if envelope.errors.iter().any(|error| {
|
||||||
|
matches!(error.code, OciErrorCode::Unauthorized | OciErrorCode::Denied)
|
||||||
|
})
|
||||||
|
) || matches!(
|
||||||
|
&error,
|
||||||
|
OciDistributionError::RequestError(error)
|
||||||
|
if error.status().is_some_and(|status| matches!(status.as_u16(), 401 | 403))
|
||||||
|
);
|
||||||
|
let platform = matches!(
|
||||||
|
&error,
|
||||||
|
OciDistributionError::ImageIndexParsingNoPlatformResolverError
|
||||||
|
);
|
||||||
|
let manifest = matches!(
|
||||||
|
&error,
|
||||||
|
OciDistributionError::ConfigConversionError(_)
|
||||||
|
| OciDistributionError::ImageManifestNotFoundError(_)
|
||||||
|
| OciDistributionError::IncompatibleLayerMediaTypeError(_)
|
||||||
|
| OciDistributionError::JsonError(_)
|
||||||
|
| OciDistributionError::ManifestEncodingError(_)
|
||||||
|
| OciDistributionError::ManifestParsingError(_)
|
||||||
|
| OciDistributionError::PullNoLayersError
|
||||||
|
| OciDistributionError::UnsupportedMediaTypeError(_)
|
||||||
|
| OciDistributionError::UnsupportedSchemaVersionError(_)
|
||||||
|
| OciDistributionError::VersionedParsingError(_)
|
||||||
|
);
|
||||||
|
let source = anyhow::Error::new(error).context(context.into());
|
||||||
|
if authentication {
|
||||||
|
PreparedLowerError::authentication(source)
|
||||||
|
} else if platform {
|
||||||
|
PreparedLowerError::platform_selection(source)
|
||||||
|
} else if manifest {
|
||||||
|
PreparedLowerError::manifest_selection(source)
|
||||||
|
} else {
|
||||||
|
PreparedLowerError::transport(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ImageConfigPlatform {
|
struct ImageConfigPlatform {
|
||||||
os: String,
|
os: String,
|
||||||
|
|
@ -726,146 +827,4 @@ fn hex(bytes: &[u8]) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
include!("transport/tests.rs");
|
||||||
mod tests {
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::net::TcpListener;
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
|
|
||||||
use oci_client::client::ClientProtocol;
|
|
||||||
|
|
||||||
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"] {
|
|
||||||
let tree = tempfile::tempdir().unwrap();
|
|
||||||
std::fs::create_dir(tree.path().join(name)).unwrap();
|
|
||||||
std::fs::set_permissions(
|
|
||||||
tree.path().join(name),
|
|
||||||
std::fs::Permissions::from_mode(0o777),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let parent = File::open(tree.path()).unwrap();
|
|
||||||
assert!(
|
|
||||||
create_directory(&parent, name).is_err(),
|
|
||||||
"accepted wrong mode for {name}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn refuses_mismatched_and_incomplete_platforms() {
|
|
||||||
assert!(
|
|
||||||
verify_platform(
|
|
||||||
r#"{"os":"linux","architecture":"amd64"}"#,
|
|
||||||
&Platform::linux("amd64")
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
verify_platform(
|
|
||||||
r#"{"os":"linux","architecture":"arm64"}"#,
|
|
||||||
&Platform::linux("amd64")
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
verify_platform(
|
|
||||||
r#"{"os":"linux","architecture":"amd64","variant":"v2"}"#,
|
|
||||||
&Platform::linux("amd64")
|
|
||||||
)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
assert!(verify_platform(r#"{"architecture":"amd64"}"#, &Platform::linux("amd64")).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn incomplete_authentication_refuses_without_layout_or_secret_disclosure() {
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let temp = tempfile::tempdir().unwrap();
|
|
||||||
let result = runtime.block_on(OciRegistryTransport.pull_with_config(
|
|
||||||
"registry.invalid/private/image:latest",
|
|
||||||
&RegistryCredentials::Basic {
|
|
||||||
username: "developer".to_owned(),
|
|
||||||
password: String::new(),
|
|
||||||
},
|
|
||||||
&Platform::linux("amd64"),
|
|
||||||
&temp.path().join("layout"),
|
|
||||||
ClientConfig::default(),
|
|
||||||
));
|
|
||||||
let error = result.unwrap_err().to_string();
|
|
||||||
assert!(error.contains("credentials are incomplete"));
|
|
||||||
assert!(!error.contains("developer"));
|
|
||||||
assert!(!temp.path().join("layout").exists());
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{:?}",
|
|
||||||
RegistryCredentials::Bearer("not-a-real-token".to_owned())
|
|
||||||
),
|
|
||||||
"Bearer(\"[redacted]\")"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn registry_authentication_failure_refuses_before_layout_creation() {
|
|
||||||
let listener = match TcpListener::bind("127.0.0.1:0") {
|
|
||||||
Ok(listener) => listener,
|
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return,
|
|
||||||
Err(error) => panic!("binding test registry: {error}"),
|
|
||||||
};
|
|
||||||
let address = listener.local_addr().unwrap();
|
|
||||||
let server = std::thread::spawn(move || {
|
|
||||||
let (mut stream, _) = listener.accept().unwrap();
|
|
||||||
let mut request = [0; 4096];
|
|
||||||
assert!(stream.read(&mut request).unwrap() > 0);
|
|
||||||
stream
|
|
||||||
.write_all(
|
|
||||||
b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let temp = tempfile::tempdir().unwrap();
|
|
||||||
let result = runtime.block_on(OciRegistryTransport.pull_with_config(
|
|
||||||
&format!("{address}/private/image:latest"),
|
|
||||||
&RegistryCredentials::Basic {
|
|
||||||
username: "wrong".to_owned(),
|
|
||||||
password: "wrong".to_owned(),
|
|
||||||
},
|
|
||||||
&Platform::linux("amd64"),
|
|
||||||
&temp.path().join("layout"),
|
|
||||||
ClientConfig {
|
|
||||||
protocol: ClientProtocol::Http,
|
|
||||||
..ClientConfig::default()
|
|
||||||
},
|
|
||||||
));
|
|
||||||
assert!(result.is_err());
|
|
||||||
assert!(!temp.path().join("layout").exists());
|
|
||||||
server.join().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
193
crates/lazaret-prepared-lower/src/transport/tests.rs
Normal file
193
crates/lazaret-prepared-lower/src/transport/tests.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::TcpListener;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use oci_client::client::ClientProtocol;
|
||||||
|
|
||||||
|
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"] {
|
||||||
|
let tree = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir(tree.path().join(name)).unwrap();
|
||||||
|
std::fs::set_permissions(
|
||||||
|
tree.path().join(name),
|
||||||
|
std::fs::Permissions::from_mode(0o777),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let parent = File::open(tree.path()).unwrap();
|
||||||
|
assert!(
|
||||||
|
create_directory(&parent, name).is_err(),
|
||||||
|
"accepted wrong mode for {name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuses_mismatched_and_incomplete_platforms() {
|
||||||
|
assert!(
|
||||||
|
verify_platform(
|
||||||
|
r#"{"os":"linux","architecture":"amd64"}"#,
|
||||||
|
&Platform::linux("amd64")
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
verify_platform(
|
||||||
|
r#"{"os":"linux","architecture":"arm64"}"#,
|
||||||
|
&Platform::linux("amd64")
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
verify_platform(
|
||||||
|
r#"{"os":"linux","architecture":"amd64","variant":"v2"}"#,
|
||||||
|
&Platform::linux("amd64")
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert!(verify_platform(r#"{"architecture":"amd64"}"#, &Platform::linux("amd64")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn incomplete_authentication_refuses_without_layout_or_secret_disclosure() {
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let result = runtime.block_on(OciRegistryTransport.pull_with_config(
|
||||||
|
"registry.invalid/private/image:latest",
|
||||||
|
&RegistryCredentials::Basic {
|
||||||
|
username: "developer".to_owned(),
|
||||||
|
password: String::new(),
|
||||||
|
},
|
||||||
|
&Platform::linux("amd64"),
|
||||||
|
&temp.path().join("layout"),
|
||||||
|
ClientConfig::default(),
|
||||||
|
));
|
||||||
|
let error = result.unwrap_err();
|
||||||
|
assert!(matches!(error, PreparedLowerError::Authentication { .. }));
|
||||||
|
let error = error.to_string();
|
||||||
|
assert!(error.contains("credentials are incomplete"));
|
||||||
|
assert!(!error.contains("developer"));
|
||||||
|
assert!(!temp.path().join("layout").exists());
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"{:?}",
|
||||||
|
RegistryCredentials::Bearer("not-a-real-token".to_owned())
|
||||||
|
),
|
||||||
|
"Bearer(\"[redacted]\")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_authentication_failure_refuses_before_layout_creation() {
|
||||||
|
let listener = match TcpListener::bind("127.0.0.1:0") {
|
||||||
|
Ok(listener) => listener,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => return,
|
||||||
|
Err(error) => panic!("binding test registry: {error}"),
|
||||||
|
};
|
||||||
|
let address = listener.local_addr().unwrap();
|
||||||
|
listener.set_nonblocking(true).unwrap();
|
||||||
|
let stop = Arc::new(AtomicBool::new(false));
|
||||||
|
let server_stop = Arc::clone(&stop);
|
||||||
|
let server = std::thread::spawn(move || {
|
||||||
|
while !server_stop.load(Ordering::Relaxed) {
|
||||||
|
match listener.accept() {
|
||||||
|
Ok((mut stream, _)) => {
|
||||||
|
let mut request = [0; 4096];
|
||||||
|
assert!(stream.read(&mut request).unwrap() > 0);
|
||||||
|
stream
|
||||||
|
.write_all(
|
||||||
|
b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
}
|
||||||
|
Err(error) => panic!("accepting test registry request: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let result = runtime.block_on(OciRegistryTransport.pull_with_config(
|
||||||
|
&format!("{address}/private/image:latest"),
|
||||||
|
&RegistryCredentials::Basic {
|
||||||
|
username: "wrong".to_owned(),
|
||||||
|
password: "wrong".to_owned(),
|
||||||
|
},
|
||||||
|
&Platform::linux("amd64"),
|
||||||
|
&temp.path().join("layout"),
|
||||||
|
ClientConfig {
|
||||||
|
protocol: ClientProtocol::Http,
|
||||||
|
..ClientConfig::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(PreparedLowerError::Authentication { .. })),
|
||||||
|
"{result:?}"
|
||||||
|
);
|
||||||
|
assert!(!temp.path().join("layout").exists());
|
||||||
|
stop.store(true, Ordering::Relaxed);
|
||||||
|
server.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_failures_map_to_stable_caller_categories() {
|
||||||
|
assert!(matches!(
|
||||||
|
distribution_error(
|
||||||
|
OciDistributionError::AuthenticationFailure("denied".into()),
|
||||||
|
"pull"
|
||||||
|
),
|
||||||
|
PreparedLowerError::Authentication { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
distribution_error(
|
||||||
|
OciDistributionError::ImageIndexParsingNoPlatformResolverError,
|
||||||
|
"pull"
|
||||||
|
),
|
||||||
|
PreparedLowerError::PlatformSelection { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
distribution_error(
|
||||||
|
OciDistributionError::ManifestParsingError("invalid".into()),
|
||||||
|
"pull"
|
||||||
|
),
|
||||||
|
PreparedLowerError::ManifestSelection { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
distribution_error(OciDistributionError::GenericError(None), "pull"),
|
||||||
|
PreparedLowerError::Transport { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ use lazaret_core::identity::{
|
||||||
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
use lazaret_core::records::{PrepareOperation, PrepareStageState};
|
||||||
use lazaret_prepared_lower::{
|
use lazaret_prepared_lower::{
|
||||||
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedDestination,
|
IdMap, IdMapRange, PrepareRecovery, PrepareRequest, PreparedDestination,
|
||||||
PreparedDestinationKind, PreparedLowerStore, ReclaimOutcome,
|
PreparedDestinationKind, PreparedLowerError, PreparedLowerStore, ReclaimOutcome,
|
||||||
};
|
};
|
||||||
use lazaret_storage_catalog::{Catalog, CatalogError};
|
use lazaret_storage_catalog::{Catalog, CatalogError};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
@ -81,7 +81,10 @@ fn preexisting_fixed_children_require_exact_private_mode() {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
PreparedLowerStore::initialize(&fixture.root, fixture.catalog.clone()).is_err(),
|
matches!(
|
||||||
|
PreparedLowerStore::initialize(&fixture.root, fixture.catalog.clone()),
|
||||||
|
Err(PreparedLowerError::Confinement { .. })
|
||||||
|
),
|
||||||
"accepted wrong mode for {name}"
|
"accepted wrong mode for {name}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,10 @@ fn recovery_deletes_only_exact_owned_stage_and_preserves_foreign_stage() {
|
||||||
fixture.catalog.record_prepare(&intent).unwrap();
|
fixture.catalog.record_prepare(&intent).unwrap();
|
||||||
std::fs::create_dir(fixture.root.join(&stage)).unwrap();
|
std::fs::create_dir(fixture.root.join(&stage)).unwrap();
|
||||||
std::fs::write(fixture.root.join(&stage).join("foreign"), b"keep").unwrap();
|
std::fs::write(fixture.root.join(&stage).join("foreign"), b"keep").unwrap();
|
||||||
assert!(fixture.store.recover_prepare(&intent).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.recover_prepare(&intent),
|
||||||
|
Err(PreparedLowerError::Recovery { .. })
|
||||||
|
));
|
||||||
assert!(fixture.root.join(&stage).join("foreign").exists());
|
assert!(fixture.root.join(&stage).join("foreign").exists());
|
||||||
std::fs::remove_dir_all(fixture.root.join(&stage)).unwrap();
|
std::fs::remove_dir_all(fixture.root.join(&stage)).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -337,7 +340,7 @@ fn reclaim_refuses_forged_identity_and_leases() {
|
||||||
fixture.catalog.acquire_lease(&lease).unwrap();
|
fixture.catalog.acquire_lease(&lease).unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
fixture.store.reclaim(&artifact),
|
fixture.store.reclaim(&artifact),
|
||||||
Err(error)
|
Err(error @ PreparedLowerError::Catalog { .. })
|
||||||
if matches!(error.downcast_ref::<CatalogError>(), Some(CatalogError::LeasesPresent))
|
if matches!(error.downcast_ref::<CatalogError>(), Some(CatalogError::LeasesPresent))
|
||||||
));
|
));
|
||||||
fixture.catalog.release_lease(&lease).unwrap();
|
fixture.catalog.release_lease(&lease).unwrap();
|
||||||
|
|
@ -367,7 +370,10 @@ fn selected_gc_is_idempotent_only_at_the_durable_reclaim_boundary() {
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let id = artifact.prepared_artifact_id;
|
let id = artifact.prepared_artifact_id;
|
||||||
assert!(fixture.store.gc_artifacts(&[id, id]).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.gc_artifacts(&[id, id]),
|
||||||
|
Err(PreparedLowerError::Reclaim { .. })
|
||||||
|
));
|
||||||
|
|
||||||
let lease = lazaret_core::records::ArtifactLease {
|
let lease = lazaret_core::records::ArtifactLease {
|
||||||
prepared_artifact_id: id,
|
prepared_artifact_id: id,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,10 @@ fn request_manifest_identity_must_match_verified_layout_manifest() {
|
||||||
let mut request = fixture.request(3, &[(bytes, OCI_TAR)]);
|
let mut request = fixture.request(3, &[(bytes, OCI_TAR)]);
|
||||||
request.selected_manifest_digest = [0xff; 32];
|
request.selected_manifest_digest = [0xff; 32];
|
||||||
|
|
||||||
assert!(fixture.store.prepare(&request).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.prepare(&request),
|
||||||
|
Err(PreparedLowerError::ManifestSelection { .. })
|
||||||
|
));
|
||||||
let snapshot = fixture.catalog.snapshot().unwrap();
|
let snapshot = fixture.catalog.snapshot().unwrap();
|
||||||
assert!(snapshot.prepared_artifacts.is_empty());
|
assert!(snapshot.prepared_artifacts.is_empty());
|
||||||
assert!(snapshot.prepare_operations.is_empty());
|
assert!(snapshot.prepare_operations.is_empty());
|
||||||
|
|
@ -191,7 +194,10 @@ fn unmapped_nonzero_owners_refuse_before_publication() {
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(fixture.store.prepare(&request).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.prepare(&request),
|
||||||
|
Err(PreparedLowerError::Materialization { .. })
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
fixture
|
fixture
|
||||||
.catalog
|
.catalog
|
||||||
|
|
@ -258,7 +264,10 @@ fn hostile_and_corrupt_layers_refuse_without_publication() {
|
||||||
for (index, bytes) in cases.into_iter().enumerate() {
|
for (index, bytes) in cases.into_iter().enumerate() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
let request = fixture.request(u8::try_from(index).unwrap() + 10, &[(bytes, OCI_TAR)]);
|
let request = fixture.request(u8::try_from(index).unwrap() + 10, &[(bytes, OCI_TAR)]);
|
||||||
assert!(fixture.store.prepare(&request).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.prepare(&request),
|
||||||
|
Err(PreparedLowerError::Materialization { .. })
|
||||||
|
));
|
||||||
let snapshot = fixture.catalog.snapshot().unwrap();
|
let snapshot = fixture.catalog.snapshot().unwrap();
|
||||||
assert!(snapshot.prepared_artifacts.is_empty());
|
assert!(snapshot.prepared_artifacts.is_empty());
|
||||||
assert!(snapshot.prepare_operations.is_empty());
|
assert!(snapshot.prepare_operations.is_empty());
|
||||||
|
|
@ -291,7 +300,10 @@ fn hostile_and_corrupt_layers_refuse_without_publication() {
|
||||||
.find(|path| std::fs::metadata(path).unwrap().len() > 1024)
|
.find(|path| std::fs::metadata(path).unwrap().len() > 1024)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
OpenOptionsExt::append_byte(&blob);
|
OpenOptionsExt::append_byte(&blob);
|
||||||
assert!(fixture.store.prepare(&request).is_err());
|
assert!(matches!(
|
||||||
|
fixture.store.prepare(&request),
|
||||||
|
Err(PreparedLowerError::LayerValidation { .. })
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
fixture
|
fixture
|
||||||
.catalog
|
.catalog
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue