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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
29pub struct RestoreApplyDryRun {
30 pub dry_run_version: u16,
31 pub backup_id: String,
32 pub ready: bool,
33 pub readiness_reasons: Vec<String>,
34 pub member_count: usize,
35 pub planned_snapshot_uploads: usize,
36 pub planned_snapshot_loads: usize,
37 pub planned_verification_checks: usize,
38 pub planned_operations: usize,
39 pub rendered_operations: usize,
40 #[serde(default)]
41 pub operation_counts: RestoreApplyOperationKindCounts,
42 pub artifact_validation: Option<RestoreApplyArtifactValidation>,
43 pub operations: Vec<RestoreApplyDryRunOperation>,
44}
45
46impl RestoreApplyDryRun {
47 #[must_use]
49 pub fn from_plan(plan: &RestorePlan) -> Self {
50 Self::from_validated_plan(plan)
51 }
52
53 pub fn try_from_plan_with_artifacts(
55 plan: &RestorePlan,
56 backup_root: &Path,
57 ) -> Result<Self, RestoreApplyDryRunError> {
58 let mut dry_run = Self::from_plan(plan);
59 dry_run.artifact_validation = Some(validate_restore_apply_artifacts(plan, backup_root)?);
60 Ok(dry_run)
61 }
62
63 fn from_validated_plan(plan: &RestorePlan) -> Self {
65 let mut next_sequence = 0;
66 let mut operations = plan
67 .members
68 .iter()
69 .flat_map(|member| member_operations(member, &mut next_sequence))
70 .collect::<Vec<_>>();
71 append_fleet_verification_operations(plan, &mut operations, &mut next_sequence);
72 let rendered_operations = operations.len();
73 let operation_counts =
74 RestoreApplyOperationKindCounts::from_dry_run_operations(&operations);
75
76 Self {
77 dry_run_version: 1,
78 backup_id: plan.backup_id.clone(),
79 ready: plan.readiness_summary.ready,
80 readiness_reasons: plan.readiness_summary.reasons.clone(),
81 member_count: plan.member_count,
82 planned_snapshot_uploads: plan.operation_summary.planned_snapshot_uploads,
83 planned_snapshot_loads: plan.operation_summary.planned_snapshot_loads,
84 planned_verification_checks: plan.operation_summary.planned_verification_checks,
85 planned_operations: plan.operation_summary.planned_operations,
86 rendered_operations,
87 operation_counts,
88 artifact_validation: None,
89 operations,
90 }
91 }
92}
93
94fn validate_restore_apply_artifacts(
96 plan: &RestorePlan,
97 backup_root: &Path,
98) -> Result<RestoreApplyArtifactValidation, RestoreApplyDryRunError> {
99 let mut checks = Vec::new();
100
101 for member in plan.ordered_members() {
102 checks.push(validate_restore_apply_artifact(member, backup_root)?);
103 }
104
105 let members_with_expected_checksums = checks
106 .iter()
107 .filter(|check| check.checksum_expected.is_some())
108 .count();
109 let artifacts_present = checks.iter().all(|check| check.exists);
110 let checksums_verified = members_with_expected_checksums == plan.member_count
111 && checks.iter().all(|check| check.checksum_verified);
112
113 Ok(RestoreApplyArtifactValidation {
114 backup_root: backup_root.to_string_lossy().to_string(),
115 checked_members: checks.len(),
116 artifacts_present,
117 checksums_verified,
118 members_with_expected_checksums,
119 checks,
120 })
121}
122
123fn validate_restore_apply_artifact(
125 member: &RestorePlanMember,
126 backup_root: &Path,
127) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
128 let resolved_path = safe_restore_artifact_path(
129 backup_root,
130 &member.source_canister,
131 &member.source_snapshot.artifact_path,
132 )?;
133
134 if !resolved_path.exists() {
135 return Err(RestoreApplyDryRunError::ArtifactMissing {
136 source_canister: member.source_canister.clone(),
137 artifact_path: member.source_snapshot.artifact_path.clone(),
138 resolved_path: resolved_path.to_string_lossy().to_string(),
139 });
140 }
141
142 let (checksum_actual, checksum_verified) =
143 if let Some(expected) = &member.source_snapshot.checksum {
144 let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
145 RestoreApplyDryRunError::ArtifactChecksum {
146 source_canister: member.source_canister.clone(),
147 artifact_path: member.source_snapshot.artifact_path.clone(),
148 source,
149 }
150 })?;
151 checksum.verify(expected).map_err(|source| {
152 RestoreApplyDryRunError::ArtifactChecksum {
153 source_canister: member.source_canister.clone(),
154 artifact_path: member.source_snapshot.artifact_path.clone(),
155 source,
156 }
157 })?;
158 (Some(checksum.hash), true)
159 } else {
160 (None, false)
161 };
162
163 Ok(RestoreApplyArtifactCheck {
164 source_canister: member.source_canister.clone(),
165 target_canister: member.target_canister.clone(),
166 snapshot_id: member.source_snapshot.snapshot_id.clone(),
167 artifact_path: member.source_snapshot.artifact_path.clone(),
168 resolved_path: resolved_path.to_string_lossy().to_string(),
169 exists: true,
170 checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
171 checksum_expected: member.source_snapshot.checksum.clone(),
172 checksum_actual,
173 checksum_verified,
174 })
175}
176
177fn safe_restore_artifact_path(
179 backup_root: &Path,
180 source_canister: &str,
181 artifact_path: &str,
182) -> Result<PathBuf, RestoreApplyDryRunError> {
183 if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
184 return Ok(path);
185 }
186
187 Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
188 source_canister: source_canister.to_string(),
189 artifact_path: artifact_path.to_string(),
190 })
191}
192
193#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198pub struct RestoreApplyArtifactValidation {
199 pub backup_root: String,
200 pub checked_members: usize,
201 pub artifacts_present: bool,
202 pub checksums_verified: bool,
203 pub members_with_expected_checksums: usize,
204 pub checks: Vec<RestoreApplyArtifactCheck>,
205}
206
207#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
212pub struct RestoreApplyArtifactCheck {
213 pub source_canister: String,
214 pub target_canister: String,
215 pub snapshot_id: String,
216 pub artifact_path: String,
217 pub resolved_path: String,
218 pub exists: bool,
219 pub checksum_algorithm: String,
220 pub checksum_expected: Option<String>,
221 pub checksum_actual: Option<String>,
222 pub checksum_verified: bool,
223}
224
225fn member_operations(
227 member: &RestorePlanMember,
228 next_sequence: &mut usize,
229) -> Vec<RestoreApplyDryRunOperation> {
230 let mut operations = Vec::new();
231 push_member_operation(
232 &mut operations,
233 next_sequence,
234 RestoreApplyOperationKind::UploadSnapshot,
235 member,
236 None,
237 );
238 push_member_operation(
239 &mut operations,
240 next_sequence,
241 RestoreApplyOperationKind::LoadSnapshot,
242 member,
243 None,
244 );
245
246 for check in &member.verification_checks {
247 push_member_operation(
248 &mut operations,
249 next_sequence,
250 RestoreApplyOperationKind::VerifyMember,
251 member,
252 Some(check),
253 );
254 }
255
256 operations
257}
258
259fn push_member_operation(
261 operations: &mut Vec<RestoreApplyDryRunOperation>,
262 next_sequence: &mut usize,
263 operation: RestoreApplyOperationKind,
264 member: &RestorePlanMember,
265 check: Option<&VerificationCheck>,
266) {
267 let sequence = *next_sequence;
268 *next_sequence += 1;
269
270 operations.push(RestoreApplyDryRunOperation {
271 sequence,
272 operation,
273 member_order: member.member_order,
274 source_canister: member.source_canister.clone(),
275 target_canister: member.target_canister.clone(),
276 role: member.role.clone(),
277 snapshot_id: Some(member.source_snapshot.snapshot_id.clone()),
278 artifact_path: Some(member.source_snapshot.artifact_path.clone()),
279 verification_kind: check.map(|check| check.kind.clone()),
280 });
281}
282
283fn append_fleet_verification_operations(
285 plan: &RestorePlan,
286 operations: &mut Vec<RestoreApplyDryRunOperation>,
287 next_sequence: &mut usize,
288) {
289 if plan.fleet_verification_checks.is_empty() {
290 return;
291 }
292
293 let root = plan
294 .members
295 .iter()
296 .find(|member| member.source_canister == plan.source_root_canister);
297 let source_canister = root.map_or_else(
298 || plan.source_root_canister.clone(),
299 |member| member.source_canister.clone(),
300 );
301 let target_canister = root.map_or_else(
302 || plan.source_root_canister.clone(),
303 |member| member.target_canister.clone(),
304 );
305 for check in &plan.fleet_verification_checks {
306 push_fleet_operation(
307 operations,
308 next_sequence,
309 &source_canister,
310 &target_canister,
311 check,
312 );
313 }
314}
315
316fn push_fleet_operation(
318 operations: &mut Vec<RestoreApplyDryRunOperation>,
319 next_sequence: &mut usize,
320 source_canister: &str,
321 target_canister: &str,
322 check: &VerificationCheck,
323) {
324 let sequence = *next_sequence;
325 *next_sequence += 1;
326 let member_order = operations.len();
327
328 operations.push(RestoreApplyDryRunOperation {
329 sequence,
330 operation: RestoreApplyOperationKind::VerifyFleet,
331 member_order,
332 source_canister: source_canister.to_string(),
333 target_canister: target_canister.to_string(),
334 role: "fleet".to_string(),
335 snapshot_id: None,
336 artifact_path: None,
337 verification_kind: Some(check.kind.clone()),
338 });
339}
340
341#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
346pub struct RestoreApplyDryRunOperation {
347 pub sequence: usize,
348 pub operation: RestoreApplyOperationKind,
349 pub member_order: usize,
350 pub source_canister: String,
351 pub target_canister: String,
352 pub role: String,
353 pub snapshot_id: Option<String>,
354 pub artifact_path: Option<String>,
355 pub verification_kind: Option<String>,
356}
357
358#[derive(Debug, ThisError)]
363pub enum RestoreApplyDryRunError {
364 #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
365 ArtifactPathEscapesBackup {
366 source_canister: String,
367 artifact_path: String,
368 },
369
370 #[error(
371 "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
372 )]
373 ArtifactMissing {
374 source_canister: String,
375 artifact_path: String,
376 resolved_path: String,
377 },
378
379 #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
380 ArtifactChecksum {
381 source_canister: String,
382 artifact_path: String,
383 #[source]
384 source: ArtifactChecksumError,
385 },
386}