use crate::model::{
artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
operation_plan::OperationPlanRecord,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub const MAX_CONSISTENCY_REQUIREMENT_BYTES: u64 = 1024;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConsistencyGuaranteeRecord {
PerCanister,
ApplicationCoordinated,
}
impl ConsistencyGuaranteeRecord {
pub(super) const fn tag(self) -> u8 {
match self {
Self::PerCanister => 0,
Self::ApplicationCoordinated => 1,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "RequirementFields")]
pub struct ConsistencyRequirementRecord {
version: u16,
plan_intent: String,
guarantee: ConsistencyGuaranteeRecord,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RequirementFields {
version: u16,
plan_intent: String,
guarantee: ConsistencyGuaranteeRecord,
}
impl TryFrom<RequirementFields> for ConsistencyRequirementRecord {
type Error = ConsistencyRequirementError;
fn try_from(fields: RequirementFields) -> Result<Self, Self::Error> {
if fields.version != 1 {
return Err(ConsistencyRequirementError::UnsupportedVersion(
fields.version,
));
}
Ok(Self {
version: 1,
plan_intent: canonical_hash(&fields.plan_intent)?,
guarantee: fields.guarantee,
})
}
}
impl ConsistencyRequirementRecord {
#[must_use]
pub fn new(plan: &OperationPlanRecord, guarantee: ConsistencyGuaranteeRecord) -> Self {
Self {
version: 1,
plan_intent: plan.digest().hash().into(),
guarantee,
}
}
#[must_use]
pub fn plan_intent(&self) -> &str {
&self.plan_intent
}
#[must_use]
pub const fn guarantee(&self) -> ConsistencyGuaranteeRecord {
self.guarantee
}
pub fn validate_plan(
&self,
plan: &OperationPlanRecord,
) -> Result<(), ConsistencyRequirementError> {
if self.plan_intent != plan.digest().hash() {
return Err(ConsistencyRequirementError::PlanMismatch);
}
Ok(())
}
#[must_use]
pub fn digest(&self) -> ArtifactChecksumRecord {
let mut bytes = b"ic-backup/consistency-requirement/v1\0".to_vec();
bytes.extend_from_slice(self.plan_intent.as_bytes());
bytes.push(self.guarantee.tag());
ArtifactChecksumRecord::from_bytes(&bytes)
}
}
#[derive(Debug, Error)]
pub enum ConsistencyRequirementError {
#[error("unsupported consistency requirement version {0}")]
UnsupportedVersion(u16),
#[error("consistency requirement original plan mismatch")]
PlanMismatch,
#[error(transparent)]
Checksum(#[from] ChecksumError),
}