Expose typed Prepared Lower failures
This commit is contained in:
parent
ee9201b057
commit
30cd36dcdd
1 changed files with 694 additions and 249 deletions
|
|
@ -28,6 +28,7 @@ use crate::layer::{
|
|||
verify_layout,
|
||||
};
|
||||
use crate::transport::TransportedImage;
|
||||
use crate::{PreparedLowerError, PreparedLowerResult};
|
||||
|
||||
const PHYSICAL_LOCK_WAIT: Duration = Duration::from_secs(5);
|
||||
const LOCK_RETRY: Duration = Duration::from_millis(2);
|
||||
|
|
@ -74,7 +75,11 @@ impl PreparedLowerStore {
|
|||
/// # Errors
|
||||
///
|
||||
/// 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 = File::open(&root).context("opening Storage Root")?;
|
||||
Self::initialize_retained(root, catalog)
|
||||
|
|
@ -84,7 +89,15 @@ impl PreparedLowerStore {
|
|||
///
|
||||
/// # Errors
|
||||
/// 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(
|
||||
storage_root
|
||||
.open_authority()
|
||||
|
|
@ -127,7 +140,15 @@ impl PreparedLowerStore {
|
|||
///
|
||||
/// # Errors
|
||||
/// 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
|
||||
.catalog_artifact(artifact.prepared_artifact_id)?
|
||||
.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,
|
||||
/// 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))
|
||||
}
|
||||
|
||||
|
|
@ -159,10 +180,21 @@ impl PreparedLowerStore {
|
|||
image: &TransportedImage,
|
||||
id_map: IdMap,
|
||||
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
|
||||
.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 {
|
||||
operation_id,
|
||||
layout_path: image.layout_path.clone(),
|
||||
|
|
@ -178,32 +210,46 @@ impl PreparedLowerStore {
|
|||
&self,
|
||||
request: &PrepareRequest,
|
||||
layout: &LayoutSource<'_>,
|
||||
) -> Result<PreparedArtifact> {
|
||||
) -> PreparedLowerResult<PreparedArtifact> {
|
||||
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 {
|
||||
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)?;
|
||||
if let Some(existing) = self.available_artifact(identities.artifact_id)? {
|
||||
identities.require_equivalent(&existing)?;
|
||||
self.require_artifact_identity(&existing)?;
|
||||
let identities = Identities::new(request).map_err(PreparedLowerError::layer_validation)?;
|
||||
if let Some(existing) = self
|
||||
.available_artifact(identities.artifact_id)
|
||||
.map_err(PreparedLowerError::catalog)?
|
||||
{
|
||||
identities
|
||||
.require_equivalent(&existing)
|
||||
.map_err(PreparedLowerError::catalog)?;
|
||||
self.require_artifact_identity(&existing)
|
||||
.map_err(PreparedLowerError::confinement)?;
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let stage_relative = stage_path(request.operation_id);
|
||||
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) {
|
||||
Ok(stage) => stage,
|
||||
Err(error) => {
|
||||
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 {
|
||||
stage_state: PrepareStageState::Owned,
|
||||
device: Some(device),
|
||||
|
|
@ -217,7 +263,7 @@ impl PreparedLowerStore {
|
|||
{
|
||||
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)
|
||||
.and_then(|()| materialize_destinations(&stage, &request.destinations, &request.id_map))
|
||||
|
|
@ -229,9 +275,11 @@ impl PreparedLowerStore {
|
|||
{
|
||||
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.
|
||||
|
|
@ -240,7 +288,15 @@ impl PreparedLowerStore {
|
|||
///
|
||||
/// Refuses unexpected paths, missing ownership evidence, identity mismatch, and catalog or
|
||||
/// 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);
|
||||
if operation.stage_path.as_bytes() != path_bytes(&expected_stage) {
|
||||
bail!("prepare operation stage path is not canonical");
|
||||
|
|
@ -281,12 +337,19 @@ impl PreparedLowerStore {
|
|||
/// # Errors
|
||||
/// Stops at the first foreign, mismatched, unreadable, or changed operation without treating
|
||||
/// 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;
|
||||
operations
|
||||
.iter()
|
||||
.map(|operation| {
|
||||
self.recover_prepare(operation)
|
||||
self.recover_prepare_inner(operation)
|
||||
.map(|outcome| (operation.operation_id, outcome))
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -300,19 +363,32 @@ impl PreparedLowerStore {
|
|||
pub fn gc_artifacts(
|
||||
&self,
|
||||
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();
|
||||
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());
|
||||
for id in selected {
|
||||
let artifact = snapshot
|
||||
.prepared_artifacts
|
||||
.iter()
|
||||
.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 {
|
||||
ArtifactState::Available => self.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
|
||||
/// filesystem or catalog failures.
|
||||
pub fn reclaim(&self, expected: &PreparedArtifact) -> Result<ReclaimOutcome> {
|
||||
let reclaiming = self.catalog.begin_reclaim(expected)?;
|
||||
pub fn reclaim(&self, expected: &PreparedArtifact) -> PreparedLowerResult<ReclaimOutcome> {
|
||||
let reclaiming = self
|
||||
.catalog
|
||||
.begin_reclaim(expected)
|
||||
.map_err(PreparedLowerError::catalog)?;
|
||||
self.resume_reclaim(&reclaiming)
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +421,16 @@ impl PreparedLowerStore {
|
|||
///
|
||||
/// Refuses non-reclaiming state, path/identity mismatch, missing physical evidence, and
|
||||
/// 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 {
|
||||
bail!("artifact is not reclaiming");
|
||||
}
|
||||
|
|
@ -352,7 +440,6 @@ impl PreparedLowerStore {
|
|||
}
|
||||
let quarantine = quarantine_path(reclaiming);
|
||||
{
|
||||
let _lock = self.physical_lock()?;
|
||||
if let Some(canonical) = self.open_relative(&expected_artifact)? {
|
||||
if file_identity(&canonical)? == (reclaiming.device, reclaiming.inode) {
|
||||
self.rename_relative_noreplace(&expected_artifact, &quarantine)?;
|
||||
|
|
@ -395,10 +482,10 @@ impl PreparedLowerStore {
|
|||
&self,
|
||||
operation: &PrepareOperation,
|
||||
identities: &Identities,
|
||||
_physical_lock: &ExclusiveFileLock,
|
||||
) -> Result<PreparedArtifact> {
|
||||
let stage = stage_path(operation.operation_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(row) = self.available_artifact(operation.expected_artifact_id)? {
|
||||
identities.require_equivalent(&row)?;
|
||||
|
|
@ -526,20 +613,22 @@ impl PreparedLowerStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn physical_lock(&self) -> Result<ExclusiveFileLock> {
|
||||
fn physical_lock(&self) -> PreparedLowerResult<ExclusiveFileLock> {
|
||||
let deadline = Instant::now() + PHYSICAL_LOCK_WAIT;
|
||||
loop {
|
||||
let file = open_lock_at(&self.root)?;
|
||||
let file = open_lock_at(&self.root).map_err(PreparedLowerError::locking)?;
|
||||
match try_exclusive_flock(file) {
|
||||
Ok(lock) => return Ok(lock),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
let now = Instant::now();
|
||||
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)));
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
Err(error) => return Err(PreparedLowerError::locking(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue