osdns 0.2.0

Safe, transactional control of operating-system DNS configuration
Documentation
use std::fmt;

use crate::capability::BackendKind;
use crate::ownership::ResourceId;

/// Crate-wide result type.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// The typed error model of `osdns`.
///
/// Variants carry structured context (backend, resource) instead of collapsing
/// everything into strings. Platform-specific detail lives in the message
/// fields of each variant.
///
/// # Operational semantics
///
/// - [`Error::Unsupported`]: nothing was mutated. The backend cannot
///   represent the request. Do not retry without changing the configuration;
///   probe [`Capabilities`](crate::Capabilities) first.
/// - [`Error::RequiresPrivilege`]: nothing was mutated. Re-run with elevated
///   privileges; `osdns` never escalates on its own.
/// - [`Error::BackendUnavailable`]: nothing was mutated. The backend or an OS
///   resource it depends on is missing. The caller may retry later or select
///   a different backend.
/// - [`Error::Timeout`]: the operation (usually lock acquisition) exceeded its
///   deadline. Nothing was mutated by this call, but another lease may hold
///   the resource; retry with backoff or abandon.
/// - [`Error::Conflict`]: nothing was mutated by this call. Another lease or
///   an unresolved journal blocks ownership; see [`ConflictReason`]. A lease
///   that reports this remains usable.
/// - [`Error::ExternalModification`]: nothing was mutated. Current state
///   matches neither the applied nor the original snapshot. The lease remains
///   usable: retry [`Lease::restore`](crate::Lease::restore), or call
///   [`Lease::abandon`](crate::Lease::abandon) to leave external state in
///   place. Journal state is retained for recovery.
/// - [`Error::InvalidConfig`]: nothing was mutated. The request failed
///   validation before any lock, journal write, or OS call; fix the input.
/// - [`Error::UpdateRequiresRebind`]: nothing was mutated. The requested
///   config is valid but resolves to a different resource set than the lease
///   owns. Drop this lease (restore or abandon) and apply fresh.
/// - [`Error::VerificationFailed`]: the mutation was applied but read-back did
///   not match. A rollback to the captured state was attempted (best-effort)
///   and the journal record was kept, so [`DnsManager::recover_stale`](crate::DnsManager::recover_stale)
///   can finish the work. The lease remains usable.
/// - [`Error::JournalCorrupt`]: no mutation was attempted. The call fails
///   closed; inspect or clear the state directory manually.
/// - [`Error::UnsupportedJournalVersion`]: the durable state belongs to an
///   incompatible osdns journal format. Clear the old state before upgrade.
/// - [`Error::ResourceGone`]: the named native incarnation no longer exists;
///   its owned state cannot still affect the OS.
/// - [`Error::ResourcePlatform`] / [`Error::ResourceIdentity`]: a native
///   failure or identity ambiguity scoped to the carried resource.
/// - [`Error::Io`] / [`Error::Platform`]: the effect is backend-dependent.
///   Assume the state may have changed, keep the lease, and use read-back
///   ([`DnsManager::snapshot`](crate::DnsManager::snapshot)) or recovery to
///   determine the outcome before retrying.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The backend proved that this native resource incarnation is gone and
    /// can no longer carry osdns-owned state.
    #[error("resource is gone on {backend}: {resource}: {message}")]
    ResourceGone {
        /// Backend that established disappearance.
        backend: BackendKind,
        /// Historical mutation target.
        resource: ResourceId,
        /// Native diagnostic detail.
        message: String,
    },
    /// A resource-scoped native failure with a programmatically available target.
    #[error("platform error on {backend} for {resource}: {message}")]
    ResourcePlatform {
        /// Backend that failed.
        backend: BackendKind,
        /// Resource being operated on.
        resource: ResourceId,
        /// Native diagnostic detail.
        message: String,
    },
    /// The current native target cannot be proven to be the recorded incarnation.
    #[error("resource identity mismatch on {backend} for {resource}: {message}")]
    ResourceIdentity {
        /// Backend validating the identity.
        backend: BackendKind,
        /// Historical mutation target.
        resource: ResourceId,
        /// Why equality could not be established.
        message: String,
    },
    /// The active backend cannot represent or perform the requested operation.
    ///
    /// Nothing was mutated. Check [`Capabilities`](crate::Capabilities) before
    /// retrying with a different configuration.
    #[error("unsupported operation on {backend}: {reason}")]
    Unsupported {
        /// The backend that rejected the operation.
        backend: BackendKind,
        /// Why the operation is not supported.
        reason: String,
    },
    /// The current process lacks the privileges required for the operation.
    ///
    /// Nothing was mutated. `osdns` never elevates privileges on its own;
    /// re-run with elevated privileges.
    #[error("operation requires elevated privileges: {0}")]
    RequiresPrivilege(String),
    /// The backend or an OS resource it depends on is not available.
    ///
    /// Nothing was mutated. Retry later or select a different backend.
    #[error("backend is unavailable: {0}")]
    BackendUnavailable(String),
    /// A bounded operation (e.g. acquiring a resource lock) exceeded its deadline.
    ///
    /// Nothing was mutated by this call, but another lease may hold the
    /// resource. Retry with backoff.
    #[error("operation timed out on {resource}: {operation}")]
    Timeout {
        /// The resource the operation targeted.
        resource: ResourceId,
        /// What timed out.
        operation: String,
    },
    /// The resource cannot be mutated right now because someone else owns it
    /// or a previous transaction blocks ownership.
    ///
    /// Nothing was mutated by this call; see [`ConflictReason`]. The calling
    /// lease, if any, remains usable.
    #[error("resource conflict on {resource}: {reason}")]
    Conflict {
        /// The contended resource.
        resource: ResourceId,
        /// Why the conflict occurred.
        reason: ConflictReason,
    },
    /// Another actor changed the DNS state after we applied ours.
    ///
    /// The current state is neither the state we applied nor the state we
    /// captured before applying. Per the ownership invariant, nothing was
    /// mutated and the journal record was kept. The lease remains usable:
    /// retry [`Lease::restore`](crate::Lease::restore) or
    /// [`Lease::abandon`](crate::Lease::abandon) it.
    #[error("external modification detected on {resource}: {detail}")]
    ExternalModification {
        /// The resource whose state changed externally.
        resource: ResourceId,
        /// Detail about what was detected.
        detail: String,
    },
    /// The requested configuration is invalid or not representable.
    ///
    /// Nothing was mutated; validation runs before locks, journals, or OS
    /// calls. Fix the input and retry.
    #[error("invalid configuration: {0}")]
    InvalidConfig(String),
    /// The requested update is valid but cannot be applied to this lease
    /// because it resolves to a different set of OS resources than the lease
    /// owns.
    ///
    /// Nothing was mutated. A lease can never silently change what OS
    /// resources it owns: restore or abandon this lease and apply the new
    /// configuration fresh. This is distinct from [`Error::InvalidConfig`],
    /// which means the configuration itself is malformed or unrepresentable.
    #[error("update requires a fresh lease: owned {owned:?} vs requested {requested:?}")]
    UpdateRequiresRebind {
        /// Resources currently owned by the lease.
        owned: Vec<ResourceId>,
        /// Resources the requested configuration would require.
        requested: Vec<ResourceId>,
    },
    /// A mutation was applied but the read-back from the system did not match
    /// the desired semantics.
    ///
    /// A rollback to the captured state was attempted best-effort and the
    /// journal record was kept for
    /// [`DnsManager::recover_stale`](crate::DnsManager::recover_stale). The
    /// lease remains usable.
    #[error("verification failed on {resource}: {detail}")]
    VerificationFailed {
        /// The resource that failed verification.
        resource: ResourceId,
        /// Detail about the mismatch.
        detail: String,
    },
    /// A journal record uses an intentionally incompatible format version.
    ///
    /// No mutation was attempted. osdns does not migrate pre-v1 durable
    /// state; clear or reset the old state directory before upgrading.
    #[error(
        "unsupported journal schema version {found} in {path} (supported: {supported}); clear or reset old osdns state before upgrading"
    )]
    UnsupportedJournalVersion {
        /// Journal file containing the incompatible record.
        path: std::path::PathBuf,
        /// Version found in the record envelope.
        found: u32,
        /// Version understood by this build.
        supported: u32,
    },
    /// A current-schema journal record is malformed or internally inconsistent.
    ///
    /// This is always treated as fail-closed: no mutation is attempted.
    /// Inspect the state directory manually.
    #[error("journal is corrupt: {0}")]
    JournalCorrupt(String),
    /// An I/O error occurred while operating on state or lock files.
    ///
    /// The effect depends on when the failure happened; assume state may have
    /// changed and use read-back or recovery before retrying.
    #[error(transparent)]
    Io(#[from] std::io::Error),
    /// A platform API returned an unexpected error.
    ///
    /// The effect is backend-dependent; assume state may have changed, keep
    /// the lease, and verify with
    /// [`DnsManager::snapshot`](crate::DnsManager::snapshot) before retrying.
    #[error("platform error on {backend}: {message}")]
    Platform {
        /// The backend that produced the error.
        backend: BackendKind,
        /// Detail from the platform API.
        message: String,
    },
}

