1use super::{RestorePhase, RestorePlan, RestorePlanMember, RestoreStatus};
2use crate::{
3 artifacts::{ArtifactChecksum, ArtifactChecksumError},
4 manifest::VerificationCheck,
5};
6use serde::{Deserialize, Serialize};
7use std::path::{Component, Path, PathBuf};
8use thiserror::Error as ThisError;
9
10mod journal;
11
12pub use journal::{
13 RestoreApplyCommandConfig, RestoreApplyCommandOutput, RestoreApplyCommandOutputPair,
14 RestoreApplyCommandPreview, RestoreApplyJournal, RestoreApplyJournalError,
15 RestoreApplyJournalOperation, RestoreApplyJournalReport, RestoreApplyJournalStatus,
16 RestoreApplyNextOperation, RestoreApplyOperationKind, RestoreApplyOperationKindCounts,
17 RestoreApplyOperationReceipt, RestoreApplyOperationReceiptOutcome, RestoreApplyOperationState,
18 RestoreApplyPendingSummary, RestoreApplyProgressSummary, RestoreApplyReportOperation,
19 RestoreApplyReportOutcome, RestoreApplyRunnerCommand,
20};
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
27pub struct RestoreApplyDryRun {
28 pub dry_run_version: u16,
29 pub backup_id: String,
30 pub ready: bool,
31 pub readiness_reasons: Vec<String>,
32 pub member_count: usize,
33 pub phase_count: usize,
34 pub status_supplied: bool,
35 #[serde(default)]
36 pub planned_snapshot_uploads: usize,
37 pub planned_snapshot_loads: usize,
38 pub planned_code_reinstalls: usize,
39 pub planned_verification_checks: usize,
40 #[serde(default)]
41 pub planned_operations: usize,
42 pub rendered_operations: usize,
43 #[serde(default)]
44 pub operation_counts: RestoreApplyOperationKindCounts,
45 pub artifact_validation: Option<RestoreApplyArtifactValidation>,
46 pub phases: Vec<RestoreApplyDryRunPhase>,
47}
48
49impl RestoreApplyDryRun {
50 pub fn try_from_plan(
52 plan: &RestorePlan,
53 status: Option<&RestoreStatus>,
54 ) -> Result<Self, RestoreApplyDryRunError> {
55 if let Some(status) = status {
56 validate_restore_status_matches_plan(plan, status)?;
57 }
58
59 Ok(Self::from_validated_plan(plan, status))
60 }
61
62 pub fn try_from_plan_with_artifacts(
64 plan: &RestorePlan,
65 status: Option<&RestoreStatus>,
66 backup_root: &Path,
67 ) -> Result<Self, RestoreApplyDryRunError> {
68 let mut dry_run = Self::try_from_plan(plan, status)?;
69 dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
70 Ok(dry_run)
71 }
72
73 fn from_validated_plan(plan: &RestorePlan, status: Option<&RestoreStatus>) -> Self {
75 let mut next_sequence = 0;
76 let phases = plan
77 .phases
78 .iter()
79 .map(|phase| RestoreApplyDryRunPhase::from_plan_phase(phase, &mut next_sequence))
80 .collect::<Vec<_>>();
81 let mut phases = phases;
82 append_fleet_verification_operations(plan, &mut phases, &mut next_sequence);
83 let rendered_operations = phases
84 .iter()
85 .map(|phase| phase.operations.len())
86 .sum::<usize>();
87 let operation_counts = RestoreApplyOperationKindCounts::from_dry_run_phases(&phases);
88
89 Self {
90 dry_run_version: 1,
91 backup_id: plan.backup_id.clone(),
92 ready: status.map_or(plan.readiness_summary.ready, |status| status.ready),
93 readiness_reasons: status.map_or_else(
94 || plan.readiness_summary.reasons.clone(),
95 |status| status.readiness_reasons.clone(),
96 ),
97 member_count: plan.member_count,
98 phase_count: plan.ordering_summary.phase_count,
99 status_supplied: status.is_some(),
100 planned_snapshot_uploads: plan
101 .operation_summary
102 .effective_planned_snapshot_uploads(plan.member_count),
103 planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
104 planned_code_reinstalls: plan.operation_summary.planned_code_reinstalls,
105 planned_verification_checks: plan.operation_summary.planned_verification_checks,
106 planned_operations: plan
107 .operation_summary
108 .effective_planned_operations(plan.member_count),
109 rendered_operations,
110 operation_counts,
111 artifact_validation: None,
112 phases,
113 }
114 }
115}
116
117fn validate_restore_apply_artifacts(
119 plan: &RestorePlan,
120 backup_root: &Path,
121) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
122 let mut checks = Vec::new();
123
124 for member in plan.ordered_members() {
125 checks.push(validate_restore_apply_artifact(member, backup_root)?);
126 }
127
128 let members_with_expected_checksums = checks
129 .iter()
130 .filter(|check| check.checksum_expected.is_some())
131 .count();
132 let artifacts_present = checks.iter().all(|check| check.exists);
133 let checksums_verified = members_with_expected_checksums == plan.member_count
134 && checks.iter().all(|check| check.checksum_verified);
135
136 Ok(RestoreApplyArtifactValidation {
137 backup_root: backup_root.to_string_lossy().to_string(),
138 checked_members: checks.len(),
139 artifacts_present,
140 checksums_verified,
141 members_with_expected_checksums,
142 checks,
143 })
144}
145
146fn validate_restore_apply_artifact(
148 member: &RestorePlanMember,
149 backup_root: &Path,
150) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
151 let artifact_path = safe_restore_artifact_path(
152 &member.source_canister,
153 &member.source_snapshot.artifact_path,
154 )?;
155 let resolved_path = backup_root.join(&artifact_path);
156
157 if !resolved_path.exists() {
158 return Err(RestoreApplyDryRunError::ArtifactMissing {
159 source_canister: member.source_canister.clone(),
160 artifact_path: member.source_snapshot.artifact_path.clone(),
161 resolved_path: resolved_path.to_string_lossy().to_string(),
162 });
163 }
164
165 let (checksum_actual, checksum_verified) =
166 if let Some(expected) = &member.source_snapshot.checksum {
167 let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
168 RestoreApplyDryRunError::ArtifactChecksum {
169 source_canister: member.source_canister.clone(),
170 artifact_path: member.source_snapshot.artifact_path.clone(),
171 source,
172 }
173 })?;
174 checksum.verify(expected).map_err(|source| {
175 RestoreApplyDryRunError::ArtifactChecksum {
176 source_canister: member.source_canister.clone(),
177 artifact_path: member.source_snapshot.artifact_path.clone(),
178 source,
179 }
180 })?;
181 (Some(checksum.hash), true)
182 } else {
183 (None, false)
184 };
185
186 Ok(RestoreApplyArtifactCheck {
187 source_canister: member.source_canister.clone(),
188 target_canister: member.target_canister.clone(),
189 snapshot_id: member.source_snapshot.snapshot_id.clone(),
190 artifact_path: member.source_snapshot.artifact_path.clone(),
191 resolved_path: resolved_path.to_string_lossy().to_string(),
192 exists: true,
193 checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
194 checksum_expected: member.source_snapshot.checksum.clone(),
195 checksum_actual,
196 checksum_verified,
197 })
198}
199
200fn safe_restore_artifact_path(
202 source_canister: &str,
203 artifact_path: &str,
204) -> Result<PathBuf, RestoreApplyDryRunError> {
205 let path = Path::new(artifact_path);
206 let is_safe = path
207 .components()
208 .all(|component| matches!(component, Component::Normal(_) | Component::CurDir));
209
210 if is_safe {
211 return Ok(path.to_path_buf());
212 }
213
214 Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
215 source_canister: source_canister.to_string(),
216 artifact_path: artifact_path.to_string(),
217 })
218}
219
220fn validate_restore_status_matches_plan(
222 plan: &RestorePlan,
223 status: &RestoreStatus,
224) -> Result<(), RestoreApplyDryRunError> {
225 validate_status_string_field("backup_id", &plan.backup_id, &status.backup_id)?;
226 validate_status_string_field(
227 "source_environment",
228 &plan.source_environment,
229 &status.source_environment,
230 )?;
231 validate_status_string_field(
232 "source_root_canister",
233 &plan.source_root_canister,
234 &status.source_root_canister,
235 )?;
236 validate_status_string_field("topology_hash", &plan.topology_hash, &status.topology_hash)?;
237 validate_status_usize_field("member_count", plan.member_count, status.member_count)?;
238 validate_status_usize_field(
239 "phase_count",
240 plan.ordering_summary.phase_count,
241 status.phase_count,
242 )?;
243 Ok(())
244}
245
246fn validate_status_string_field(
248 field: &'static str,
249 plan: &str,
250 status: &str,
251) -> Result<(), RestoreApplyDryRunError> {
252 if plan == status {
253 return Ok(());
254 }
255
256 Err(RestoreApplyDryRunError::StatusPlanMismatch {
257 field,
258 plan: plan.to_string(),
259 status: status.to_string(),
260 })
261}
262
263const fn validate_status_usize_field(
265 field: &'static str,
266 plan: usize,
267 status: usize,
268) -> Result<(), RestoreApplyDryRunError> {
269 if plan == status {
270 return Ok(());
271 }
272
273 Err(RestoreApplyDryRunError::StatusPlanCountMismatch {
274 field,
275 plan,
276 status,
277 })
278}
279
280#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
285pub struct RestoreApplyArtifactValidation {
286 pub backup_root: String,
287 pub checked_members: usize,
288 pub artifacts_present: bool,
289 pub checksums_verified: bool,
290 pub members_with_expected_checksums: usize,
291 pub checks: Vec<RestoreApplyArtifactCheck>,
292}
293
294#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
299pub struct RestoreApplyArtifactCheck {
300 pub source_canister: String,
301 pub target_canister: String,
302 pub snapshot_id: String,
303 pub artifact_path: String,
304 pub resolved_path: String,
305 pub exists: bool,
306 pub checksum_algorithm: String,
307 pub checksum_expected: Option<String>,
308 pub checksum_actual: Option<String>,
309 pub checksum_verified: bool,
310}
311
312#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
317pub struct RestoreApplyDryRunPhase {
318 pub restore_group: u16,
319 pub operations: Vec<RestoreApplyDryRunOperation>,
320}
321
322impl RestoreApplyDryRunPhase {
323 fn from_plan_phase(phase: &RestorePhase, next_sequence: &mut usize) -> Self {
325 let mut operations = Vec::new();
326
327 for member in &phase.members {
328 push_member_operation(
329 &mut operations,
330 next_sequence,
331 RestoreApplyOperationKind::UploadSnapshot,
332 member,
333 None,
334 );
335 push_member_operation(
336 &mut operations,
337 next_sequence,
338 RestoreApplyOperationKind::LoadSnapshot,
339 member,
340 None,
341 );
342
343 for check in &member.verification_checks {
344 push_member_operation(
345 &mut operations,
346 next_sequence,
347 RestoreApplyOperationKind::VerifyMember,
348 member,
349 Some(check),
350 );
351 }
352 }
353
354 Self {
355 restore_group: phase.restore_group,
356 operations,
357 }
358 }
359}
360
361fn push_member_operation(
363 operations: &mut Vec<RestoreApplyDryRunOperation>,
364 next_sequence: &mut usize,
365 operation: RestoreApplyOperationKind,
366 member: &RestorePlanMember,
367 check: Option<&VerificationCheck>,
368) {
369 let sequence = *next_sequence;
370 *next_sequence += 1;
371
372 operations.push(RestoreApplyDryRunOperation {
373 sequence,
374 operation,
375 restore_group: member.restore_group,
376 phase_order: member.phase_order,
377 source_canister: member.source_canister.clone(),
378 target_canister: member.target_canister.clone(),
379 role: member.role.clone(),
380 snapshot_id: Some(member.source_snapshot.snapshot_id.clone()),
381 artifact_path: Some(member.source_snapshot.artifact_path.clone()),
382 verification_kind: check.map(|check| check.kind.clone()),
383 verification_method: check.and_then(|check| check.method.clone()),
384 });
385}
386
387fn append_fleet_verification_operations(
389 plan: &RestorePlan,
390 phases: &mut [RestoreApplyDryRunPhase],
391 next_sequence: &mut usize,
392) {
393 if plan.fleet_verification_checks.is_empty() {
394 return;
395 }
396
397 let Some(phase) = phases.last_mut() else {
398 return;
399 };
400 let root = plan
401 .phases
402 .iter()
403 .flat_map(|phase| phase.members.iter())
404 .find(|member| member.source_canister == plan.source_root_canister);
405 let source_canister = root.map_or_else(
406 || plan.source_root_canister.clone(),
407 |member| member.source_canister.clone(),
408 );
409 let target_canister = root.map_or_else(
410 || plan.source_root_canister.clone(),
411 |member| member.target_canister.clone(),
412 );
413 let restore_group = phase.restore_group;
414
415 for check in &plan.fleet_verification_checks {
416 push_fleet_operation(
417 &mut phase.operations,
418 next_sequence,
419 restore_group,
420 &source_canister,
421 &target_canister,
422 check,
423 );
424 }
425}
426
427fn push_fleet_operation(
429 operations: &mut Vec<RestoreApplyDryRunOperation>,
430 next_sequence: &mut usize,
431 restore_group: u16,
432 source_canister: &str,
433 target_canister: &str,
434 check: &VerificationCheck,
435) {
436 let sequence = *next_sequence;
437 *next_sequence += 1;
438 let phase_order = operations.len();
439
440 operations.push(RestoreApplyDryRunOperation {
441 sequence,
442 operation: RestoreApplyOperationKind::VerifyFleet,
443 restore_group,
444 phase_order,
445 source_canister: source_canister.to_string(),
446 target_canister: target_canister.to_string(),
447 role: "fleet".to_string(),
448 snapshot_id: None,
449 artifact_path: None,
450 verification_kind: Some(check.kind.clone()),
451 verification_method: check.method.clone(),
452 });
453}
454
455#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
460pub struct RestoreApplyDryRunOperation {
461 pub sequence: usize,
462 pub operation: RestoreApplyOperationKind,
463 pub restore_group: u16,
464 pub phase_order: usize,
465 pub source_canister: String,
466 pub target_canister: String,
467 pub role: String,
468 pub snapshot_id: Option<String>,
469 pub artifact_path: Option<String>,
470 pub verification_kind: Option<String>,
471 pub verification_method: Option<String>,
472}
473
474#[derive(Debug, ThisError)]
479pub enum RestoreApplyDryRunError {
480 #[error("restore status field {field} does not match plan: plan={plan}, status={status}")]
481 StatusPlanMismatch {
482 field: &'static str,
483 plan: String,
484 status: String,
485 },
486
487 #[error("restore status field {field} does not match plan: plan={plan}, status={status}")]
488 StatusPlanCountMismatch {
489 field: &'static str,
490 plan: usize,
491 status: usize,
492 },
493
494 #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
495 ArtifactPathEscapesBackup {
496 source_canister: String,
497 artifact_path: String,
498 },
499
500 #[error(
501 "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
502 )]
503 ArtifactMissing {
504 source_canister: String,
505 artifact_path: String,
506 resolved_path: String,
507 },
508
509 #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
510 ArtifactChecksum {
511 source_canister: String,
512 artifact_path: String,
513 #[source]
514 source: ArtifactChecksumError,
515 },
516}