Skip to main content

canic_backup/restore/apply/
mod.rs

1use super::{RestorePlan, RestorePlanError, RestorePlanMember};
2use crate::{
3    artifacts::{ArtifactChecksum, ArtifactChecksumError},
4    manifest::VerificationCheck,
5    persistence::resolve_backup_artifact_path,
6};
7use serde::{Deserialize, Serialize};
8use std::{
9    fs,
10    path::{Path, PathBuf},
11};
12use thiserror::Error as ThisError;
13
14mod journal;
15
16pub(in crate::restore) use journal::RestoreApplyCommandOutputPair;
17pub(in crate::restore) use journal::RestoreApplyJournalReport;
18pub use journal::{
19    RestoreApplyCommandConfig, RestoreApplyCommandOutput, RestoreApplyCommandPreview,
20    RestoreApplyJournal, RestoreApplyJournalError, RestoreApplyJournalOperation,
21    RestoreApplyOperationKind, RestoreApplyOperationKindCounts, RestoreApplyOperationReceipt,
22    RestoreApplyOperationReceiptOutcome, RestoreApplyOperationState, RestoreApplyPendingSummary,
23    RestoreApplyProgressSummary, RestoreApplyReportOperation, RestoreApplyReportOutcome,
24    RestoreApplyRunnerCommand,
25};
26
27///
28/// RestoreApplyDryRun
29///
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33pub struct RestoreApplyDryRun {
34    pub dry_run_version: u16,
35    pub backup_id: String,
36    pub ready: bool,
37    pub readiness_reasons: Vec<String>,
38    pub member_count: usize,
39    pub planned_canister_stops: usize,
40    pub planned_canister_starts: usize,
41    pub planned_snapshot_uploads: usize,
42    pub planned_snapshot_loads: usize,
43    pub planned_verification_checks: usize,
44    pub planned_operations: usize,
45    pub rendered_operations: usize,
46    pub operation_counts: RestoreApplyOperationKindCounts,
47    pub artifact_validation: Option<RestoreApplyArtifactValidation>,
48    pub operations: Vec<RestoreApplyDryRunOperation>,
49}
50
51impl RestoreApplyDryRun {
52    /// Build a no-mutation apply dry-run from a restore plan.
53    pub fn from_plan(plan: &RestorePlan) -> Result<Self, RestorePlanError> {
54        plan.validate()?;
55        Ok(Self::from_validated_plan(plan))
56    }
57
58    /// Build an apply dry-run and verify all referenced artifacts under a backup root.
59    pub fn try_from_plan_with_artifacts(
60        plan: &RestorePlan,
61        backup_root: &Path,
62    ) -> Result<Self, RestoreApplyDryRunError> {
63        let mut dry_run = Self::from_plan(plan)?;
64        dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
65        Ok(dry_run)
66    }
67
68    // Build a no-mutation apply dry-run from a restore plan.
69    fn from_validated_plan(plan: &RestorePlan) -> Self {
70        let mut next_sequence = 0;
71        let ordered_members = plan.ordered_members();
72        let mut operations = Vec::new();
73        append_member_phase(
74            &mut operations,
75            &mut next_sequence,
76            RestoreApplyOperationKind::UploadSnapshot,
77            ordered_members.iter().copied(),
78        );
79        append_member_phase(
80            &mut operations,
81            &mut next_sequence,
82            RestoreApplyOperationKind::StopCanister,
83            ordered_members.iter().copied(),
84        );
85        append_member_phase(
86            &mut operations,
87            &mut next_sequence,
88            RestoreApplyOperationKind::LoadSnapshot,
89            ordered_members.iter().copied(),
90        );
91        append_member_phase(
92            &mut operations,
93            &mut next_sequence,
94            RestoreApplyOperationKind::StartCanister,
95            ordered_members.iter().rev().copied(),
96        );
97        append_member_verification_operations(
98            &mut operations,
99            &mut next_sequence,
100            &ordered_members,
101        );
102        append_deployment_verification_operations(plan, &mut operations, &mut next_sequence);
103        let rendered_operations = operations.len();
104        let operation_counts =
105            RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
106
107        Self {
108            dry_run_version: 1,
109            backup_id: plan.backup_id.clone(),
110            ready: plan.readiness_summary.ready,
111            readiness_reasons: plan.readiness_summary.reasons.clone(),
112            member_count: plan.member_count,
113            planned_canister_stops: plan.operation_summary.planned_canister_stops,
114            planned_canister_starts: plan.operation_summary.planned_canister_starts,
115            planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
116            planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
117            planned_verification_checks: plan.operation_summary.planned_verification_checks,
118            planned_operations: plan.operation_summary.planned_operations,
119            rendered_operations,
120            operation_counts,
121            artifact_validation: None,
122            operations,
123        }
124    }
125}
126
127// Verify every planned restore artifact against one backup directory root.
128fn validate_restore_apply_artifacts(
129    plan: &RestorePlan,
130    backup_root: &Path,
131) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
132    let backup_root = canonical_restore_backup_root(backup_root)?;
133    let mut checks = Vec::new();
134
135    for member in plan.ordered_members() {
136        checks.push(validate_restore_apply_artifact(member, &backup_root)?);
137    }
138
139    let members_with_expected_checksums = checks
140        .iter()
141        .filter(|check| check.checksum_expected.is_some())
142        .count();
143    let artifacts_present = checks.iter().all(|check| check.exists);
144    let checksums_verified = members_with_expected_checksums == plan.member_count
145        && checks.iter().all(|check| check.checksum_verified);
146
147    Ok(RestoreApplyArtifactValidation {
148        backup_root: backup_root.to_string_lossy().to_string(),
149        checked_members: checks.len(),
150        artifacts_present,
151        checksums_verified,
152        members_with_expected_checksums,
153        checks,
154    })
155}
156
157// Verify one planned restore artifact path and checksum.
158fn validate_restore_apply_artifact(
159    member: &RestorePlanMember,
160    backup_root: &Path,
161) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
162    let resolved_path = safe_restore_artifact_path(
163        backup_root,
164        &member.source_canister,
165        &member.source_snapshot.artifact_path,
166    )?;
167
168    validate_restore_artifact_type(
169        backup_root,
170        &member.source_canister,
171        &member.source_snapshot.artifact_path,
172        &resolved_path,
173    )?;
174
175    let (checksum_actual, checksum_verified) =
176        if let Some(expected) = &member.source_snapshot.checksum {
177            let checksum = ArtifactChecksum::from_relative_path_no_follow(
178                backup_root,
179                Path::new(&member.source_snapshot.artifact_path),
180            )
181            .map_err(|source| RestoreApplyDryRunError::ArtifactChecksum {
182                source_canister: member.source_canister.clone(),
183                artifact_path: member.source_snapshot.artifact_path.clone(),
184                source,
185            })?;
186            checksum.verify(expected).map_err(|source| {
187                RestoreApplyDryRunError::ArtifactChecksum {
188                    source_canister: member.source_canister.clone(),
189                    artifact_path: member.source_snapshot.artifact_path.clone(),
190                    source,
191                }
192            })?;
193            (Some(checksum.hash), true)
194        } else {
195            (None, false)
196        };
197
198    Ok(RestoreApplyArtifactCheck {
199        source_canister: member.source_canister.clone(),
200        target_canister: member.target_canister.clone(),
201        snapshot_id: member.source_snapshot.snapshot_id.clone(),
202        artifact_path: member.source_snapshot.artifact_path.clone(),
203        resolved_path: resolved_path.to_string_lossy().to_string(),
204        exists: true,
205        checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
206        checksum_expected: member.source_snapshot.checksum.clone(),
207        checksum_actual,
208        checksum_verified,
209    })
210}
211
212fn canonical_restore_backup_root(backup_root: &Path) -> Result<PathBuf, RestoreApplyDryRunError> {
213    let metadata = fs::symlink_metadata(backup_root).map_err(|source| {
214        RestoreApplyDryRunError::ArtifactRootIo {
215            backup_root: backup_root.to_path_buf(),
216            source,
217        }
218    })?;
219    if metadata.file_type().is_symlink() || !metadata.is_dir() {
220        return Err(RestoreApplyDryRunError::ArtifactRootUnsafe {
221            backup_root: backup_root.to_path_buf(),
222        });
223    }
224    backup_root
225        .canonicalize()
226        .map_err(|source| RestoreApplyDryRunError::ArtifactRootIo {
227            backup_root: backup_root.to_path_buf(),
228            source,
229        })
230}
231
232fn validate_restore_artifact_type(
233    backup_root: &Path,
234    source_canister: &str,
235    artifact_path: &str,
236    resolved_path: &Path,
237) -> Result<(), RestoreApplyDryRunError> {
238    let mut current = backup_root.to_path_buf();
239    let mut artifact_metadata = None;
240    for component in Path::new(artifact_path).components() {
241        current.push(component);
242        let metadata = match fs::symlink_metadata(&current) {
243            Ok(metadata) => metadata,
244            Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
245                return Err(RestoreApplyDryRunError::ArtifactMissing {
246                    source_canister: source_canister.to_string(),
247                    artifact_path: artifact_path.to_string(),
248                    resolved_path: resolved_path.to_string_lossy().to_string(),
249                });
250            }
251            Err(source) => {
252                return Err(RestoreApplyDryRunError::ArtifactChecksum {
253                    source_canister: source_canister.to_string(),
254                    artifact_path: artifact_path.to_string(),
255                    source: ArtifactChecksumError::Io(source),
256                });
257            }
258        };
259        if metadata.file_type().is_symlink() {
260            return Err(RestoreApplyDryRunError::ArtifactUnsafeType {
261                source_canister: source_canister.to_string(),
262                artifact_path: current.to_string_lossy().to_string(),
263                kind: "symlink".to_string(),
264            });
265        }
266        artifact_metadata = Some(metadata);
267    }
268    let metadata =
269        artifact_metadata.ok_or_else(|| RestoreApplyDryRunError::ArtifactPathEscapesBackup {
270            source_canister: source_canister.to_string(),
271            artifact_path: artifact_path.to_string(),
272        })?;
273    if metadata.is_file() || metadata.is_dir() {
274        Ok(())
275    } else {
276        Err(RestoreApplyDryRunError::ArtifactUnsafeType {
277            source_canister: source_canister.to_string(),
278            artifact_path: artifact_path.to_string(),
279            kind: "special".to_string(),
280        })
281    }
282}
283
284// Reject absolute paths and parent traversal before joining with the backup root.
285fn safe_restore_artifact_path(
286    backup_root: &Path,
287    source_canister: &str,
288    artifact_path: &str,
289) -> Result<PathBuf, RestoreApplyDryRunError> {
290    if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
291        return Ok(path);
292    }
293
294    Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
295        source_canister: source_canister.to_string(),
296        artifact_path: artifact_path.to_string(),
297    })
298}
299
300///
301/// RestoreApplyArtifactValidation
302///
303
304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
305#[serde(deny_unknown_fields)]
306pub struct RestoreApplyArtifactValidation {
307    pub backup_root: String,
308    pub checked_members: usize,
309    pub artifacts_present: bool,
310    pub checksums_verified: bool,
311    pub members_with_expected_checksums: usize,
312    pub checks: Vec<RestoreApplyArtifactCheck>,
313}
314
315///
316/// RestoreApplyArtifactCheck
317///
318
319#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
320#[serde(deny_unknown_fields)]
321pub struct RestoreApplyArtifactCheck {
322    pub source_canister: String,
323    pub target_canister: String,
324    pub snapshot_id: String,
325    pub artifact_path: String,
326    pub resolved_path: String,
327    pub exists: bool,
328    pub checksum_algorithm: String,
329    pub checksum_expected: Option<String>,
330    pub checksum_actual: Option<String>,
331    pub checksum_verified: bool,
332}
333
334// Append a full member operation phase across the selected topology.
335fn append_member_phase<'a>(
336    operations: &mut Vec<RestoreApplyDryRunOperation>,
337    next_sequence: &mut usize,
338    operation: RestoreApplyOperationKind,
339    members: impl IntoIterator<Item = &'a RestorePlanMember>,
340) {
341    for member in members {
342        push_member_operation(operations, next_sequence, operation.clone(), member, None);
343    }
344}
345
346// Append member verification checks after restore mutations complete.
347fn append_member_verification_operations(
348    operations: &mut Vec<RestoreApplyDryRunOperation>,
349    next_sequence: &mut usize,
350    members: &[&RestorePlanMember],
351) {
352    for member in members {
353        for check in &member.verification_checks {
354            push_member_operation(
355                operations,
356                next_sequence,
357                RestoreApplyOperationKind::VerifyMember,
358                member,
359                Some(check),
360            );
361        }
362    }
363}
364
365// Append one member-level dry-run operation using the member's restore order.
366fn push_member_operation(
367    operations: &mut Vec<RestoreApplyDryRunOperation>,
368    next_sequence: &mut usize,
369    operation: RestoreApplyOperationKind,
370    member: &RestorePlanMember,
371    check: Option<&VerificationCheck>,
372) {
373    let sequence = *next_sequence;
374    *next_sequence += 1;
375
376    let snapshot_id = operation_snapshot_id(&operation, member);
377    let artifact_path = operation_artifact_path(&operation, member);
378    let artifact_checksum = operation_artifact_checksum(&operation, member);
379
380    operations.push(RestoreApplyDryRunOperation {
381        sequence,
382        operation,
383        member_order: member.member_order,
384        source_canister: member.source_canister.clone(),
385        target_canister: member.target_canister.clone(),
386        role: member.role.clone(),
387        snapshot_id,
388        artifact_path,
389        artifact_checksum,
390        verification_kind: check.map(|check| check.kind.clone()),
391    });
392}
393
394fn operation_snapshot_id(
395    operation: &RestoreApplyOperationKind,
396    member: &RestorePlanMember,
397) -> Option<String> {
398    matches!(
399        operation,
400        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
401    )
402    .then(|| member.source_snapshot.snapshot_id.clone())
403}
404
405fn operation_artifact_path(
406    operation: &RestoreApplyOperationKind,
407    member: &RestorePlanMember,
408) -> Option<String> {
409    matches!(
410        operation,
411        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
412    )
413    .then(|| member.source_snapshot.artifact_path.clone())
414}
415
416fn operation_artifact_checksum(
417    operation: &RestoreApplyOperationKind,
418    member: &RestorePlanMember,
419) -> Option<ArtifactChecksum> {
420    matches!(
421        operation,
422        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
423    )
424    .then(|| {
425        member
426            .source_snapshot
427            .checksum
428            .as_ref()
429            .map(|hash| ArtifactChecksum {
430                algorithm: member.source_snapshot.checksum_algorithm.clone(),
431                hash: hash.clone(),
432            })
433    })
434    .flatten()
435}
436
437// Append deployment-level verification checks after all member operations.
438fn append_deployment_verification_operations(
439    plan: &RestorePlan,
440    operations: &mut Vec<RestoreApplyDryRunOperation>,
441    next_sequence: &mut usize,
442) {
443    if plan.deployment_verification_checks.is_empty() {
444        return;
445    }
446
447    let root = plan
448        .members
449        .iter()
450        .find(|member| member.source_canister == plan.source_root_canister);
451    let source_canister = root.map_or_else(
452        || plan.source_root_canister.clone(),
453        |member| member.source_canister.clone(),
454    );
455    let target_canister = root.map_or_else(
456        || plan.source_root_canister.clone(),
457        |member| member.target_canister.clone(),
458    );
459    for check in &plan.deployment_verification_checks {
460        push_deployment_operation(
461            operations,
462            next_sequence,
463            &source_canister,
464            &target_canister,
465            check,
466        );
467    }
468}
469
470// Append one deployment-level dry-run verification operation.
471fn push_deployment_operation(
472    operations: &mut Vec<RestoreApplyDryRunOperation>,
473    next_sequence: &mut usize,
474    source_canister: &str,
475    target_canister: &str,
476    check: &VerificationCheck,
477) {
478    let sequence = *next_sequence;
479    *next_sequence += 1;
480    let member_order = operations.len();
481
482    operations.push(RestoreApplyDryRunOperation {
483        sequence,
484        operation: RestoreApplyOperationKind::VerifyDeployment,
485        member_order,
486        source_canister: source_canister.to_string(),
487        target_canister: target_canister.to_string(),
488        role: "deployment".to_string(),
489        snapshot_id: None,
490        artifact_path: None,
491        artifact_checksum: None,
492        verification_kind: Some(check.kind.clone()),
493    });
494}
495
496///
497/// RestoreApplyDryRunOperation
498///
499
500#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
501#[serde(deny_unknown_fields)]
502pub struct RestoreApplyDryRunOperation {
503    pub sequence: usize,
504    pub operation: RestoreApplyOperationKind,
505    pub member_order: usize,
506    pub source_canister: String,
507    pub target_canister: String,
508    pub role: String,
509    pub snapshot_id: Option<String>,
510    pub artifact_path: Option<String>,
511    pub artifact_checksum: Option<ArtifactChecksum>,
512    pub verification_kind: Option<String>,
513}
514
515///
516/// RestoreApplyDryRunError
517///
518
519#[derive(Debug, ThisError)]
520pub enum RestoreApplyDryRunError {
521    #[error(transparent)]
522    InvalidPlan(#[from] RestorePlanError),
523
524    #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
525    ArtifactPathEscapesBackup {
526        source_canister: String,
527        artifact_path: String,
528    },
529
530    #[error("restore backup artifact root IO failed at {backup_root}")]
531    ArtifactRootIo {
532        backup_root: PathBuf,
533        #[source]
534        source: std::io::Error,
535    },
536
537    #[error("restore backup artifact root is not a direct directory: {backup_root}")]
538    ArtifactRootUnsafe { backup_root: PathBuf },
539
540    #[error(
541        "restore artifact for {source_canister} has unsupported filesystem type {kind}: {artifact_path}"
542    )]
543    ArtifactUnsafeType {
544        source_canister: String,
545        artifact_path: String,
546        kind: String,
547    },
548
549    #[error(
550        "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
551    )]
552    ArtifactMissing {
553        source_canister: String,
554        artifact_path: String,
555        resolved_path: String,
556    },
557
558    #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
559    ArtifactChecksum {
560        source_canister: String,
561        artifact_path: String,
562        #[source]
563        source: ArtifactChecksumError,
564    },
565}