ic_backup/model/restore_safety/requirement/
mod.rs1use crate::model::evidence_digest::EvidenceDigest;
4use crate::model::{
5 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
6 operation_plan::OperationPlanRecord,
7};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11pub const MAX_RESTORE_SAFETY_REQUIREMENT_BYTES: u64 = 1024;
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
15#[serde(rename_all = "snake_case")]
16pub enum RestoreSafetyLaneRecord {
17 NoIrreversibleEffects,
19 ApplicationFenced,
21}
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct RestoreFenceBindingRecord {
26 pub identity: ArtifactChecksumRecord,
28 pub membership_revision: ArtifactChecksumRecord,
30 pub external_obligations_revision: ArtifactChecksumRecord,
32}
33#[derive(Clone, Debug)]
35pub struct RestoreSafetyRequirementRequest {
36 pub source_artifacts: ArtifactChecksumRecord,
38 pub safety: RestoreSafetyLaneRecord,
40 pub expected_fence: Option<RestoreFenceBindingRecord>,
42}
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
49#[serde(try_from = "RequirementFields")]
50pub struct RestoreSafetyRequirementRecord {
51 version: u16,
52 plan_intent: String,
53 source_plan_intent: String,
54 source_artifacts: ArtifactChecksumRecord,
55 safety: RestoreSafetyLaneRecord,
56 expected_fence: Option<RestoreFenceBindingRecord>,
57}
58#[derive(Deserialize)]
59#[serde(deny_unknown_fields)]
60struct RequirementFields {
61 version: u16,
62 plan_intent: String,
63 source_plan_intent: String,
64 source_artifacts: ArtifactChecksumRecord,
65 safety: RestoreSafetyLaneRecord,
66 #[serde(deserialize_with = "crate::model::required_option::deserialize")]
67 expected_fence: Option<RestoreFenceBindingRecord>,
68}
69
70impl TryFrom<RequirementFields> for RestoreSafetyRequirementRecord {
71 type Error = RestoreSafetyRequirementError;
72 fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
73 if fields.version != 1 {
74 return Err(RestoreSafetyRequirementError::UnsupportedVersion(
75 fields.version,
76 ));
77 }
78 validate_lane(fields.safety, fields.expected_fence.as_ref())?;
79 Ok(Self {
80 version: 1,
81 plan_intent: canonical_hash(&fields.plan_intent)?,
82 source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
83 source_artifacts: fields.source_artifacts,
84 safety: fields.safety,
85 expected_fence: fields.expected_fence,
86 })
87 }
88}
89fn validate_lane(
90 safety: RestoreSafetyLaneRecord,
91 fence: Option<&RestoreFenceBindingRecord>,
92) -> Result<(), RestoreSafetyRequirementError> {
93 match (safety, fence) {
94 (RestoreSafetyLaneRecord::ApplicationFenced, None) => {
95 Err(RestoreSafetyRequirementError::FenceRequired)
96 }
97 (RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
98 Err(RestoreSafetyRequirementError::UnexpectedFence)
99 }
100 _ => Ok(()),
101 }
102}
103fn validate_source(
104 plan: &OperationPlanRecord,
105 source: &OperationPlanRecord,
106) -> Result<(), RestoreSafetyRequirementError> {
107 if plan.context().network() != source.context().network() {
108 return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
109 }
110 if plan.context().release() != source.context().release() {
111 return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
112 }
113 if plan
114 .selected_targets()
115 .iter()
116 .any(|target| source.selected_targets().binary_search(target).is_err())
117 {
118 return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
119 }
120 Ok(())
121}
122impl RestoreSafetyRequirementRecord {
123 pub fn new(
131 plan: &OperationPlanRecord,
132 source: &OperationPlanRecord,
133 input: RestoreSafetyRequirementRequest,
134 ) -> Result<Self, RestoreSafetyRequirementError> {
135 validate_source(plan, source)?;
136 validate_lane(input.safety, input.expected_fence.as_ref())?;
137 Ok(Self {
138 version: 1,
139 plan_intent: plan.digest().hash().into(),
140 source_plan_intent: source.digest().hash().into(),
141 source_artifacts: input.source_artifacts,
142 safety: input.safety,
143 expected_fence: input.expected_fence,
144 })
145 }
146 #[must_use]
148 pub fn plan_intent(&self) -> &str {
149 &self.plan_intent
150 }
151 #[must_use]
153 pub fn source_plan_intent(&self) -> &str {
154 &self.source_plan_intent
155 }
156 #[must_use]
158 pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
159 &self.source_artifacts
160 }
161 #[must_use]
163 pub const fn safety(&self) -> RestoreSafetyLaneRecord {
164 self.safety
165 }
166 #[must_use]
168 pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
169 self.expected_fence.as_ref()
170 }
171 pub fn validate_plans(
175 &self,
176 plan: &OperationPlanRecord,
177 source: &OperationPlanRecord,
178 ) -> Result<(), RestoreSafetyRequirementError> {
179 if self.plan_intent != plan.digest().hash() {
180 return Err(RestoreSafetyRequirementError::PlanMismatch);
181 }
182 if self.source_plan_intent != source.digest().hash() {
183 return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
184 }
185 validate_source(plan, source)
186 }
187 #[must_use]
193 pub fn digest(&self) -> ArtifactChecksumRecord {
194 let mut bytes = EvidenceDigest::new(b"ic-backup/restore-safety-requirement/v1\0");
195 bytes.append(self.plan_intent.as_bytes());
196 bytes.append(self.source_plan_intent.as_bytes());
197 bytes.append(self.source_artifacts.hash().as_bytes());
198 bytes.byte(match self.safety {
199 RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
200 RestoreSafetyLaneRecord::ApplicationFenced => 1,
201 });
202 if let Some(fence) = &self.expected_fence {
203 bytes.append(fence.identity.hash().as_bytes());
204 bytes.append(fence.membership_revision.hash().as_bytes());
205 bytes.append(fence.external_obligations_revision.hash().as_bytes());
206 }
207 bytes.finish()
208 }
209}
210#[derive(Debug, Error)]
212pub enum RestoreSafetyRequirementError {
213 #[error("unsupported restore safety requirement version {0}")]
215 UnsupportedVersion(u16),
216 #[error("restore safety original plan mismatch")]
218 PlanMismatch,
219 #[error("restore safety original source plan mismatch")]
221 SourcePlanMismatch,
222 #[error("restore source network mismatch")]
224 SourceNetworkMismatch,
225 #[error("restore source release mismatch")]
227 SourceReleaseMismatch,
228 #[error("restore source selection mismatch")]
230 SourceSelectionMismatch,
231 #[error("restore safety requires original fence binding")]
233 FenceRequired,
234 #[error("unexpected restore safety fence binding")]
236 UnexpectedFence,
237 #[error(transparent)]
239 Checksum(#[from] ChecksumError),
240}