use crate::model::{
artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
operation_plan::OperationPlanRecord,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const MAX_RESTORE_SAFETY_REQUIREMENT_BYTES: u64 = 1024;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RestoreSafetyLaneRecord {
NoIrreversibleEffects,
ApplicationFenced,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RestoreFenceBindingRecord {
pub identity: ArtifactChecksumRecord,
pub membership_revision: ArtifactChecksumRecord,
pub external_obligations_revision: ArtifactChecksumRecord,
}
#[derive(Clone, Debug)]
pub struct RestoreSafetyRequirementRequest {
pub source_artifacts: ArtifactChecksumRecord,
pub safety: RestoreSafetyLaneRecord,
pub expected_fence: Option<RestoreFenceBindingRecord>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "RequirementFields")]
pub struct RestoreSafetyRequirementRecord {
version: u16,
plan_intent: String,
source_plan_intent: String,
source_artifacts: ArtifactChecksumRecord,
safety: RestoreSafetyLaneRecord,
expected_fence: Option<RestoreFenceBindingRecord>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RequirementFields {
version: u16,
plan_intent: String,
source_plan_intent: String,
source_artifacts: ArtifactChecksumRecord,
safety: RestoreSafetyLaneRecord,
#[serde(deserialize_with = "required_fence")]
expected_fence: Option<RestoreFenceBindingRecord>,
}
fn required_fence<'de, D: serde::Deserializer<'de>>(
decoder: D,
) -> Result<Option<RestoreFenceBindingRecord>, D::Error> {
Option::deserialize(decoder)
}
impl TryFrom<RequirementFields> for RestoreSafetyRequirementRecord {
type Error = RestoreSafetyRequirementError;
fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
if fields.version != 1 {
return Err(RestoreSafetyRequirementError::UnsupportedVersion(
fields.version,
));
}
validate_lane(fields.safety, fields.expected_fence.as_ref())?;
Ok(Self {
version: 1,
plan_intent: canonical_hash(&fields.plan_intent)?,
source_plan_intent: canonical_hash(&fields.source_plan_intent)?,
source_artifacts: fields.source_artifacts,
safety: fields.safety,
expected_fence: fields.expected_fence,
})
}
}
fn validate_lane(
safety: RestoreSafetyLaneRecord,
fence: Option<&RestoreFenceBindingRecord>,
) -> Result<(), RestoreSafetyRequirementError> {
match (safety, fence) {
(RestoreSafetyLaneRecord::ApplicationFenced, None) => {
Err(RestoreSafetyRequirementError::FenceRequired)
}
(RestoreSafetyLaneRecord::NoIrreversibleEffects, Some(_)) => {
Err(RestoreSafetyRequirementError::UnexpectedFence)
}
_ => Ok(()),
}
}
fn validate_source(
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
) -> Result<(), RestoreSafetyRequirementError> {
if plan.context().network() != source.context().network() {
return Err(RestoreSafetyRequirementError::SourceNetworkMismatch);
}
if plan.context().release() != source.context().release() {
return Err(RestoreSafetyRequirementError::SourceReleaseMismatch);
}
if plan
.selected_targets()
.iter()
.any(|target| source.selected_targets().binary_search(target).is_err())
{
return Err(RestoreSafetyRequirementError::SourceSelectionMismatch);
}
Ok(())
}
impl RestoreSafetyRequirementRecord {
pub fn new(
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
input: RestoreSafetyRequirementRequest,
) -> Result<Self, RestoreSafetyRequirementError> {
validate_source(plan, source)?;
validate_lane(input.safety, input.expected_fence.as_ref())?;
Ok(Self {
version: 1,
plan_intent: plan.digest().hash().into(),
source_plan_intent: source.digest().hash().into(),
source_artifacts: input.source_artifacts,
safety: input.safety,
expected_fence: input.expected_fence,
})
}
#[must_use]
pub fn plan_intent(&self) -> &str {
&self.plan_intent
}
#[must_use]
pub fn source_plan_intent(&self) -> &str {
&self.source_plan_intent
}
#[must_use]
pub const fn source_artifacts(&self) -> &ArtifactChecksumRecord {
&self.source_artifacts
}
#[must_use]
pub const fn safety(&self) -> RestoreSafetyLaneRecord {
self.safety
}
#[must_use]
pub const fn expected_fence(&self) -> Option<&RestoreFenceBindingRecord> {
self.expected_fence.as_ref()
}
pub fn validate_plans(
&self,
plan: &OperationPlanRecord,
source: &OperationPlanRecord,
) -> Result<(), RestoreSafetyRequirementError> {
if self.plan_intent != plan.digest().hash() {
return Err(RestoreSafetyRequirementError::PlanMismatch);
}
if self.source_plan_intent != source.digest().hash() {
return Err(RestoreSafetyRequirementError::SourcePlanMismatch);
}
validate_source(plan, source)
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/restore-safety-requirement/v1\0".to_vec();
bytes.extend_from_slice(self.plan_intent.as_bytes());
bytes.extend_from_slice(self.source_plan_intent.as_bytes());
bytes.extend_from_slice(self.source_artifacts.hash().as_bytes());
bytes.push(match self.safety {
RestoreSafetyLaneRecord::NoIrreversibleEffects => 0,
RestoreSafetyLaneRecord::ApplicationFenced => 1,
});
if let Some(fence) = &self.expected_fence {
bytes.extend_from_slice(fence.identity.hash().as_bytes());
bytes.extend_from_slice(fence.membership_revision.hash().as_bytes());
bytes.extend_from_slice(fence.external_obligations_revision.hash().as_bytes());
}
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[derive(Debug, Error)]
pub enum RestoreSafetyRequirementError {
#[error("unsupported restore safety requirement version {0}")]
UnsupportedVersion(u16),
#[error("restore safety original plan mismatch")]
PlanMismatch,
#[error("restore safety original source plan mismatch")]
SourcePlanMismatch,
#[error("restore source network mismatch")]
SourceNetworkMismatch,
#[error("restore source release mismatch")]
SourceReleaseMismatch,
#[error("restore source selection mismatch")]
SourceSelectionMismatch,
#[error("restore safety requires original fence binding")]
FenceRequired,
#[error("unexpected restore safety fence binding")]
UnexpectedFence,
#[error(transparent)]
Checksum(#[from] ChecksumError),
}