Skip to main content

ic_backup/workflow/ic_snapshot_restore/source/
mod.rs

1//! Exact existing-snapshot or completed-upload provenance for one restore step.
2
3use crate::{
4    model::{
5        attempt_journal::AppliedReceiptEvidence,
6        execution_workflow::ExecutionStagePredecessorRecord,
7        ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
8        ic_snapshot_metadata::IcSnapshotMetadataRequest,
9        ic_snapshot_upload::{IcSnapshotDataUploadPlan, IcSnapshotDataUploadPlanningError},
10        operation_plan::OperationPlanRecord,
11    },
12    ops::persistence::{
13        AttemptJournalError, AttemptJournalGuard, BackupLayoutGuard, ExecutionStageCheckpointError,
14        ExecutionStageGuard, ExecutionWorkflowPersistenceError,
15    },
16};
17use thiserror::Error;
18
19/// Source identity retained independently of the restore request being admitted.
20///
21/// Existing snapshots require authenticated metadata/source association from the
22/// integration. Uploaded snapshots additionally require the exact complete retained
23/// data stage and allocation predecessor. Neither form authenticates backend bytes,
24/// permissions or application safety; the mandatory admission callback still does.
25#[derive(Clone)]
26pub struct IcRestoreSource<'a> {
27    pub(super) layout: &'a BackupLayoutGuard,
28    pub(super) plan: &'a OperationPlanRecord,
29    target: String,
30    snapshot_id: Vec<u8>,
31    upload: Option<UploadOrigin<'a>>,
32}
33
34#[derive(Clone)]
35struct UploadOrigin<'a> {
36    layout: &'a BackupLayoutGuard,
37    workflow: crate::model::artifacts::ArtifactChecksumRecord,
38    completed: ExecutionStagePredecessorRecord,
39    allocation_receipt: Option<crate::model::artifacts::ArtifactChecksumRecord>,
40}
41
42impl<'a> IcRestoreSource<'a> {
43    /// Bind an existing snapshot's independently qualified metadata request.
44    ///
45    /// Use only for an existing source snapshot, not as a substitute for upload
46    /// completion. This passive declaration supplies no authentic byte association.
47    #[must_use]
48    pub fn existing(
49        layout: &'a BackupLayoutGuard,
50        plan: &'a OperationPlanRecord,
51        metadata: &IcSnapshotMetadataRequest,
52    ) -> Self {
53        Self {
54            layout,
55            plan,
56            target: metadata.target().into(),
57            snapshot_id: metadata.snapshot_id().into(),
58            upload: None,
59        }
60    }
61
62    /// Bind an allocated destination to its complete original upload stage.
63    ///
64    /// Hold no upload attempt/stage guards. Reopening the existing stage checks
65    /// allocation ancestry, every original Applied history and its checkpoint;
66    /// it creates no receipt, stage, allowance or replacement authority.
67    /// # Errors
68    /// Rejects incomplete/pending, substituted or cross-plan upload evidence.
69    pub fn uploaded(
70        layout: &'a BackupLayoutGuard,
71        upload: &IcSnapshotDataUploadPlan<'_, '_, 'a>,
72        workflow_layout: &'a BackupLayoutGuard,
73        completed: &ExecutionStagePredecessorRecord,
74    ) -> Result<Self, IcRestoreSourceError> {
75        let origin = UploadOrigin {
76            layout: workflow_layout,
77            workflow: upload.workflow_digest(),
78            completed: completed.clone(),
79            allocation_receipt: upload
80                .plan()
81                .is_none()
82                .then(|| completed.learned_evidence().clone()),
83        };
84        let stage = origin.open()?;
85        upload.validate_completion(stage.binding(), stage.plan(), completed.learned_evidence())?;
86        origin.replay(&stage)?;
87        Ok(Self {
88            layout,
89            plan: upload.metadata().source_plan(),
90            target: upload.metadata().target().into(),
91            snapshot_id: upload.destination().into(),
92            upload: Some(origin),
93        })
94    }
95
96    pub(crate) fn validate(
97        &self,
98        stage: &ExecutionStageGuard<'_>,
99        wire: &IcManagementRequestRecord,
100    ) -> Result<(), IcRestoreSourceError> {
101        if wire.target() != self.target
102            || (wire.method() == IcManagementMethodRecord::LoadCanisterSnapshot
103                && wire.snapshot_id() != Some(self.snapshot_id.as_slice()))
104        {
105            return Err(IcRestoreSourceError::Mismatch);
106        }
107        if let Some(origin) = &self.upload {
108            if stage.binding().workflow() == &origin.workflow
109                && (wire.method() == IcManagementMethodRecord::LoadCanisterSnapshot
110                    && !stage.binding().predecessors().contains(&origin.completed))
111            {
112                return Err(IcRestoreSourceError::Mismatch);
113            }
114            let upload = origin.open()?;
115            origin.replay(&upload)?;
116        }
117        Ok(())
118    }
119}
120
121impl UploadOrigin<'_> {
122    fn open(&self) -> Result<ExecutionStageGuard<'_>, ExecutionWorkflowPersistenceError> {
123        ExecutionStageGuard::open(
124            self.layout,
125            &self.workflow,
126            self.completed.stage_sequence(),
127            self.completed.binding(),
128        )
129    }
130
131    fn replay(&self, stage: &ExecutionStageGuard<'_>) -> Result<(), IcRestoreSourceError> {
132        if stage.replay_checkpoint(self.completed.learned_evidence().clone())? != self.completed {
133            return Err(IcRestoreSourceError::Mismatch);
134        }
135        if let Some(expected) = &self.allocation_receipt {
136            let sequence = stage.plan().operations()[0].operation_sequence();
137            let authority = stage
138                .plan()
139                .attempt_authority(sequence)
140                .map_err(|_| IcRestoreSourceError::Mismatch)?;
141            let journal = AttemptJournalGuard::open(stage.layout()?, &authority)?;
142            if !matches!(journal.record()?.applied_receipt(), Some(AppliedReceiptEvidence::Mutation { evidence, .. }) if evidence == expected.hash())
143            {
144                return Err(IcRestoreSourceError::Mismatch);
145            }
146        }
147        Ok(())
148    }
149}
150
151/// Retained upload/load provenance rejection; no variant permits reissue.
152#[derive(Debug, Error)]
153pub enum IcRestoreSourceError {
154    /// Original allocation receipt cannot be admitted under its journal exclusion.
155    #[error(transparent)]
156    Journal(#[from] AttemptJournalError),
157    /// Target, snapshot, original plan or predecessor identity differs.
158    #[error("restore snapshot source differs from its original upload or metadata")]
159    Mismatch,
160    /// Original allocation/data-plan admission failed.
161    #[error(transparent)]
162    Planning(#[from] IcSnapshotDataUploadPlanningError),
163    /// Original stage/ancestor admission failed.
164    #[error(transparent)]
165    Stage(#[from] ExecutionWorkflowPersistenceError),
166    /// Complete original upload settlement is absent or changed.
167    #[error(transparent)]
168    Checkpoint(#[from] ExecutionStageCheckpointError),
169}