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)]
29#[serde(deny_unknown_fields)]
30pub struct RestoreApplyDryRun {
31    pub dry_run_version: u16,
32    pub backup_id: String,
33    pub ready: bool,
34    pub readiness_reasons: Vec<String>,
35    pub member_count: usize,
36    #[serde(default)]
37    pub planned_canister_stops: usize,
38    #[serde(default)]
39    pub planned_canister_starts: usize,
40    pub planned_snapshot_uploads: usize,
41    pub planned_snapshot_loads: usize,
42    pub planned_verification_checks: usize,
43    pub planned_operations: usize,
44    pub rendered_operations: usize,
45    #[serde(default)]
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    #[must_use]
54    pub fn from_plan(plan: &RestorePlan) -> Self {
55        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 mut checks = Vec::new();
133
134    for member in plan.ordered_members() {
135        checks.push(validate_restore_apply_artifact(member, backup_root)?);
136    }
137
138    let members_with_expected_checksums = checks
139        .iter()
140        .filter(|check| check.checksum_expected.is_some())
141        .count();
142    let artifacts_present = checks.iter().all(|check| check.exists);
143    let checksums_verified = members_with_expected_checksums == plan.member_count
144        && checks.iter().all(|check| check.checksum_verified);
145
146    Ok(RestoreApplyArtifactValidation {
147        backup_root: backup_root.to_string_lossy().to_string(),
148        checked_members: checks.len(),
149        artifacts_present,
150        checksums_verified,
151        members_with_expected_checksums,
152        checks,
153    })
154}
155
156// Verify one planned restore artifact path and checksum.
157fn validate_restore_apply_artifact(
158    member: &RestorePlanMember,
159    backup_root: &Path,
160) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
161    let resolved_path = safe_restore_artifact_path(
162        backup_root,
163        &member.source_canister,
164        &member.source_snapshot.artifact_path,
165    )?;
166
167    if !resolved_path.exists() {
168        return Err(RestoreApplyDryRunError::ArtifactMissing {
169            source_canister: member.source_canister.clone(),
170            artifact_path: member.source_snapshot.artifact_path.clone(),
171            resolved_path: resolved_path.to_string_lossy().to_string(),
172        });
173    }
174
175    let (checksum_actual, checksum_verified) =
176        if let Some(expected) = &member.source_snapshot.checksum {
177            let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
178                RestoreApplyDryRunError::ArtifactChecksum {
179                    source_canister: member.source_canister.clone(),
180                    artifact_path: member.source_snapshot.artifact_path.clone(),
181                    source,
182                }
183            })?;
184            checksum.verify(expected).map_err(|source| {
185                RestoreApplyDryRunError::ArtifactChecksum {
186                    source_canister: member.source_canister.clone(),
187                    artifact_path: member.source_snapshot.artifact_path.clone(),
188                    source,
189                }
190            })?;
191            (Some(checksum.hash), true)
192        } else {
193            (None, false)
194        };
195
196    Ok(RestoreApplyArtifactCheck {
197        source_canister: member.source_canister.clone(),
198        target_canister: member.target_canister.clone(),
199        snapshot_id: member.source_snapshot.snapshot_id.clone(),
200        artifact_path: member.source_snapshot.artifact_path.clone(),
201        resolved_path: resolved_path.to_string_lossy().to_string(),
202        exists: true,
203        checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
204        checksum_expected: member.source_snapshot.checksum.clone(),
205        checksum_actual,
206        checksum_verified,
207    })
208}
209
210// Reject absolute paths and parent traversal before joining with the backup root.
211fn safe_restore_artifact_path(
212    backup_root: &Path,
213    source_canister: &str,
214    artifact_path: &str,
215) -> Result<PathBuf, RestoreApplyDryRunError> {
216    if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
217        return Ok(path);
218    }
219
220    Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
221        source_canister: source_canister.to_string(),
222        artifact_path: artifact_path.to_string(),
223    })
224}
225
226///
227/// RestoreApplyArtifactValidation
228///
229
230#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
231#[serde(deny_unknown_fields)]
232pub struct RestoreApplyArtifactValidation {
233    pub backup_root: String,
234    pub checked_members: usize,
235    pub artifacts_present: bool,
236    pub checksums_verified: bool,
237    pub members_with_expected_checksums: usize,
238    pub checks: Vec<RestoreApplyArtifactCheck>,
239}
240
241///
242/// RestoreApplyArtifactCheck
243///
244
245#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246#[serde(deny_unknown_fields)]
247pub struct RestoreApplyArtifactCheck {
248    pub source_canister: String,
249    pub target_canister: String,
250    pub snapshot_id: String,
251    pub artifact_path: String,
252    pub resolved_path: String,
253    pub exists: bool,
254    pub checksum_algorithm: String,
255    pub checksum_expected: Option<String>,
256    pub checksum_actual: Option<String>,
257    pub checksum_verified: bool,
258}
259
260// Append a full member operation phase across the selected topology.
261fn append_member_phase<'a>(
262    operations: &mut Vec<RestoreApplyDryRunOperation>,
263    next_sequence: &mut usize,
264    operation: RestoreApplyOperationKind,
265    members: impl IntoIterator<Item = &'a RestorePlanMember>,
266) {
267    for member in members {
268        push_member_operation(operations, next_sequence, operation.clone(), member, None);
269    }
270}
271
272// Append member verification checks after restore mutations complete.
273fn append_member_verification_operations(
274    operations: &mut Vec<RestoreApplyDryRunOperation>,
275    next_sequence: &mut usize,
276    members: &[&RestorePlanMember],
277) {
278    for member in members {
279        for check in &member.verification_checks {
280            push_member_operation(
281                operations,
282                next_sequence,
283                RestoreApplyOperationKind::VerifyMember,
284                member,
285                Some(check),
286            );
287        }
288    }
289}
290
291// Append one member-level dry-run operation using the member's restore order.
292fn push_member_operation(
293    operations: &mut Vec<RestoreApplyDryRunOperation>,
294    next_sequence: &mut usize,
295    operation: RestoreApplyOperationKind,
296    member: &RestorePlanMember,
297    check: Option<&VerificationCheck>,
298) {
299    let sequence = *next_sequence;
300    *next_sequence += 1;
301
302    let snapshot_id = operation_snapshot_id(&operation, member);
303    let artifact_path = operation_artifact_path(&operation, member);
304
305    operations.push(RestoreApplyDryRunOperation {
306        sequence,
307        operation,
308        member_order: member.member_order,
309        source_canister: member.source_canister.clone(),
310        target_canister: member.target_canister.clone(),
311        role: member.role.clone(),
312        snapshot_id,
313        artifact_path,
314        verification_kind: check.map(|check| check.kind.clone()),
315    });
316}
317
318fn operation_snapshot_id(
319    operation: &RestoreApplyOperationKind,
320    member: &RestorePlanMember,
321) -> Option<String> {
322    matches!(
323        operation,
324        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
325    )
326    .then(|| member.source_snapshot.snapshot_id.clone())
327}
328
329fn operation_artifact_path(
330    operation: &RestoreApplyOperationKind,
331    member: &RestorePlanMember,
332) -> Option<String> {
333    matches!(
334        operation,
335        RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
336    )
337    .then(|| member.source_snapshot.artifact_path.clone())
338}
339
340// Append deployment-level verification checks after all member operations.
341fn append_deployment_verification_operations(
342    plan: &RestorePlan,
343    operations: &mut Vec<RestoreApplyDryRunOperation>,
344    next_sequence: &mut usize,
345) {
346    if plan.deployment_verification_checks.is_empty() {
347        return;
348    }
349
350    let root = plan
351        .members
352        .iter()
353        .find(|member| member.source_canister == plan.source_root_canister);
354    let source_canister = root.map_or_else(
355        || plan.source_root_canister.clone(),
356        |member| member.source_canister.clone(),
357    );
358    let target_canister = root.map_or_else(
359        || plan.source_root_canister.clone(),
360        |member| member.target_canister.clone(),
361    );
362    for check in &plan.deployment_verification_checks {
363        push_deployment_operation(
364            operations,
365            next_sequence,
366            &source_canister,
367            &target_canister,
368            check,
369        );
370    }
371}
372
373// Append one deployment-level dry-run verification operation.
374fn push_deployment_operation(
375    operations: &mut Vec<RestoreApplyDryRunOperation>,
376    next_sequence: &mut usize,
377    source_canister: &str,
378    target_canister: &str,
379    check: &VerificationCheck,
380) {
381    let sequence = *next_sequence;
382    *next_sequence += 1;
383    let member_order = operations.len();
384
385    operations.push(RestoreApplyDryRunOperation {
386        sequence,
387        operation: RestoreApplyOperationKind::VerifyDeployment,
388        member_order,
389        source_canister: source_canister.to_string(),
390        target_canister: target_canister.to_string(),
391        role: "deployment".to_string(),
392        snapshot_id: None,
393        artifact_path: None,
394        verification_kind: Some(check.kind.clone()),
395    });
396}
397
398///
399/// RestoreApplyDryRunOperation
400///
401
402#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(deny_unknown_fields)]
404pub struct RestoreApplyDryRunOperation {
405    pub sequence: usize,
406    pub operation: RestoreApplyOperationKind,
407    pub member_order: usize,
408    pub source_canister: String,
409    pub target_canister: String,
410    pub role: String,
411    pub snapshot_id: Option<String>,
412    pub artifact_path: Option<String>,
413    pub verification_kind: Option<String>,
414}
415
416///
417/// RestoreApplyDryRunError
418///
419
420#[derive(Debug, ThisError)]
421pub enum RestoreApplyDryRunError {
422    #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
423    ArtifactPathEscapesBackup {
424        source_canister: String,
425        artifact_path: String,
426    },
427
428    #[error(
429        "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
430    )]
431    ArtifactMissing {
432        source_canister: String,
433        artifact_path: String,
434        resolved_path: String,
435    },
436
437    #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
438    ArtifactChecksum {
439        source_canister: String,
440        artifact_path: String,
441        #[source]
442        source: ArtifactChecksumError,
443    },
444}