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