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