Skip to main content

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}