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)]
29#[serde(deny_unknown_fields)]
30pub struct RestoreApplyDryRun {
31 pub dry_run_version: u16,
32 pub backup_id: String,
33 pub ready: bool,
34 pub readiness_reasons: Vec<String>,
35 pub member_count: usize,
36 #[serde(default)]
37 pub planned_canister_stops: usize,
38 #[serde(default)]
39 pub planned_canister_starts: usize,
40 pub planned_snapshot_uploads: usize,
41 pub planned_snapshot_loads: usize,
42 pub planned_verification_checks: usize,
43 pub planned_operations: usize,
44 pub rendered_operations: usize,
45 #[serde(default)]
46 pub operation_counts: RestoreApplyOperationKindCounts,
47 pub artifact_validation: Option<RestoreApplyArtifactValidation>,
48 pub operations: Vec<RestoreApplyDryRunOperation>,
49}
50
51impl RestoreApplyDryRun {
52 #[must_use]
54 pub fn from_plan(plan: &RestorePlan) -> Self {
55 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 mut checks = Vec::new();
133
134 for member in plan.ordered_members() {
135 checks.push(validate_restore_apply_artifact(member, backup_root)?);
136 }
137
138 let members_with_expected_checksums = checks
139 .iter()
140 .filter(|check| check.checksum_expected.is_some())
141 .count();
142 let artifacts_present = checks.iter().all(|check| check.exists);
143 let checksums_verified = members_with_expected_checksums == plan.member_count
144 && checks.iter().all(|check| check.checksum_verified);
145
146 Ok(RestoreApplyArtifactValidation {
147 backup_root: backup_root.to_string_lossy().to_string(),
148 checked_members: checks.len(),
149 artifacts_present,
150 checksums_verified,
151 members_with_expected_checksums,
152 checks,
153 })
154}
155
156fn validate_restore_apply_artifact(
158 member: &RestorePlanMember,
159 backup_root: &Path,
160) -> Result<RestoreApplyArtifactCheck, RestoreApplyDryRunError> {
161 let resolved_path = safe_restore_artifact_path(
162 backup_root,
163 &member.source_canister,
164 &member.source_snapshot.artifact_path,
165 )?;
166
167 if !resolved_path.exists() {
168 return Err(RestoreApplyDryRunError::ArtifactMissing {
169 source_canister: member.source_canister.clone(),
170 artifact_path: member.source_snapshot.artifact_path.clone(),
171 resolved_path: resolved_path.to_string_lossy().to_string(),
172 });
173 }
174
175 let (checksum_actual, checksum_verified) =
176 if let Some(expected) = &member.source_snapshot.checksum {
177 let checksum = ArtifactChecksum::from_path(&resolved_path).map_err(|source| {
178 RestoreApplyDryRunError::ArtifactChecksum {
179 source_canister: member.source_canister.clone(),
180 artifact_path: member.source_snapshot.artifact_path.clone(),
181 source,
182 }
183 })?;
184 checksum.verify(expected).map_err(|source| {
185 RestoreApplyDryRunError::ArtifactChecksum {
186 source_canister: member.source_canister.clone(),
187 artifact_path: member.source_snapshot.artifact_path.clone(),
188 source,
189 }
190 })?;
191 (Some(checksum.hash), true)
192 } else {
193 (None, false)
194 };
195
196 Ok(RestoreApplyArtifactCheck {
197 source_canister: member.source_canister.clone(),
198 target_canister: member.target_canister.clone(),
199 snapshot_id: member.source_snapshot.snapshot_id.clone(),
200 artifact_path: member.source_snapshot.artifact_path.clone(),
201 resolved_path: resolved_path.to_string_lossy().to_string(),
202 exists: true,
203 checksum_algorithm: member.source_snapshot.checksum_algorithm.clone(),
204 checksum_expected: member.source_snapshot.checksum.clone(),
205 checksum_actual,
206 checksum_verified,
207 })
208}
209
210fn safe_restore_artifact_path(
212 backup_root: &Path,
213 source_canister: &str,
214 artifact_path: &str,
215) -> Result<PathBuf, RestoreApplyDryRunError> {
216 if let Some(path) = resolve_backup_artifact_path(backup_root, artifact_path) {
217 return Ok(path);
218 }
219
220 Err(RestoreApplyDryRunError::ArtifactPathEscapesBackup {
221 source_canister: source_canister.to_string(),
222 artifact_path: artifact_path.to_string(),
223 })
224}
225
226#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
231#[serde(deny_unknown_fields)]
232pub struct RestoreApplyArtifactValidation {
233 pub backup_root: String,
234 pub checked_members: usize,
235 pub artifacts_present: bool,
236 pub checksums_verified: bool,
237 pub members_with_expected_checksums: usize,
238 pub checks: Vec<RestoreApplyArtifactCheck>,
239}
240
241#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
246#[serde(deny_unknown_fields)]
247pub struct RestoreApplyArtifactCheck {
248 pub source_canister: String,
249 pub target_canister: String,
250 pub snapshot_id: String,
251 pub artifact_path: String,
252 pub resolved_path: String,
253 pub exists: bool,
254 pub checksum_algorithm: String,
255 pub checksum_expected: Option<String>,
256 pub checksum_actual: Option<String>,
257 pub checksum_verified: bool,
258}
259
260fn append_member_phase<'a>(
262 operations: &mut Vec<RestoreApplyDryRunOperation>,
263 next_sequence: &mut usize,
264 operation: RestoreApplyOperationKind,
265 members: impl IntoIterator<Item = &'a RestorePlanMember>,
266) {
267 for member in members {
268 push_member_operation(operations, next_sequence, operation.clone(), member, None);
269 }
270}
271
272fn append_member_verification_operations(
274 operations: &mut Vec<RestoreApplyDryRunOperation>,
275 next_sequence: &mut usize,
276 members: &[&RestorePlanMember],
277) {
278 for member in members {
279 for check in &member.verification_checks {
280 push_member_operation(
281 operations,
282 next_sequence,
283 RestoreApplyOperationKind::VerifyMember,
284 member,
285 Some(check),
286 );
287 }
288 }
289}
290
291fn push_member_operation(
293 operations: &mut Vec<RestoreApplyDryRunOperation>,
294 next_sequence: &mut usize,
295 operation: RestoreApplyOperationKind,
296 member: &RestorePlanMember,
297 check: Option<&VerificationCheck>,
298) {
299 let sequence = *next_sequence;
300 *next_sequence += 1;
301
302 let snapshot_id = operation_snapshot_id(&operation, member);
303 let artifact_path = operation_artifact_path(&operation, member);
304
305 operations.push(RestoreApplyDryRunOperation {
306 sequence,
307 operation,
308 member_order: member.member_order,
309 source_canister: member.source_canister.clone(),
310 target_canister: member.target_canister.clone(),
311 role: member.role.clone(),
312 snapshot_id,
313 artifact_path,
314 verification_kind: check.map(|check| check.kind.clone()),
315 });
316}
317
318fn operation_snapshot_id(
319 operation: &RestoreApplyOperationKind,
320 member: &RestorePlanMember,
321) -> Option<String> {
322 matches!(
323 operation,
324 RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
325 )
326 .then(|| member.source_snapshot.snapshot_id.clone())
327}
328
329fn operation_artifact_path(
330 operation: &RestoreApplyOperationKind,
331 member: &RestorePlanMember,
332) -> Option<String> {
333 matches!(
334 operation,
335 RestoreApplyOperationKind::UploadSnapshot | RestoreApplyOperationKind::LoadSnapshot
336 )
337 .then(|| member.source_snapshot.artifact_path.clone())
338}
339
340fn append_deployment_verification_operations(
342 plan: &RestorePlan,
343 operations: &mut Vec<RestoreApplyDryRunOperation>,
344 next_sequence: &mut usize,
345) {
346 if plan.deployment_verification_checks.is_empty() {
347 return;
348 }
349
350 let root = plan
351 .members
352 .iter()
353 .find(|member| member.source_canister == plan.source_root_canister);
354 let source_canister = root.map_or_else(
355 || plan.source_root_canister.clone(),
356 |member| member.source_canister.clone(),
357 );
358 let target_canister = root.map_or_else(
359 || plan.source_root_canister.clone(),
360 |member| member.target_canister.clone(),
361 );
362 for check in &plan.deployment_verification_checks {
363 push_deployment_operation(
364 operations,
365 next_sequence,
366 &source_canister,
367 &target_canister,
368 check,
369 );
370 }
371}
372
373fn push_deployment_operation(
375 operations: &mut Vec<RestoreApplyDryRunOperation>,
376 next_sequence: &mut usize,
377 source_canister: &str,
378 target_canister: &str,
379 check: &VerificationCheck,
380) {
381 let sequence = *next_sequence;
382 *next_sequence += 1;
383 let member_order = operations.len();
384
385 operations.push(RestoreApplyDryRunOperation {
386 sequence,
387 operation: RestoreApplyOperationKind::VerifyDeployment,
388 member_order,
389 source_canister: source_canister.to_string(),
390 target_canister: target_canister.to_string(),
391 role: "deployment".to_string(),
392 snapshot_id: None,
393 artifact_path: None,
394 verification_kind: Some(check.kind.clone()),
395 });
396}
397
398#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(deny_unknown_fields)]
404pub struct RestoreApplyDryRunOperation {
405 pub sequence: usize,
406 pub operation: RestoreApplyOperationKind,
407 pub member_order: usize,
408 pub source_canister: String,
409 pub target_canister: String,
410 pub role: String,
411 pub snapshot_id: Option<String>,
412 pub artifact_path: Option<String>,
413 pub verification_kind: Option<String>,
414}
415
416#[derive(Debug, ThisError)]
421pub enum RestoreApplyDryRunError {
422 #[error("restore artifact path for {source_canister} escapes backup root: {artifact_path}")]
423 ArtifactPathEscapesBackup {
424 source_canister: String,
425 artifact_path: String,
426 },
427
428 #[error(
429 "restore artifact for {source_canister} is missing: {artifact_path} at {resolved_path}"
430 )]
431 ArtifactMissing {
432 source_canister: String,
433 artifact_path: String,
434 resolved_path: String,
435 },
436
437 #[error("restore artifact checksum failed for {source_canister} at {artifact_path}: {source}")]
438 ArtifactChecksum {
439 source_canister: String,
440 artifact_path: String,
441 #[source]
442 source: ArtifactChecksumError,
443 },
444}