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 snapshot_taken_at_timestamp: None,
61 snapshot_total_size_bytes: None,
62 artifact_path: None,
63 checksum: None,
64 failure_reason,
65 }
66 }
67
68 pub(super) fn validate_against(
69 &self,
70 journal: &BackupExecutionJournal,
71 ) -> Result<(), BackupExecutionJournalError> {
72 validate_nonempty("operation_receipts[].plan_id", &self.plan_id)?;
73 validate_nonempty("operation_receipts[].run_id", &self.run_id)?;
74 validate_nonempty("operation_receipts[].operation_id", &self.operation_id)?;
75 validate_nonempty(
76 "operation_receipts[].updated_at",
77 self.updated_at.as_deref().unwrap_or_default(),
78 )?;
79 validate_optional_nonempty(
80 "operation_receipts[].snapshot_id",
81 self.snapshot_id.as_deref(),
82 )?;
83 validate_optional_nonempty(
84 "operation_receipts[].artifact_path",
85 self.artifact_path.as_deref(),
86 )?;
87 validate_optional_nonempty("operation_receipts[].checksum", self.checksum.as_deref())?;
88
89 if self.plan_id != journal.plan_id || self.run_id != journal.run_id {
90 return Err(BackupExecutionJournalError::ReceiptJournalMismatch {
91 sequence: self.sequence,
92 });
93 }
94 let operation = journal
95 .operations
96 .iter()
97 .find(|operation| operation.sequence == self.sequence)
98 .ok_or(BackupExecutionJournalError::ReceiptOperationNotFound(
99 self.sequence,
100 ))?;
101 if operation.operation_id != self.operation_id
102 || operation.kind != self.kind
103 || operation.target_canister_id != self.target_canister_id
104 {
105 return Err(BackupExecutionJournalError::ReceiptOperationMismatch {
106 sequence: self.sequence,
107 });
108 }
109 if operation_kind_is_mutating(&operation.kind) && self.preflight_id != journal.preflight_id
110 {
111 return Err(BackupExecutionJournalError::ReceiptPreflightMismatch {
112 sequence: self.sequence,
113 });
114 }
115 if self.outcome == BackupExecutionOperationReceiptOutcome::Failed {
116 validate_nonempty(
117 "operation_receipts[].failure_reason",
118 self.failure_reason.as_deref().unwrap_or_default(),
119 )?;
120 }
121 if self.kind == BackupOperationKind::CreateSnapshot
122 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
123 {
124 validate_nonempty(
125 "operation_receipts[].snapshot_id",
126 self.snapshot_id.as_deref().unwrap_or_default(),
127 )?;
128 }
129 if self.kind == BackupOperationKind::DownloadSnapshot
130 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
131 {
132 validate_nonempty(
133 "operation_receipts[].artifact_path",
134 self.artifact_path.as_deref().unwrap_or_default(),
135 )?;
136 }
137 if self.kind == BackupOperationKind::VerifyArtifact
138 && self.outcome == BackupExecutionOperationReceiptOutcome::Completed
139 {
140 validate_nonempty(
141 "operation_receipts[].checksum",
142 self.checksum.as_deref().unwrap_or_default(),
143 )?;
144 }
145
146 Ok(())
147 }
148}