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