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#[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 pub fn from_plan(plan: &RestorePlan) -> Result<Self, RestorePlanError> {
54 plan.validate()?;
55 Ok(Self::from_validated_plan(plan))
56 }
57
58 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 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
127fn 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
157fn 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(¤t) {
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
284fn 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#[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#[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
334fn 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
346fn 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
365fn 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
437fn 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
470fn 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#[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#[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}