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    #[serde(default)]
36    pub planned_canister_stops: usize,
37    #[serde(default)]
38    pub planned_canister_starts: usize,
39    pub planned_snapshot_uploads: usize,
40    pub planned_snapshot_loads: usize,
41    pub planned_verification_checks: usize,
42    pub planned_operations: usize,
43    pub rendered_operations: usize,
44    #[serde(default)]
45    pub operation_counts: RestoreApplyOperationKindCounts,
46    pub artifact_validation: Option<RestoreApplyArtifactValidation>,
47    pub operations: Vec<RestoreApplyDryRunOperation>,
48}
49
50impl RestoreApplyDryRun {
51    /// Build a no-mutation apply dry-run from a restore plan.
52    #[must_use]
53    pub fn from_plan(plan: &RestorePlan) -> Self {
54        Self::from_validated_plan(plan)
55    }
56
57    /// Build an apply dry-run and verify all referenced artifacts under a backup root.
58    pub fn try_from_plan_with_artifacts(
59        plan: &RestorePlan,
60        backup_root: &Path,
61    ) -> Result<Self, RestoreApplyDryRunError> {
62        let mut dry_run = Self::from_plan(plan);
63        dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
64        Ok(dry_run)
65    }
66
67    // Build a no-mutation apply dry-run from a restore plan.
68    fn from_validated_plan(plan: &RestorePlan) -> Self {
69        let mut next_sequence = 0;
70        let ordered_members = plan.ordered_members();
71        let mut operations = Vec::new();
72        append_member_phase(
73            &mut operations,
74            &mut next_sequence,
75            RestoreApplyOperationKind::UploadSnapshot,
76            ordered_members.iter().copied(),
77        );
78        append_member_phase(
79            &mut operations,
80            &mut next_sequence,
81            RestoreApplyOperationKind::StopCanister,
82            ordered_members.iter().copied(),
83        );
84        append_member_phase(
85            &mut operations,
86            &mut next_sequence,
87            RestoreApplyOperationKind::LoadSnapshot,
88            ordered_members.iter().copied(),
89        );
90        append_member_phase(
91            &mut operations,
92            &mut next_sequence,
93            RestoreApplyOperationKind::StartCanister,
94            ordered_members.iter().rev().copied(),
95        );
96        append_member_verification_operations(
97            &mut operations,
98            &mut next_sequence,
99            &ordered_members,
100        );
101        append_fleet_verification_operations(plan, &mut operations, &mut next_sequence);
102        let rendered_operations = operations.len();
103        let operation_counts =
104            RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
105
106        Self {
107            dry_run_version: 1,
108            backup_id: plan.backup_id.clone(),
109            ready: plan.readiness_summary.ready,
110            readiness_reasons: plan.readiness_summary.reasons.clone(),
111            member_count: plan.member_count,
112            planned_canister_stops: plan.operation_summary.planned_canister_stops,
113            planned_canister_starts: plan.operation_summary.planned_canister_starts,
114            planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
115            planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
116            planned_verification_checks: plan.operation_summary.planned_verification_checks,
117            planned_operations: plan.operation_summary.planned_operations,
118            rendered_operations,
119            operation_counts,
120            artifact_validation: None,
121            operations,
122        }
123    }
124}
125
126// Verify every planned restore artifact against one backup directory root.
127fn validate_restore_apply_artifacts(
128    plan: &RestorePlan,
129    backup_root: &Path,
130) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
131    let mut checks = Vec::new();
132
133    for member in plan.ordered_members() {
134        checks.push(validate_restore_apply_artifact(member, backup_root)?);
135    }
136
137    let members_with_expected_checksums = checks
138        .iter()
139        .filter(|check| check.checksum_expected.is_some())
140        .count();
141    let artifacts_present = checks.iter().all(|check| check.exists);
142    let checksums_verified = members_with_expected_checksums == plan.member_count
143        && checks.iter().all(|check| check.checksum_verified);
144
145    Ok(RestoreApplyArtifactValidation {
146        backup_root: backup_root.to_string_lossy().to_string(),
147        checked_members: checks.len(),
148        artifacts_present,
149        checksums_verified,
150        members_with_expected_checksums,
151        checks,
152    })
153}
154
155// Verify one planned restore artifact path and checksum.
156fn validate_restore_apply_artifact(
157    member: &RestorePlanMember,
158    backup_root: &Path,
159) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
160    let resolved_path = safe_restore_artifact_path(
161        backup_root,
162        &member.source_canister,
163        &member.source_snapshot.artifact_path,
164    )?;
165
166    if !resolved_path.exists() {
167        return Err(RestoreApplyDryRunError::ArtifactMissing {
168            source_canister: member.source_canister.clone(),
169            artifact_path: member.source_snapshot.artifact_path.clone(),
170            resolved_path: resolved_path.to_string_lossy().to_string(),
171        });
172    }
173
174    let (checksum_actual, checksum_verified) =
175        if let Some(expected) = &member.source_snapshot.checksum {
176            let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
177                RestoreApplyDryRunError::ArtifactChecksum {
178                    source_canister: member.source_canister.clone(),
179                    artifact_path: member.source_snapshot.artifact_path.clone(),
180                    source,
181                }
182            })?;
183            checksum.verify(expected).map_err(|source| {
184                RestoreApplyDryRunError::ArtifactChecksum {
185                    source_canister: member.source_canister.clone(),
186                    artifact_path: member.source_snapshot.artifact_path.clone(),
187                    source,
188                }
189            })?;
190            (Some(checksum.hash), true)
191        } else {
192            (None, false)
193        };
194
195    Ok(RestoreApplyArtifactCheck {
196        source_canister: member.source_canister.clone(),
197        target_canister: member.target_canister.clone(),
198        snapshot_id: member.source_snapshot.snapshot_id.clone(),
199        artifact_path: member.source_snapshot.artifact_path.clone(),
200        resolved_path: resolved_path.to_string_lossy().to_string(),
201        exists: true,
202        checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
203        checksum_expected: member.source_snapshot.checksum.clone(),
204        checksum_actual,
205        checksum_verified,
206    })
207}
208
209// Reject absolute paths and parent traversal before joining with the backup root.
210fn safe_restore_artifact_path(
211    backup_root: &Path,
212    source_canister: &str,
213    artifact_path: &str,
214) -> Result<PathBuf, RestoreApplyDryRunError> {
215    if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
216        return Ok(path);
217    }
218
219    Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
220        source_canister: source_canister.to_string(),
221        artifact_path: artifact_path.to_string(),
222    })
223}
224
225///
226/// RestoreApplyArtifactValidation
227///
228
229#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230pub struct RestoreApplyArtifactValidation {
231    pub backup_root: String,
232    pub checked_members: usize,
233    pub artifacts_present: bool,
234    pub checksums_verified: bool,
235    pub members_with_expected_checksums: usize,
236    pub checks: Vec<RestoreApplyArtifactCheck>,
237}
238
239///
240/// RestoreApplyArtifactCheck
241///
242
243#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
244pub struct RestoreApplyArtifactCheck {
245    pub source_canister: String,
246    pub target_canister: String,
247    pub snapshot_id: String,
248    pub artifact_path: String,
249    pub resolved_path: String,
250    pub exists: bool,
251    pub checksum_algorithm: String,
252    pub checksum_expected: Option<String>,
253    pub checksum_actual: Option<String>,
254    pub checksum_verified: bool,
255}
256
257// Append a full member operation phase across the selected topology.
258fn append_member_phase<'a>(
259    operations: &mut Vec<RestoreApplyDryRunOperation>,
260    next_sequence: &mut usize,
261    operation: RestoreApplyOperationKind,
262    members: impl IntoIterator<Item = &'a RestorePlanMember>,
263) {
264    for member in members {
265        push_member_operation(operations, next_sequence, operation.clone(), member, None);
266    }
267}
268
269// Append member verification checks after restore mutations complete.
270fn append_member_verification_operations(
271    operations: &mut Vec<RestoreApplyDryRunOperation>,
272    next_sequence: &mut usize,
273    members: &[&RestorePlanMember],
274) {
275    for member in members {
276        for check in &member.verification_checks {
277            push_member_operation(
278                operations,
279                next_sequence,
280                RestoreApplyOperationKind::VerifyMember,
281                member,
282                Some(check),
283            );
284        }
285    }
286}
287
288// Append one member-level dry-run operation using the member's restore order.
289fn push_member_operation(
290    operations: &mut Vec<RestoreApplyDryRunOperation>,
291    next_sequence: &mut usize,
292    operation: RestoreApplyOperationKind,
293    member: &RestorePlanMember,
294    check: Option<&VerificationCheck>,
295) {
296    let sequence = *next_sequence;
297    *next_sequence += 1;
298
299    let snapshot_id = operation_snapshot_id(&operation, member);
300    let artifact_path = operation_artifact_path(&operation, member);
301
302    operations.push(RestoreApplyDryRunOperation {
303        sequence,
304        operation,
305        member_order: member.member_order,
306        source_canister: member.source_canister.clone(),
307        target_canister: member.target_canister.clone(),
308        role: member.role.clone(),
309        snapshot_id,
310        artifact_path,
311        verification_kind: check.map(|check| check.kind.clone()),
312    });
313}
314
315fn operation_snapshot_id(
316    operation: &RestoreApplyOperationKind,
317    member: &RestorePlanMember,
318) -> Option<String> {
319    matches!(
320        operation,
321        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
322    )
323    .then(|| member.source_snapshot.snapshot_id.clone())
324}
325
326fn operation_artifact_path(
327    operation: &RestoreApplyOperationKind,
328    member: &RestorePlanMember,
329) -> Option<String> {
330    matches!(
331        operation,
332        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
333    )
334    .then(|| member.source_snapshot.artifact_path.clone())
335}
336
337// Append fleet-level verification checks after all member operations.
338fn append_fleet_verification_operations(
339    plan: &RestorePlan,
340    operations: &mut Vec<RestoreApplyDryRunOperation>,
341    next_sequence: &mut usize,
342) {
343    if plan.fleet_verification_checks.is_empty() {
344        return;
345    }
346
347    let root = plan
348        .members
349        .iter()
350        .find(|member| member.source_canister == plan.source_root_canister);
351    let source_canister = root.map_or_else(
352        || plan.source_root_canister.clone(),
353        |member| member.source_canister.clone(),
354    );
355    let target_canister = root.map_or_else(
356        || plan.source_root_canister.clone(),
357        |member| member.target_canister.clone(),
358    );
359    for check in &plan.fleet_verification_checks {
360        push_fleet_operation(
361            operations,
362            next_sequence,
363            &source_canister,
364            &target_canister,
365            check,
366        );
367    }
368}
369
370// Append one fleet-level dry-run verification operation.
371fn push_fleet_operation(
372    operations: &mut Vec<RestoreApplyDryRunOperation>,
373    next_sequence: &mut usize,
374    source_canister: &str,
375    target_canister: &str,
376    check: &VerificationCheck,
377) {
378    let sequence = *next_sequence;
379    *next_sequence += 1;
380    let member_order = operations.len();
381
382    operations.push(RestoreApplyDryRunOperation {
383        sequence,
384        operation: RestoreApplyOperationKind::VerifyFleet,
385        member_order,
386        source_canister: source_canister.to_string(),
387        target_canister: target_canister.to_string(),
388        role: "fleet".to_string(),
389        snapshot_id: None,
390        artifact_path: None,
391        verification_kind: Some(check.kind.clone()),
392    });
393}
394
395///
396/// RestoreApplyDryRunOperation
397///
398
399#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
400pub struct RestoreApplyDryRunOperation {
401    pub sequence: usize,
402    pub operation: RestoreApplyOperationKind,
403    pub member_order: usize,
404    pub source_canister: String,
405    pub target_canister: String,
406    pub role: String,
407    pub snapshot_id: Option<String>,
408    pub artifact_path: Option<String>,
409    pub verification_kind: Option<String>,
410}
411
412///
413/// RestoreApplyDryRunError
414///
415
416#[derive(Debug, ThisError)]
417pub enum RestoreApplyDryRunError {
418    #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
419    ArtifactPathEscapesBackup {
420        source_canister: String,
421        artifact_path: String,
422    },
423
424    #[error(
425        "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
426    )]
427    ArtifactMissing {
428        source_canister: String,
429        artifact_path: String,
430        resolved_path: String,
431    },
432
433    #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
434    ArtifactChecksum {
435        source_canister: String,
436        artifact_path: String,
437        #[source]
438        source: ArtifactChecksumError,
439    },
440}