host_identity/hostid.rs
1//! The resolved [`HostId`] value.
2
3use std::fmt;
4
5use uuid::Uuid;
6
7use crate::source::SourceKind;
8
9/// A stable host identifier.
10///
11/// A `HostId` is always a UUID, but wraps additional provenance: which source
12/// produced the raw value and whether the host was running inside a container
13/// at resolution time. The wire representation (via [`HostId::as_uuid`] or
14/// [`fmt::Display`]) is the hyphenated UUID string.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct HostId {
17 uuid: Uuid,
18 source: SourceKind,
19 in_container: bool,
20}
21
22impl HostId {
23 pub(crate) fn new(uuid: Uuid, source: SourceKind, in_container: bool) -> Self {
24 Self {
25 uuid,
26 source,
27 in_container,
28 }
29 }
30
31 /// The identifier as a [`Uuid`].
32 #[must_use]
33 pub fn as_uuid(&self) -> Uuid {
34 self.uuid
35 }
36
37 /// Which source the raw value was read from.
38 #[must_use]
39 pub fn source(&self) -> SourceKind {
40 self.source
41 }
42
43 /// Whether the resolver detected a container runtime at resolution time.
44 ///
45 /// When `true`, [`HostId::source`] reflects the container branch rather
46 /// than a host-level source.
47 ///
48 /// On non-Linux targets this is always `false` — container-runtime
49 /// detection is implemented via `/.dockerenv` and `/proc/1/cgroup`,
50 /// both Linux-only. A macOS or Windows host running Docker Desktop
51 /// will still report `false` because the host process itself is not
52 /// inside the container namespace.
53 #[must_use]
54 pub fn in_container(&self) -> bool {
55 self.in_container
56 }
57
58 /// Log-friendly summary combining source kind and UUID.
59 ///
60 /// Returns a value that implements [`fmt::Display`] as
61 /// `"{source_kind}:{uuid}"`, e.g. `"aws-imds:i-0abc…"` becomes
62 /// `"aws-imds:12345678-1234-…"` after wrapping. Keeps `HostId`'s own
63 /// `Display` impl wire-clean (just the UUID) while giving operators
64 /// the provenance tag they usually want in logs.
65 ///
66 /// ```
67 /// # use host_identity::{HostId, Resolver, sources::EnvOverride};
68 /// # // SAFETY: test-only env manipulation.
69 /// # unsafe { std::env::set_var("HOST_IDENTITY_TEST_SUMMARY", "x") };
70 /// # let id = Resolver::new()
71 /// # .push(EnvOverride::new("HOST_IDENTITY_TEST_SUMMARY"))
72 /// # .resolve().unwrap();
73 /// # unsafe { std::env::remove_var("HOST_IDENTITY_TEST_SUMMARY") };
74 /// let s = id.summary().to_string();
75 /// assert!(s.starts_with("env-override:"));
76 /// ```
77 #[must_use]
78 pub fn summary(&self) -> HostIdSummary<'_> {
79 HostIdSummary(self)
80 }
81}
82
83/// `Display` wrapper returned by [`HostId::summary`].
84///
85/// Formats as `"{source_kind}:{uuid}"`. Not constructible directly;
86/// callers get an instance from `HostId::summary`.
87#[derive(Debug)]
88pub struct HostIdSummary<'a>(&'a HostId);
89
90impl fmt::Display for HostIdSummary<'_> {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "{}:{}", self.0.source, self.0.uuid)
93 }
94}
95
96impl fmt::Display for HostId {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 self.uuid.fmt(f)
99 }
100}
101
102/// One source's outcome in a full-chain walk.
103///
104/// Returned by [`crate::Resolver::resolve_all`] (and the free
105/// [`crate::resolve_all`] / [`crate::resolve_all_with_transport`] wrappers)
106/// for every source in the chain, in chain order. Unlike [`crate::Resolver::resolve`],
107/// a full walk does **not** short-circuit on the first success or the first
108/// error — every source is consulted.
109///
110/// Use this when you want to audit what each source would produce (diagnostics,
111/// operator tooling, test harnesses). For normal resolution use
112/// [`crate::resolve`] — it stops at the first usable source.
113#[derive(Debug)]
114pub enum ResolveOutcome {
115 /// The source produced a usable identifier.
116 Found(HostId),
117 /// The source had nothing to offer (file absent, endpoint unreachable,
118 /// feature disabled, wrong platform).
119 Skipped(SourceKind),
120 /// The source produced a hard error. In a short-circuiting `resolve()`
121 /// this would have aborted the chain; in `resolve_all` the error is
122 /// captured here and the walk continues.
123 ///
124 /// The outer [`SourceKind`] and the inner [`crate::Error::source_kind`]
125 /// are guaranteed to be equal. The field on this variant is the
126 /// authoritative provenance for callers matching outcomes —
127 /// introspecting the inner `Error` for its kind is equivalent but
128 /// noisier.
129 Errored(SourceKind, crate::Error),
130}
131
132impl ResolveOutcome {
133 /// Which source produced this outcome.
134 #[must_use]
135 pub fn source(&self) -> SourceKind {
136 match self {
137 Self::Found(id) => id.source(),
138 Self::Skipped(kind) | Self::Errored(kind, _) => *kind,
139 }
140 }
141
142 /// The `HostId` if the source produced one, else `None`.
143 #[must_use]
144 pub fn host_id(&self) -> Option<&HostId> {
145 match self {
146 Self::Found(id) => Some(id),
147 Self::Skipped(_) | Self::Errored(_, _) => None,
148 }
149 }
150}