ic_backup/model/restore_safety/requirement/
mod.rs1use crate::model::{
4 artifacts::{ArtifactChecksumRecord, ChecksumError},
5 operation_plan::OperationPlanRecord,
6};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10pub const MAX_RESTORE_SAFETY_REQUIREMENT_BYTES: u64 = 1024;
12#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum RestoreSafetyLaneRecord {
16 NoIrreversibleEffects,
18 ApplicationFenced,
20}
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct RestoreFenceBindingRecord {
25 pub identity: ArtifactChecksumRecord,
27 pub membership_revision: ArtifactChecksumRecord,
29 pub external_obligations_revision: ArtifactChecksumRecord,
31}
32#[derive(Clone, Debug)]
34pub struct RestoreSafetyRequirementRequest {
35 pub source_artifacts: ArtifactChecksumRecord,
37 pub safety: RestoreSafetyLaneRecord,
39 pub expected_fence: Option<RestoreFenceBindingRecord>,
41}
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48#[serde(try_from = "RequirementFields")]
49pub struct RestoreSafetyRequirementRecord {
50 version: u16,
51 plan_intent: String,
52 source_plan_intent: String,
53 source_artifacts: ArtifactChecksumRecord,
54 safety: RestoreSafetyLaneRecord,
55 expected_fence: Option<RestoreFenceBindingRecord>,
56}
57#[derive(Deserialize)]
58#[serde(deny_unknown_fields)]
59struct RequirementFields {
60 version: u16,
61 plan_intent: String,
62 source_plan_intent: String,
63 source_artifacts: ArtifactChecksumRecord,
64 safety: RestoreSafetyLaneRecord,
65 #[serde(deserialize_with = "required_fence")]
66 expected_fence: Option<RestoreFenceBindingRecord>,
67}
68fn required_fence<'de, D: serde::Deserializer<'de>>(
69 decoder: D,
70) -> Result<Option<RestoreFenceBindingRecord>, D::Error> {
71 Option::deserialize(decoder)
72}
73impl TryFrom<RequirementFields> for RestoreSafetyRequirementRecord {
74 type Error = RestoreSafetyRequirementError;
75 fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
76 if fields.version != 1 {
77 return Err(RestoreSafetyRequirementError::UnsupportedVersion(
78 fields.version,
79 ));
80 }
81 validate_lane(fields.safety, fields.expected_fence.as_ref())?;
82 Ok(Self {
83 version: 1,
84 plan_intent: ArtifactChecksumRecord::from_hash(&fields.plan_intent)?
85 .hash()
86 .into(),
87 source_plan_intent: ArtifactChecksumRecord::from_hash(&fields.source_plan_intent)?
88 .hash()
89 .into(),
90 source_artifacts: fields.source_artifacts,
91 safety: fields.safety,
92 expected_fence: fields.expected_fence,
93 })
94 }
95}
96fn validate_lane(
97 safety: RestoreSafetyLaneRecord,
98 fence: Option<&RestoreFenceBindingRecord>,
99) -> Result<(), RestoreSafetyRequirementError> {
100 match (safety, fence) {
101 (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
102 Err(RestoreSafetyRequirementError::FenceRequired)
103 }
104 (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
105 Err(RestoreSafetyRequirementError::UnexpectedFence)
106 }
107 _ => Ok(()),
108 }
109}
110fn validate_source(
111 plan: &OperationPlanRecord,
112 source: &OperationPlanRecord,
113) -> Result<(), RestoreSafetyRequirementError> {
114 if plan.context().network() != source.context().network() {
115 return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
116 }
117 if plan.context().release() != source.context().release() {
118 return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
119 }
120 if plan
121 .selected_targets()
122 .iter()
123 .any(|target| source.selected_targets().binary_search(target).is_err())
124 {
125 return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
126 }
127 Ok(())
128}
129impl RestoreSafetyRequirementRecord {
130 pub fn new(
138 plan: &OperationPlanRecord,
139 source: &OperationPlanRecord,
140 input: RestoreSafetyRequirementRequest,
141 ) -> Result<Self, RestoreSafetyRequirementError> {
142 validate_source(plan, source)?;
143 validate_lane(input.safety, input.expected_fence.as_ref())?;
144 Ok(Self {
145 version: 1,
146 plan_intent: plan.digest().hash().into(),
147 source_plan_intent: source.digest().hash().into(),
148 source_artifacts: input.source_artifacts,
149 safety: input.safety,
150 expected_fence: input.expected_fence,
151 })
152 }
153 #[must_use]
155 pub fn plan_intent(&self) -> &str {
156 &self.plan_intent
157 }
158 #[must_use]
160 pub fn source_plan_intent(&self) -> &str {
161 &self.source_plan_intent
162 }
163 #[must_use]
165 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
166 &self.source_artifacts
167 }
168 #[must_use]
170 pub const fn safety(&self) -> RestoreSafetyLaneRecord {
171 self.safety
172 }
173 #[must_use]
175 pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
176 self.expected_fence.as_ref()
177 }
178 pub fn validate_plans(
182 &self,
183 plan: &OperationPlanRecord,
184 source: &OperationPlanRecord,
185 ) -> Result<(), RestoreSafetyRequirementError> {
186 if self.plan_intent != plan.digest().hash() {
187 return Err(RestoreSafetyRequirementError::PlanMismatch);
188 }
189 if self.source_plan_intent != source.digest().hash() {
190 return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
191 }
192 validate_source(plan, source)
193 }
194 #[must_use]
200 pub fn digest(&self) -> ArtifactChecksumRecord {
201 let mut bytes = b"ic-backup/restore-safety-requirement/v1\0".to_vec();
202 bytes.extend_from_slice(self.plan_intent.as_bytes());
203 bytes.extend_from_slice(self.source_plan_intent.as_bytes());
204 bytes.extend_from_slice(self.source_artifacts.hash().as_bytes());
205 bytes.push(match self.safety {
206 RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
207 RestoreSafetyLaneRecord::ApplicationFenced => 1,
208 });
209 if let Some(fence) = &self.expected_fence {
210 bytes.extend_from_slice(fence.identity.hash().as_bytes());
211 bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
212 bytes.extend_from_slice(fence.external_obligations_revision.hash().as_bytes());
213 }
214 ArtifactChecksumRecord::from_bytes(&bytes)
215 }
216}
217#[derive(Debug, Error)]
219pub enum RestoreSafetyRequirementError {
220 #[error("unsupported restore safety requirement version {0}")]
222 UnsupportedVersion(u16),
223 #[error("restore safety original plan mismatch")]
225 PlanMismatch,
226 #[error("restore safety original source plan mismatch")]
228 SourcePlanMismatch,
229 #[error("restore source network mismatch")]
231 SourceNetworkMismatch,
232 #[error("restore source release mismatch")]
234 SourceReleaseMismatch,
235 #[error("restore source selection mismatch")]
237 SourceSelectionMismatch,
238 #[error("restore safety requires original fence binding")]
240 FenceRequired,
241 #[error("unexpected restore safety fence binding")]
243 UnexpectedFence,
244 #[error(transparent)]
246 Checksum(#[from] ChecksumError),
247}