impl Error {
    pub(crate) fn invalid_config(message: impl fmt::Display) -> Self {
        Error::InvalidConfig(message.to_string())
    }

    pub(crate) fn unsupported(backend: BackendKind, reason: impl fmt::Display) -> Self {
        Error::Unsupported {
            backend,
            reason: reason.to_string(),
        }
    }

    pub(crate) fn platform(backend: BackendKind, message: impl fmt::Display) -> Self {
        Error::Platform {
            backend,
            message: message.to_string(),
        }
    }

    /// Returns `true` when this error is [`Error::ExternalModification`].
    pub fn is_external_modification(&self) -> bool {
        matches!(self, Error::ExternalModification { .. })
    }
}

/// Why a resource conflict occurred.
///
/// All variants imply nothing was mutated by the call that reported them.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ConflictReason {
    /// A live lease in this process already owns the resource.
    #[error("the resource is already owned by an active lease in this process")]
    AlreadyLeasedInProcess,
    /// A journal record from a previous lease exists and cannot be safely
    /// resolved automatically (typically because the current state matches
    /// neither the recorded applied state nor the original state).
    #[error("an unresolved journal record from a previous lease blocks this operation: {detail}")]
    StaleJournalUnresolved {
        /// Detail about the unresolved journal.
        detail: String,
    },
    /// The lease has already been restored, abandoned, or invalidated.
    #[error("the lease is no longer active")]
    LeaseNotActive,
    /// The resource already exists and is claimed by someone else (another
    /// osdns owner or a manual configuration). Nothing was mutated.
    #[error("the resource is already occupied and is not owned by this lease: {detail}")]
    ResourceOccupied {
        /// Detail about the existing claim.
        detail: String,
    },
}