Skip to main content

host_identity/
error.rs

1//! Error type for identity resolution.
2
3use std::io;
4use std::path::PathBuf;
5
6use crate::source::SourceKind;
7
8/// Errors returned by [`crate::Resolver::resolve`].
9///
10/// Every variant except [`Error::NoSource`] carries the [`SourceKind`]
11/// that produced it, so logs and error messages unambiguously identify
12/// which source failed. [`Error::source_kind`] exposes the field
13/// uniformly across variants.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum Error {
17    /// Every configured source was tried and none produced a usable identity.
18    #[error("no identity source produced a value (tried: {tried})")]
19    NoSource {
20        /// Comma-separated list of sources that were attempted.
21        tried: String,
22    },
23
24    /// A source file was present but contained the systemd `uninitialized`
25    /// sentinel. The caller should not treat this as a valid identity — every
26    /// host in this state would hash to the same UUID.
27    #[error("{source_kind}: {path} contains the `uninitialized` sentinel")]
28    Uninitialized {
29        /// Which source produced the error.
30        source_kind: SourceKind,
31        /// Path of the offending file.
32        path: PathBuf,
33    },
34
35    /// I/O failure while reading a source file. Command-spawn failures are
36    /// reported as [`Error::Platform`] instead — this variant's `path`
37    /// field is always a real filesystem path.
38    #[error("{source_kind}: I/O error reading {}: {source}", path.display())]
39    Io {
40        /// Which source produced the error.
41        source_kind: SourceKind,
42        /// Filesystem path that produced the error.
43        path: PathBuf,
44        /// Underlying I/O error.
45        #[source]
46        source: io::Error,
47    },
48
49    /// A source returned a value that is not a well-formed identifier
50    /// (empty after trimming, wrong shape, invalid UTF-8, …).
51    #[error("{source_kind}: malformed value: {reason}")]
52    Malformed {
53        /// Which source produced the error.
54        source_kind: SourceKind,
55        /// Human-readable reason.
56        reason: String,
57    },
58
59    /// Platform-specific lookup failed (registry query, syscall, ioreg,
60    /// cloud metadata contract violation, …).
61    #[error("{source_kind}: {reason}")]
62    Platform {
63        /// Which source produced the error.
64        source_kind: SourceKind,
65        /// Human-readable reason.
66        reason: String,
67    },
68}
69
70impl Error {
71    /// The source that produced this error, if the variant carries one.
72    ///
73    /// Returns `None` only for [`Error::NoSource`], which reports that
74    /// *every* source was tried and none produced a value — that error
75    /// doesn't belong to any single source.
76    #[must_use]
77    pub fn source_kind(&self) -> Option<SourceKind> {
78        match self {
79            Self::NoSource { .. } => None,
80            Self::Uninitialized { source_kind, .. }
81            | Self::Io { source_kind, .. }
82            | Self::Malformed { source_kind, .. }
83            | Self::Platform { source_kind, .. } => Some(*source_kind),
84        }
85    }
86
87    /// Whether this error is reasonable for the caller to recover from
88    /// at runtime (log a warning, mint a per-run placeholder UUID, etc.)
89    /// rather than treat as a fatal configuration problem.
90    ///
91    /// - [`Error::NoSource`] → `true`. No source produced a value, but
92    ///   the crate is behaving correctly; the caller's chain simply
93    ///   doesn't match the environment. Apps often handle this by
94    ///   falling back to their own ID scheme.
95    /// - All other variants → `false`. They indicate a concrete fault
96    ///   (sentinel, I/O failure, malformed source value, platform-tool
97    ///   failure) that won't fix itself on retry; the caller should
98    ///   surface them to the operator.
99    ///
100    /// This classification is a guideline, not a hard contract. A
101    /// particular deployment might reasonably treat an `Io` error on
102    /// `/etc/machine-id` as recoverable (keep going with the next
103    /// source) — the method exists to give the common case a one-liner,
104    /// not to remove the caller's judgement.
105    #[must_use]
106    pub fn is_recoverable(&self) -> bool {
107        matches!(self, Self::NoSource { .. })
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn is_recoverable_only_true_for_no_source() {
117        assert!(Error::NoSource { tried: "x".into() }.is_recoverable());
118        assert!(
119            !Error::Uninitialized {
120                source_kind: SourceKind::MachineId,
121                path: "/etc/machine-id".into(),
122            }
123            .is_recoverable()
124        );
125        assert!(
126            !Error::Platform {
127                source_kind: SourceKind::IoPlatformUuid,
128                reason: "ioreg failed".into(),
129            }
130            .is_recoverable()
131        );
132        assert!(
133            !Error::Malformed {
134                source_kind: SourceKind::Dmi,
135                reason: "not a uuid".into(),
136            }
137            .is_recoverable()
138        );
139    }
140
141    #[test]
142    fn source_kind_round_trips_through_error() {
143        let err = Error::Platform {
144            source_kind: SourceKind::AwsImds,
145            reason: "x".into(),
146        };
147        assert_eq!(err.source_kind(), Some(SourceKind::AwsImds));
148        assert_eq!(
149            Error::NoSource {
150                tried: "env-override".into()
151            }
152            .source_kind(),
153            None
154        );
155    }
156}