Skip to main content

canic_backup/restore/apply/
mod.rs

1use super::{RestorePlan, RestorePlanMember};
2use crate::{
3    artifacts::{ArtifactChecksum, ArtifactChecksumError},
4    manifest::VerificationCheck,
5    persistence::resolve_backup_artifact_path,
6};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use thiserror::Error as ThisError;
10
11mod journal;
12
13pub(in crate::restore) use journal::RestoreApplyCommandOutputPair;
14pub(in crate::restore) use journal::RestoreApplyJournalReport;
15pub use journal::{
16    RestoreApplyCommandConfig, RestoreApplyCommandOutput, RestoreApplyCommandPreview,
17    RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
18    RestoreApplyOperationKind, RestoreApplyOperationKindCounts, RestoreApplyOperationReceipt,
19    RestoreApplyOperationReceiptOutcome, RestoreApplyOperationState, RestoreApplyPendingSummary,
20    RestoreApplyProgressSummary, RestoreApplyReportOperation, RestoreApplyReportOutcome,
21    RestoreApplyRunnerCommand,
22};
23
24///
25/// RestoreApplyDryRun
26///
27
28#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
29pub struct RestoreApplyDryRun {
30    pub dry_run_version: u16,
31    pub backup_id: String,
32    pub ready: bool,
33    pub readiness_reasons: Vec<String>,
34    pub member_count: usize,
35    pub planned_snapshot_uploads: usize,
36    pub planned_snapshot_loads: usize,
37    pub planned_verification_checks: usize,
38    pub planned_operations: usize,
39    pub rendered_operations: usize,
40    #[serde(default)]
41    pub operation_counts: RestoreApplyOperationKindCounts,
42    pub artifact_validation: Option<RestoreApplyArtifactValidation>,
43    pub operations: Vec<RestoreApplyDryRunOperation>,
44}
45
46impl RestoreApplyDryRun {
47    /// Build a no-mutation apply dry-run from a restore plan.
48    #[must_use]
49    pub fn from_plan(plan: &RestorePlan) -> Self {
50        Self::from_validated_plan(plan)
51    }
52
53    /// Build an apply dry-run and verify all referenced artifacts under a backup root.
54    pub fn try_from_plan_with_artifacts(
55        plan: &RestorePlan,
56        backup_root: &Path,
57    ) -> Result<Self, RestoreApplyDryRunError> {
58        let mut dry_run = Self::from_plan(plan);
59        dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
60        Ok(dry_run)
61    }
62
63    // Build a no-mutation apply dry-run from a restore plan.
64    fn from_validated_plan(plan: &RestorePlan) -> Self {
65        let mut next_sequence = 0;
66        let mut operations = plan
67            .members
68            .iter()
69            .flat_map(|member| member_operations(member, &mut next_sequence))
70            .collect::<Vec<_>>();
71        append_fleet_verification_operations(plan, &mut operations, &mut next_sequence);
72        let rendered_operations = operations.len();
73        let operation_counts =
74            RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
75
76        Self {
77            dry_run_version: 1,
78            backup_id: plan.backup_id.clone(),
79            ready: plan.readiness_summary.ready,
80            readiness_reasons: plan.readiness_summary.reasons.clone(),
81            member_count: plan.member_count,
82            planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
83            planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
84            planned_verification_checks: plan.operation_summary.planned_verification_checks,
85            planned_operations: plan.operation_summary.planned_operations,
86            rendered_operations,
87            operation_counts,
88            artifact_validation: None,
89            operations,
90        }
91    }
92}
93
94// Verify every planned restore artifact against one backup directory root.
95fn validate_restore_apply_artifacts(
96    plan: &RestorePlan,
97    backup_root: &Path,
98) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
99    let mut checks = Vec::new();
100
101    for member in plan.ordered_members() {
102        checks.push(validate_restore_apply_artifact(member, backup_root)?);
103    }
104
105    let members_with_expected_checksums = checks
106        .iter()
107        .filter(|check| check.checksum_expected.is_some())
108        .count();
109    let artifacts_present = checks.iter().all(|check| check.exists);
110    let checksums_verified = members_with_expected_checksums == plan.member_count
111        && checks.iter().all(|check| check.checksum_verified);
112
113    Ok(RestoreApplyArtifactValidation {
114        backup_root: backup_root.to_string_lossy().to_string(),
115        checked_members: checks.len(),
116        artifacts_present,
117        checksums_verified,
118        members_with_expected_checksums,
119        checks,
120    })
121}
122
123// Verify one planned restore artifact path and checksum.
124fn validate_restore_apply_artifact(
125    member: &RestorePlanMember,
126    backup_root: &Path,
127) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
128    let resolved_path = safe_restore_artifact_path(
129        backup_root,
130        &member.source_canister,
131        &member.source_snapshot.artifact_path,
132    )?;
133
134    if !resolved_path.exists() {
135        return Err(RestoreApplyDryRunError::ArtifactMissing {
136            source_canister: member.source_canister.clone(),
137            artifact_path: member.source_snapshot.artifact_path.clone(),
138            resolved_path: resolved_path.to_string_lossy().to_string(),
139        });
140    }
141
142    let (checksum_actual, checksum_verified) =
143        if let Some(expected) = &member.source_snapshot.checksum {
144            let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
145                RestoreApplyDryRunError::ArtifactChecksum {
146                    source_canister: member.source_canister.clone(),
147                    artifact_path: member.source_snapshot.artifact_path.clone(),
148                    source,
149                }
150            })?;
151            checksum.verify(expected).map_err(|source| {
152                RestoreApplyDryRunError::ArtifactChecksum {
153                    source_canister: member.source_canister.clone(),
154                    artifact_path: member.source_snapshot.artifact_path.clone(),
155                    source,
156                }
157            })?;
158            (Some(checksum.hash), true)
159        } else {
160            (None, false)
161        };
162
163    Ok(RestoreApplyArtifactCheck {
164        source_canister: member.source_canister.clone(),
165        target_canister: member.target_canister.clone(),
166        snapshot_id: member.source_snapshot.snapshot_id.clone(),
167        artifact_path: member.source_snapshot.artifact_path.clone(),
168        resolved_path: resolved_path.to_string_lossy().to_string(),
169        exists: true,
170        checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
171        checksum_expected: member.source_snapshot.checksum.clone(),
172        checksum_actual,
173        checksum_verified,
174    })
175}
176
177// Reject absolute paths and parent traversal before joining with the backup root.
178fn safe_restore_artifact_path(
179    backup_root: &Path,
180    source_canister: &str,
181    artifact_path: &str,
182) -> Result<PathBuf, RestoreApplyDryRunError> {
183    if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
184        return Ok(path);
185    }
186
187    Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
188        source_canister: source_canister.to_string(),
189        artifact_path: artifact_path.to_string(),
190    })
191}
192
193///
194/// RestoreApplyArtifactValidation
195///
196
197#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198pub struct RestoreApplyArtifactValidation {
199    pub backup_root: String,
200    pub checked_members: usize,
201    pub artifacts_present: bool,
202    pub checksums_verified: bool,
203    pub members_with_expected_checksums: usize,
204    pub checks: Vec<RestoreApplyArtifactCheck>,
205}
206
207///
208/// RestoreApplyArtifactCheck
209///
210
211#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
212pub struct RestoreApplyArtifactCheck {
213    pub source_canister: String,
214    pub target_canister: String,
215    pub snapshot_id: String,
216    pub artifact_path: String,
217    pub resolved_path: String,
218    pub exists: bool,
219    pub checksum_algorithm: String,
220    pub checksum_expected: Option<String>,
221    pub checksum_actual: Option<String>,
222    pub checksum_verified: bool,
223}
224
225// Build upload, load, and verification operations for one restore member.
226fn member_operations(
227    member: &RestorePlanMember,
228    next_sequence: &mut usize,
229) -> Vec<RestoreApplyDryRunOperation> {
230    let mut operations = Vec::new();
231    push_member_operation(
232        &mut operations,
233        next_sequence,
234        RestoreApplyOperationKind::UploadSnapshot,
235        member,
236        None,
237    );
238    push_member_operation(
239        &mut operations,
240        next_sequence,
241        RestoreApplyOperationKind::LoadSnapshot,
242        member,
243        None,
244    );
245
246    for check in &member.verification_checks {
247        push_member_operation(
248            &mut operations,
249            next_sequence,
250            RestoreApplyOperationKind::VerifyMember,
251            member,
252            Some(check),
253        );
254    }
255
256    operations
257}
258
259// Append one member-level dry-run operation using the member's restore order.
260fn push_member_operation(
261    operations: &mut Vec<RestoreApplyDryRunOperation>,
262    next_sequence: &mut usize,
263    operation: RestoreApplyOperationKind,
264    member: &RestorePlanMember,
265    check: Option<&VerificationCheck>,
266) {
267    let sequence = *next_sequence;
268    *next_sequence += 1;
269
270    operations.push(RestoreApplyDryRunOperation {
271        sequence,
272        operation,
273        member_order: member.member_order,
274        source_canister: member.source_canister.clone(),
275        target_canister: member.target_canister.clone(),
276        role: member.role.clone(),
277        snapshot_id: Some(member.source_snapshot.snapshot_id.clone()),
278        artifact_path: Some(member.source_snapshot.artifact_path.clone()),
279        verification_kind: check.map(|check| check.kind.clone()),
280    });
281}
282
283// Append fleet-level verification checks after all member operations.
284fn append_fleet_verification_operations(
285    plan: &RestorePlan,
286    operations: &mut Vec<RestoreApplyDryRunOperation>,
287    next_sequence: &mut usize,
288) {
289    if plan.fleet_verification_checks.is_empty() {
290        return;
291    }
292
293    let root = plan
294        .members
295        .iter()
296        .find(|member| member.source_canister == plan.source_root_canister);
297    let source_canister = root.map_or_else(
298        || plan.source_root_canister.clone(),
299        |member| member.source_canister.clone(),
300    );
301    let target_canister = root.map_or_else(
302        || plan.source_root_canister.clone(),
303        |member| member.target_canister.clone(),
304    );
305    for check in &plan.fleet_verification_checks {
306        push_fleet_operation(
307            operations,
308            next_sequence,
309            &source_canister,
310            &target_canister,
311            check,
312        );
313    }
314}
315
316// Append one fleet-level dry-run verification operation.
317fn push_fleet_operation(
318    operations: &mut Vec<RestoreApplyDryRunOperation>,
319    next_sequence: &mut usize,
320    source_canister: &str,
321    target_canister: &str,
322    check: &VerificationCheck,
323) {
324    let sequence = *next_sequence;
325    *next_sequence += 1;
326    let member_order = operations.len();
327
328    operations.push(RestoreApplyDryRunOperation {
329        sequence,
330        operation: RestoreApplyOperationKind::VerifyFleet,
331        member_order,
332        source_canister: source_canister.to_string(),
333        target_canister: target_canister.to_string(),
334        role: "fleet".to_string(),
335        snapshot_id: None,
336        artifact_path: None,
337        verification_kind: Some(check.kind.clone()),
338    });
339}
340
341///
342/// RestoreApplyDryRunOperation
343///
344
345#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
346pub struct RestoreApplyDryRunOperation {
347    pub sequence: usize,
348    pub operation: RestoreApplyOperationKind,
349    pub member_order: usize,
350    pub source_canister: String,
351    pub target_canister: String,
352    pub role: String,
353    pub snapshot_id: Option<String>,
354    pub artifact_path: Option<String>,
355    pub verification_kind: Option<String>,
356}
357
358///
359/// RestoreApplyDryRunError
360///
361
362#[derive(Debug, ThisError)]
363pub enum RestoreApplyDryRunError {
364    #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
365    ArtifactPathEscapesBackup {
366        source_canister: String,
367        artifact_path: String,
368    },
369
370    #[error(
371        "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
372    )]
373    ArtifactMissing {
374        source_canister: String,
375        artifact_path: String,
376        resolved_path: String,
377    },
378
379    #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
380    ArtifactChecksum {
381        source_canister: String,
382        artifact_path: String,
383        #[source]
384        source: ArtifactChecksumError,
385    },
386}