Implement verified OCI prepared-lower lifecycle

This commit is contained in:
ms2k 2026-07-15 15:22:02 +09:00
commit 5458460717

View file

@ -10,8 +10,8 @@ use std::time::{Duration, Instant};
use lazaret_core::codec::{self, CodecError};
use lazaret_core::identity::{PrepareOperationId, PreparedArtifactId};
use lazaret_core::records::{
ArtifactLease, ArtifactState, PrepareOperation, PreparedArtifact, StorageCatalog,
StorageCatalogMetadata,
ArtifactLease, ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact,
StorageCatalog, StorageCatalogMetadata,
};
use lazaret_linux::filesystem::{ExclusiveFileLock, try_exclusive_flock};
use redb::{
@ -106,6 +106,11 @@ impl Catalog {
///
/// Returns an error if the operation identity already exists or the transaction fails.
pub fn record_prepare(&self, operation: &PrepareOperation) -> Result<(), CatalogError> {
if operation.stage_state != PrepareStageState::Intent
|| !operation.has_valid_evidence_shape()
{
return Err(CatalogError::InvalidPrepareState);
}
self.write(|transaction| {
let key = operation_key(operation.operation_id);
let mut table = transaction
@ -120,6 +125,44 @@ impl Catalog {
})
}
/// Binds exact physical stage ownership to an already durable preparation intent.
///
/// # Errors
///
/// Returns an error for nonmatching identity, invalid phase/evidence, or transaction failure.
pub fn bind_prepare_stage(
&self,
expected_intent: &PrepareOperation,
owned_stage: &PrepareOperation,
) -> Result<(), CatalogError> {
if expected_intent.stage_state != PrepareStageState::Intent
|| owned_stage.stage_state != PrepareStageState::Owned
|| !expected_intent.has_valid_evidence_shape()
|| !owned_stage.has_valid_evidence_shape()
|| expected_intent.operation_id != owned_stage.operation_id
|| expected_intent.expected_artifact_id != owned_stage.expected_artifact_id
|| expected_intent.source_identity != owned_stage.source_identity
|| expected_intent.selected_manifest_digest != owned_stage.selected_manifest_digest
|| expected_intent.policy_id != owned_stage.policy_id
|| expected_intent.id_map_digest != owned_stage.id_map_digest
|| expected_intent.stage_path != owned_stage.stage_path
{
return Err(CatalogError::InvalidPrepareState);
}
self.write(|transaction| {
let key = operation_key(expected_intent.operation_id);
let mut table = transaction
.open_table(PREPARES)
.map_err(CatalogError::redb)?;
require_exact(&table, &key, expected_intent, CatalogRow::PrepareOperation)?;
let encoded = encode(owned_stage)?;
table
.insert(key.as_slice(), encoded.as_slice())
.map_err(CatalogError::redb)?;
Ok(())
})
}
/// Removes only an exactly matching preparation ownership row.
///
/// # Errors
@ -148,11 +191,19 @@ impl Catalog {
expected_operation: &PrepareOperation,
artifact: &PreparedArtifact,
) -> Result<(), CatalogError> {
if artifact.state != ArtifactState::Available {
if artifact.state != ArtifactState::Available
|| expected_operation.stage_state != PrepareStageState::Owned
|| !expected_operation.has_valid_evidence_shape()
{
return Err(CatalogError::InvalidArtifactState);
}
if artifact.prepared_artifact_id != expected_operation.expected_artifact_id
|| artifact.source_identity != expected_operation.source_identity
|| artifact.selected_manifest_digest != expected_operation.selected_manifest_digest
|| artifact.policy_id != expected_operation.policy_id
|| artifact.id_map_digest != expected_operation.id_map_digest
|| expected_operation.device != Some(artifact.device)
|| expected_operation.inode != Some(artifact.inode)
{
return Err(CatalogError::OwnershipMismatch {
row: CatalogRow::PrepareOperation,
@ -196,6 +247,82 @@ impl Catalog {
})
}
/// Atomically replaces an exact reclaiming generation and consumes its prepare ownership.
///
/// # Errors
///
/// Returns an error for mismatched old/new ownership, invalid state, leases, or transaction
/// failure.
pub fn publish_replacement(
&self,
expected_operation: &PrepareOperation,
expected_reclaiming: &PreparedArtifact,
replacement: &PreparedArtifact,
) -> Result<(), CatalogError> {
if expected_operation.stage_state != PrepareStageState::Owned
|| !expected_operation.has_valid_evidence_shape()
|| expected_reclaiming.state != ArtifactState::Reclaiming
|| replacement.state != ArtifactState::Available
|| replacement.prepared_artifact_id != expected_reclaiming.prepared_artifact_id
|| replacement.prepared_artifact_id != expected_operation.expected_artifact_id
|| replacement.source_identity != expected_operation.source_identity
|| replacement.selected_manifest_digest != expected_operation.selected_manifest_digest
|| replacement.policy_id != expected_operation.policy_id
|| replacement.id_map_digest != expected_operation.id_map_digest
|| expected_operation.device != Some(replacement.device)
|| expected_operation.inode != Some(replacement.inode)
|| replacement.source_identity != expected_reclaiming.source_identity
|| replacement.selected_manifest_digest != expected_reclaiming.selected_manifest_digest
|| replacement.policy_id != expected_reclaiming.policy_id
|| replacement.id_map_digest != expected_reclaiming.id_map_digest
|| replacement.path != expected_reclaiming.path
{
return Err(CatalogError::InvalidPrepareState);
}
self.write(|transaction| {
let prepare_key = operation_key(expected_operation.operation_id);
let artifact_key = artifact_key(replacement.prepared_artifact_id);
{
let prepares = transaction
.open_table(PREPARES)
.map_err(CatalogError::redb)?;
require_exact(
&prepares,
&prepare_key,
expected_operation,
CatalogRow::PrepareOperation,
)?;
let artifacts = transaction
.open_table(ARTIFACTS)
.map_err(CatalogError::redb)?;
require_exact(
&artifacts,
&artifact_key,
expected_reclaiming,
CatalogRow::PreparedArtifact,
)?;
}
if artifact_has_lease(transaction, replacement.prepared_artifact_id)? {
return Err(CatalogError::LeasesPresent);
}
let encoded = encode(replacement)?;
let mut artifacts = transaction
.open_table(ARTIFACTS)
.map_err(CatalogError::redb)?;
artifacts
.insert(artifact_key.as_slice(), encoded.as_slice())
.map_err(CatalogError::redb)?;
drop(artifacts);
let mut prepares = transaction
.open_table(PREPARES)
.map_err(CatalogError::redb)?;
prepares
.remove(prepare_key.as_slice())
.map_err(CatalogError::redb)?;
Ok(())
})
}
/// Inserts one exact Domain-generation lease for an available artifact.
///
/// # Errors
@ -570,6 +697,7 @@ pub enum CatalogError {
AlreadyExists { row: CatalogRow },
OwnershipMismatch { row: CatalogRow },
InvalidArtifactState,
InvalidPrepareState,
ArtifactUnavailable,
LeasesPresent,
}
@ -598,6 +726,9 @@ impl fmt::Display for CatalogError {
Self::InvalidArtifactState => {
formatter.write_str("artifact state is invalid for operation")
}
Self::InvalidPrepareState => {
formatter.write_str("prepare phase or physical evidence is invalid")
}
Self::ArtifactUnavailable => {
formatter.write_str("artifact is not available for leasing")
}