canic_backup/execution/
receipt.rs1use super::{
2 BackupExecutionJournal, BackupExecutionJournalError, BackupExecutionJournalOperation,
3 BackupExecutionOperationReceipt, BackupExecutionOperationReceiptOutcome,
4 validation::{operation_kind_is_mutating, validate_nonempty, validate_optional_nonempty},
5};
6use crate::plan::BackupOperationKind;
7
8impl BackupExecutionOperationReceipt {
9 #[must_use]
11 pub fn completed(
12 journal: &BackupExecutionJournal,
13 operation: &BackupExecutionJournalOperation,
14 updated_at: Option<String>,
15 ) -> Self {
16 Self::from_operation(
17 journal,
18 operation,
19 BackupExecutionOperationReceiptOutcome::Completed,
20 updated_at,
21 None,
22 )
23 }
24
25 #[must_use]
27 pub fn failed(
28 journal: &BackupExecutionJournal,
29 operation: &BackupExecutionJournalOperation,
30 updated_at: Option<String>,
31 failure_reason: String,
32 ) -> Self {
33 Self::from_operation(
34 journal,
35 operation,
36 BackupExecutionOperationReceiptOutcome::Failed,
37 updated_at,
38 Some(failure_reason),
39 )
40 }
41
42 fn from_operation(
43 journal: &BackupExecutionJournal,
44 operation: &BackupExecutionJournalOperation,
45 outcome: BackupExecutionOperationReceiptOutcome,
46 updated_at: Option<String>,
47 failure_reason: Option<String>,
48 ) -> Self {
49 Self {
50 plan_id: journal.plan_id.clone(),
51 run_id: journal.run_id.clone(),
52 preflight_id: journal.preflight_id.clone(),
53 sequence: operation.sequence,
54 operation_id: operation.operation_id.clone(),
55 kind: operation.kind.clone(),
56 target_canister_id: operation.target_canister_id.clone(),
57 outcome,
58 updated_at,
59 snapshot_id: None,
60 artifact_path: None,
61 checksum: None,
62 failure_reason,
63 }
64 }
65
66 pub(super) fn validate_against(
67 &self,
68 journal: &BackupExecutionJournal,
69 ) -> Result<(), BackupExecutionJournalError> {
70 validate_nonempty("operation_receipts[].plan_id", &self.plan_id)?;
71 validate_nonempty("operation_receipts[].run_id", &self.run_id)?;
72 validate_nonempty("operation_receipts[].operation_id", &self.operation_id)?;
73 validate_optional_nonempty(
74 "operation_receipts[].updated_at",
75 self.updated_at.as_deref(),
76 )?;
77 validate_optional_nonempty(
78 "operation_receipts[].snapshot_id",
79 self.snapshot_id.as_deref(),
80 )?;
81 validate_optional_nonempty(
82 "operation_receipts[].artifact_path",
83 self.artifact_path.as_deref(),
84 )?;
85 validate_optional_nonempty("operation_receipts[].checksum", self.checksum.as_deref())?;
86
87 if self.plan_id != journal.plan_id || self.run_id != journal.run_id {
88 return Err(BackupExecutionJournalError::ReceiptJournalMismatch {
89 sequence: self.sequence,
90 });
91 }
92 let operation = journal
93 .operations
94 .iter()
95 .find(|operation| operation.sequence == self.sequence)
96 .ok_or(BackupExecutionJournalError::ReceiptOperationNotFound(
97 self.sequence,
98 ))?;
99 if operation.operation_id != self.operation_id
100 || operation.kind != self.kind
101 || operation.target_canister_id != self.target_canister_id
102 {
103 return Err(BackupExecutionJournalError::ReceiptOperationMismatch {
104 sequence: self.sequence,
105 });
106 }
107 if operation_kind_is_mutating(&operation.kind) && self.preflight_id != journal.preflight_id
108 {
109 return Err(BackupExecutionJournalError::ReceiptPreflightMismatch {
110 sequence: self.sequence,
111 });
112 }
113 if self.outcome == BackupExecutionOperationReceiptOutcome::Failed {
114 validate_nonempty(
115 "operation_receipts[].failure_reason",
116 self.failure_reason.as_deref().unwrap_or_default(),
117 )?;
118 }
119 if self.kind == BackupOperationKind::CreateSnapshot
120 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
121 {
122 validate_nonempty(
123 "operation_receipts[].snapshot_id",
124 self.snapshot_id.as_deref().unwrap_or_default(),
125 )?;
126 }
127 if self.kind == BackupOperationKind::DownloadSnapshot
128 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
129 {
130 validate_nonempty(
131 "operation_receipts[].artifact_path",
132 self.artifact_path.as_deref().unwrap_or_default(),
133 )?;
134 }
135 if self.kind == BackupOperationKind::VerifyArtifact
136 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
137 {
138 validate_nonempty(
139 "operation_receipts[].checksum",
140 self.checksum.as_deref().unwrap_or_default(),
141 )?;
142 }
143
144 Ok(())
145 }
146}