use crate::dns::NewDnsConfig;
use crate::ifaddr::{InterfaceAddress, NewInterfaceAddress};
use crate::route::Route;
use net_lattice_core::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Mutation {
AddRoute(Route),
RemoveRoute(Route),
AddAddress(NewInterfaceAddress),
RemoveAddress(InterfaceAddress),
SetDnsConfig(NewDnsConfig),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationKind {
AddRoute,
RemoveRoute,
AddAddress,
RemoveAddress,
SetDnsConfig,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationPrecondition {
Absent,
Present,
Any,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationIdempotency {
Strict,
Replace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationConfirmation {
NativeAcknowledgement,
ReadAfterWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationPrivilege {
Elevated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MutationReversibility {
RequiresPriorState,
NotGuaranteed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MutationSemantics {
pub kind: MutationKind,
pub precondition: MutationPrecondition,
pub idempotency: MutationIdempotency,
pub privilege: MutationPrivilege,
pub confirmation: MutationConfirmation,
pub reversibility: MutationReversibility,
pub may_partially_apply: bool,
}
impl Mutation {
pub const fn semantics(&self) -> MutationSemantics {
match self {
Self::AddRoute(_) => MutationSemantics {
kind: MutationKind::AddRoute,
precondition: MutationPrecondition::Absent,
idempotency: MutationIdempotency::Strict,
privilege: MutationPrivilege::Elevated,
confirmation: MutationConfirmation::NativeAcknowledgement,
reversibility: MutationReversibility::RequiresPriorState,
may_partially_apply: false,
},
Self::RemoveRoute(_) => MutationSemantics {
kind: MutationKind::RemoveRoute,
precondition: MutationPrecondition::Present,
idempotency: MutationIdempotency::Strict,
privilege: MutationPrivilege::Elevated,
confirmation: MutationConfirmation::NativeAcknowledgement,
reversibility: MutationReversibility::RequiresPriorState,
may_partially_apply: false,
},
Self::AddAddress(_) => MutationSemantics {
kind: MutationKind::AddAddress,
precondition: MutationPrecondition::Absent,
idempotency: MutationIdempotency::Strict,
privilege: MutationPrivilege::Elevated,
confirmation: MutationConfirmation::ReadAfterWrite,
reversibility: MutationReversibility::RequiresPriorState,
may_partially_apply: false,
},
Self::RemoveAddress(_) => MutationSemantics {
kind: MutationKind::RemoveAddress,
precondition: MutationPrecondition::Present,
idempotency: MutationIdempotency::Strict,
privilege: MutationPrivilege::Elevated,
confirmation: MutationConfirmation::NativeAcknowledgement,
reversibility: MutationReversibility::RequiresPriorState,
may_partially_apply: false,
},
Self::SetDnsConfig(_) => MutationSemantics {
kind: MutationKind::SetDnsConfig,
precondition: MutationPrecondition::Any,
idempotency: MutationIdempotency::Replace,
privilege: MutationPrivilege::Elevated,
confirmation: MutationConfirmation::ReadAfterWrite,
reversibility: MutationReversibility::NotGuaranteed,
may_partially_apply: true,
},
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct MutationPlan {
operations: Vec<Mutation>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum MutationOutcome {
Applied,
Failed {
error: Error,
may_have_applied: bool,
},
NotAttempted,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum RollbackStatus {
NotNeeded,
NotAttempted,
Completed,
Failed {
operation_index: usize,
error: Error,
},
}
#[derive(Debug)]
pub struct MutationPlanReport {
outcomes: Vec<MutationOutcome>,
rollback: RollbackStatus,
}
impl MutationPlanReport {
pub fn new(
outcomes: impl IntoIterator<Item = MutationOutcome>,
rollback: RollbackStatus,
) -> Self {
Self {
outcomes: outcomes.into_iter().collect(),
rollback,
}
}
pub fn outcomes(&self) -> &[MutationOutcome] {
&self.outcomes
}
pub fn rollback(&self) -> &RollbackStatus {
&self.rollback
}
pub fn is_success(&self) -> bool {
self.outcomes
.iter()
.all(|outcome| matches!(outcome, MutationOutcome::Applied))
}
pub fn applied_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| matches!(outcome, MutationOutcome::Applied))
.count()
}
pub fn not_attempted_count(&self) -> usize {
self.outcomes
.iter()
.filter(|outcome| matches!(outcome, MutationOutcome::NotAttempted))
.count()
}
}
impl MutationPlan {
pub const fn new() -> Self {
Self {
operations: Vec::new(),
}
}
pub fn from_operations(operations: impl IntoIterator<Item = Mutation>) -> Self {
Self {
operations: operations.into_iter().collect(),
}
}
pub fn push(&mut self, operation: Mutation) {
self.operations.push(operation);
}
pub fn operations(&self) -> &[Mutation] {
&self.operations
}
pub fn is_empty(&self) -> bool {
self.operations.is_empty()
}
pub fn len(&self) -> usize {
self.operations.len()
}
}
impl IntoIterator for MutationPlan {
type Item = Mutation;
type IntoIter = std::vec::IntoIter<Mutation>;
fn into_iter(self) -> Self::IntoIter {
self.operations.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{IpAddress, Network};
use net_lattice_ip::{Ipv4Address, Ipv4Network, Ipv4PrefixLength};
fn network() -> Network {
Network::from(Ipv4Network::new(
Ipv4Address::new(192, 0, 2, 0),
Ipv4PrefixLength::new(24).expect("valid prefix"),
))
}
#[test]
fn dns_replacement_exposes_partial_application_risk() {
let operation = Mutation::SetDnsConfig(NewDnsConfig::with(
vec![IpAddress::from(Ipv4Address::new(1, 1, 1, 1))],
Vec::new(),
));
assert_eq!(
operation.semantics().precondition,
MutationPrecondition::Any
);
assert_eq!(
operation.semantics().idempotency,
MutationIdempotency::Replace
);
assert!(operation.semantics().may_partially_apply);
assert_eq!(
operation.semantics().confirmation,
MutationConfirmation::ReadAfterWrite
);
assert_eq!(
operation.semantics().reversibility,
MutationReversibility::NotGuaranteed
);
}
#[test]
fn address_addition_has_an_observed_readback_contract() {
let operation = Mutation::AddAddress(NewInterfaceAddress::new(
crate::interface::InterfaceId::new(2),
network(),
));
assert_eq!(
operation.semantics().confirmation,
MutationConfirmation::ReadAfterWrite
);
assert_eq!(
operation.semantics().precondition,
MutationPrecondition::Absent
);
assert_eq!(
operation.semantics().reversibility,
MutationReversibility::RequiresPriorState
);
assert!(!operation.semantics().may_partially_apply);
}
#[test]
fn plan_keeps_declared_order_without_executing_operations() {
let first = Mutation::AddRoute(Route::new(crate::route::RouteId::new(1), network()));
let second = Mutation::RemoveRoute(Route::new(crate::route::RouteId::new(2), network()));
let plan = MutationPlan::from_operations([first.clone(), second.clone()]);
assert_eq!(plan.operations(), [first, second]);
assert_eq!(plan.len(), 2);
assert!(!plan.is_empty());
}
#[test]
fn route_operations_expose_strict_native_acknowledgement_contracts() {
let route = Route::new(crate::route::RouteId::new(1), network());
let added = Mutation::AddRoute(route.clone()).semantics();
assert_eq!(added.kind, MutationKind::AddRoute);
assert_eq!(added.precondition, MutationPrecondition::Absent);
assert_eq!(added.idempotency, MutationIdempotency::Strict);
assert_eq!(added.privilege, MutationPrivilege::Elevated);
assert_eq!(
added.confirmation,
MutationConfirmation::NativeAcknowledgement
);
assert_eq!(
added.reversibility,
MutationReversibility::RequiresPriorState
);
assert!(!added.may_partially_apply);
let removed = Mutation::RemoveRoute(route).semantics();
assert_eq!(removed.kind, MutationKind::RemoveRoute);
assert_eq!(removed.precondition, MutationPrecondition::Present);
assert_eq!(removed.idempotency, MutationIdempotency::Strict);
assert_eq!(removed.privilege, MutationPrivilege::Elevated);
assert_eq!(
removed.confirmation,
MutationConfirmation::NativeAcknowledgement
);
assert_eq!(
removed.reversibility,
MutationReversibility::RequiresPriorState
);
assert!(!removed.may_partially_apply);
}
#[test]
fn address_removal_exposes_its_observed_record_contract() {
let address =
InterfaceAddress::new(crate::ifaddr::InterfaceAddressId::new(1), 1, network());
let semantics = Mutation::RemoveAddress(address).semantics();
assert_eq!(semantics.kind, MutationKind::RemoveAddress);
assert_eq!(semantics.precondition, MutationPrecondition::Present);
assert_eq!(semantics.idempotency, MutationIdempotency::Strict);
assert_eq!(semantics.privilege, MutationPrivilege::Elevated);
assert_eq!(
semantics.confirmation,
MutationConfirmation::NativeAcknowledgement
);
assert_eq!(
semantics.reversibility,
MutationReversibility::RequiresPriorState
);
assert!(!semantics.may_partially_apply);
}
#[test]
fn empty_plan_can_be_built_appended_and_consumed() {
let mut plan = MutationPlan::new();
assert!(plan.is_empty());
assert_eq!(plan.len(), 0);
let operation = Mutation::AddRoute(Route::new(crate::route::RouteId::new(1), network()));
plan.push(operation.clone());
assert_eq!(plan.operations(), std::slice::from_ref(&operation));
assert_eq!(plan.into_iter().collect::<Vec<_>>(), vec![operation]);
}
#[test]
fn plan_report_preserves_partial_failure_and_rollback_boundary() {
let report = MutationPlanReport::new(
[
MutationOutcome::Applied,
MutationOutcome::Failed {
error: Error::PermissionDenied,
may_have_applied: true,
},
MutationOutcome::NotAttempted,
],
RollbackStatus::NotAttempted,
);
assert!(!report.is_success());
assert_eq!(report.applied_count(), 1);
assert_eq!(report.not_attempted_count(), 1);
assert!(matches!(report.rollback(), RollbackStatus::NotAttempted));
assert!(matches!(
&report.outcomes()[1],
MutationOutcome::Failed {
may_have_applied: true,
..
}
));
}
}