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}