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