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