canic_backup/plan/preflight/
receipts.rs1use crate::plan::{
8 BackupExecutionPreflightReceipts, BackupPlan, BackupPlanError, QuiescencePreflightReceipt,
9 TopologyPreflightReceipt,
10 validation::{
11 validate_nonempty, validate_optional_nonempty, validate_preflight_id,
12 validate_preflight_timestamp, validate_preflight_window, validate_required_hash,
13 },
14};
15
16impl BackupPlan {
17 pub fn validate_execution_preflight_receipts(
19 &self,
20 topology_receipt: &TopologyPreflightReceipt,
21 quiescence_receipt: &QuiescencePreflightReceipt,
22 preflight_id: &str,
23 as_of: &str,
24 ) -> Result<(), BackupPlanError> {
25 self.validate_for_execution()?;
26 validate_preflight_id(preflight_id)?;
27 validate_preflight_timestamp("preflight.as_of", as_of)?;
28 validate_topology_preflight_receipt(self, topology_receipt, preflight_id, as_of)?;
29 validate_quiescence_preflight_receipt(self, quiescence_receipt, preflight_id, as_of)
30 }
31
32 pub fn apply_execution_preflight_receipts(
34 &mut self,
35 receipts: &BackupExecutionPreflightReceipts,
36 as_of: &str,
37 ) -> Result<(), BackupPlanError> {
38 validate_execution_preflight_bundle(self, receipts, as_of)?;
39 self.apply_authority_preflight_receipts(
40 &receipts.preflight_id,
41 &receipts.control_authority,
42 &receipts.snapshot_read_authority,
43 as_of,
44 )?;
45 self.validate_execution_preflight_receipts(
46 &receipts.topology,
47 &receipts.quiescence,
48 &receipts.preflight_id,
49 as_of,
50 )
51 }
52}
53
54fn validate_execution_preflight_bundle(
55 plan: &BackupPlan,
56 receipts: &BackupExecutionPreflightReceipts,
57 as_of: &str,
58) -> Result<(), BackupPlanError> {
59 validate_nonempty("preflight_receipts.plan_id", &receipts.plan_id)?;
60 validate_preflight_id(&receipts.preflight_id)?;
61 validate_preflight_timestamp("preflight_receipts.as_of", as_of)?;
62 validate_preflight_window(
63 &receipts.preflight_id,
64 &receipts.validated_at,
65 &receipts.expires_at,
66 as_of,
67 )?;
68
69 if receipts.plan_id != plan.plan_id {
70 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
71 expected: plan.plan_id.clone(),
72 actual: receipts.plan_id.clone(),
73 });
74 }
75
76 Ok(())
77}
78
79fn validate_topology_preflight_receipt(
80 plan: &BackupPlan,
81 receipt: &TopologyPreflightReceipt,
82 preflight_id: &str,
83 as_of: &str,
84) -> Result<(), BackupPlanError> {
85 validate_nonempty("topology_receipt.plan_id", &receipt.plan_id)?;
86 validate_preflight_id(&receipt.preflight_id)?;
87 validate_required_hash(
88 "topology_receipt.topology_hash_before_quiesce",
89 &receipt.topology_hash_before_quiesce,
90 )?;
91 validate_required_hash(
92 "topology_receipt.topology_hash_at_preflight",
93 &receipt.topology_hash_at_preflight,
94 )?;
95 validate_optional_nonempty("topology_receipt.message", receipt.message.as_deref())?;
96 validate_preflight_window(
97 &receipt.preflight_id,
98 &receipt.validated_at,
99 &receipt.expires_at,
100 as_of,
101 )?;
102
103 if receipt.plan_id != plan.plan_id {
104 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
105 expected: plan.plan_id.clone(),
106 actual: receipt.plan_id.clone(),
107 });
108 }
109 if receipt.preflight_id != preflight_id {
110 return Err(BackupPlanError::PreflightReceiptIdMismatch {
111 expected: preflight_id.to_string(),
112 actual: receipt.preflight_id.clone(),
113 });
114 }
115 if receipt.topology_hash_before_quiesce != plan.topology_hash_before_quiesce {
116 return Err(BackupPlanError::TopologyPreflightHashMismatch {
117 expected: plan.topology_hash_before_quiesce.clone(),
118 actual: receipt.topology_hash_before_quiesce.clone(),
119 });
120 }
121 if receipt.topology_hash_at_preflight != plan.topology_hash_before_quiesce {
122 return Err(BackupPlanError::TopologyPreflightHashMismatch {
123 expected: plan.topology_hash_before_quiesce.clone(),
124 actual: receipt.topology_hash_at_preflight.clone(),
125 });
126 }
127 if receipt.targets != plan.topology_preflight_request().targets {
128 return Err(BackupPlanError::TopologyPreflightTargetsMismatch);
129 }
130
131 Ok(())
132}
133
134fn validate_quiescence_preflight_receipt(
135 plan: &BackupPlan,
136 receipt: &QuiescencePreflightReceipt,
137 preflight_id: &str,
138 as_of: &str,
139) -> Result<(), BackupPlanError> {
140 validate_nonempty("quiescence_receipt.plan_id", &receipt.plan_id)?;
141 validate_preflight_id(&receipt.preflight_id)?;
142 validate_optional_nonempty("quiescence_receipt.message", receipt.message.as_deref())?;
143 validate_preflight_window(
144 &receipt.preflight_id,
145 &receipt.validated_at,
146 &receipt.expires_at,
147 as_of,
148 )?;
149
150 if receipt.plan_id != plan.plan_id {
151 return Err(BackupPlanError::PreflightReceiptPlanMismatch {
152 expected: plan.plan_id.clone(),
153 actual: receipt.plan_id.clone(),
154 });
155 }
156 if receipt.preflight_id != preflight_id {
157 return Err(BackupPlanError::PreflightReceiptIdMismatch {
158 expected: preflight_id.to_string(),
159 actual: receipt.preflight_id.clone(),
160 });
161 }
162 if receipt.quiescence_policy != plan.quiescence_policy {
163 return Err(BackupPlanError::QuiescencePolicyMismatch);
164 }
165 if !receipt.accepted {
166 return Err(BackupPlanError::QuiescencePreflightRejected);
167 }
168 if receipt.targets != plan.quiescence_preflight_request().targets {
169 return Err(BackupPlanError::QuiescencePreflightTargetsMismatch);
170 }
171
172 Ok(())
173}