ic_backup/model/restore_safety/requirement/
mod.rs1use crate::model::{
4 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
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: canonical_hash(&fields.plan_intent)?,
85 source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
86 source_artifacts: fields.source_artifacts,
87 safety: fields.safety,
88 expected_fence: fields.expected_fence,
89 })
90 }
91}
92fn validate_lane(
93 safety: RestoreSafetyLaneRecord,
94 fence: Option<&RestoreFenceBindingRecord>,
95) -> Result<(), RestoreSafetyRequirementError> {
96 match (safety, fence) {
97 (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
98 Err(RestoreSafetyRequirementError::FenceRequired)
99 }
100 (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
101 Err(RestoreSafetyRequirementError::UnexpectedFence)
102 }
103 _ => Ok(()),
104 }
105}
106fn validate_source(
107 plan: &OperationPlanRecord,
108 source: &OperationPlanRecord,
109) -> Result<(), RestoreSafetyRequirementError> {
110 if plan.context().network() != source.context().network() {
111 return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
112 }
113 if plan.context().release() != source.context().release() {
114 return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
115 }
116 if plan
117 .selected_targets()
118 .iter()
119 .any(|target| source.selected_targets().binary_search(target).is_err())
120 {
121 return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
122 }
123 Ok(())
124}
125impl RestoreSafetyRequirementRecord {
126 pub fn new(
134 plan: &OperationPlanRecord,
135 source: &OperationPlanRecord,
136 input: RestoreSafetyRequirementRequest,
137 ) -> Result<Self, RestoreSafetyRequirementError> {
138 validate_source(plan, source)?;
139 validate_lane(input.safety, input.expected_fence.as_ref())?;
140 Ok(Self {
141 version: 1,
142 plan_intent: plan.digest().hash().into(),
143 source_plan_intent: source.digest().hash().into(),
144 source_artifacts: input.source_artifacts,
145 safety: input.safety,
146 expected_fence: input.expected_fence,
147 })
148 }
149 #[must_use]
151 pub fn plan_intent(&self) -> &str {
152 &self.plan_intent
153 }
154 #[must_use]
156 pub fn source_plan_intent(&self) -> &str {
157 &self.source_plan_intent
158 }
159 #[must_use]
161 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
162 &self.source_artifacts
163 }
164 #[must_use]
166 pub const fn safety(&self) -> RestoreSafetyLaneRecord {
167 self.safety
168 }
169 #[must_use]
171 pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
172 self.expected_fence.as_ref()
173 }
174 pub fn validate_plans(
178 &self,
179 plan: &OperationPlanRecord,
180 source: &OperationPlanRecord,
181 ) -> Result<(), RestoreSafetyRequirementError> {
182 if self.plan_intent != plan.digest().hash() {
183 return Err(RestoreSafetyRequirementError::PlanMismatch);
184 }
185 if self.source_plan_intent != source.digest().hash() {
186 return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
187 }
188 validate_source(plan, source)
189 }
190 #[must_use]
196 pub fn digest(&self) -> ArtifactChecksumRecord {
197 let mut bytes = b"ic-backup/restore-safety-requirement/v1\0".to_vec();
198 bytes.extend_from_slice(self.plan_intent.as_bytes());
199 bytes.extend_from_slice(self.source_plan_intent.as_bytes());
200 bytes.extend_from_slice(self.source_artifacts.hash().as_bytes());
201 bytes.push(match self.safety {
202 RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
203 RestoreSafetyLaneRecord::ApplicationFenced => 1,
204 });
205 if let Some(fence) = &self.expected_fence {
206 bytes.extend_from_slice(fence.identity.hash().as_bytes());
207 bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
208 bytes.extend_from_slice(fence.external_obligations_revision.hash().as_bytes());
209 }
210 ArtifactChecksumRecord::from_bytes(&bytes)
211 }
212}
213#[derive(Debug, Error)]
215pub enum RestoreSafetyRequirementError {
216 #[error("unsupported restore safety requirement version {0}")]
218 UnsupportedVersion(u16),
219 #[error("restore safety original plan mismatch")]
221 PlanMismatch,
222 #[error("restore safety original source plan mismatch")]
224 SourcePlanMismatch,
225 #[error("restore source network mismatch")]
227 SourceNetworkMismatch,
228 #[error("restore source release mismatch")]
230 SourceReleaseMismatch,
231 #[error("restore source selection mismatch")]
233 SourceSelectionMismatch,
234 #[error("restore safety requires original fence binding")]
236 FenceRequired,
237 #[error("unexpected restore safety fence binding")]
239 UnexpectedFence,
240 #[error(transparent)]
242 Checksum(#[from] ChecksumError),
243}