Skip to main content

host_identity/
source.rs

1//! The [`Source`] trait and associated types.
2
3use std::fmt;
4
5use crate::error::Error;
6
7/// A single identity source.
8///
9/// Sources are composed into a [`crate::Resolver`]. The resolver walks them
10/// in order and uses the first one that yields a [`Probe`]; sources that
11/// have nothing to contribute (file missing, feature disabled, wrong
12/// platform) return `Ok(None)` and are skipped.
13///
14/// Implementations must be inexpensive to construct — the resolver may
15/// instantiate them in advance — but probing may perform I/O or spawn a
16/// subprocess.
17pub trait Source: fmt::Debug + Send + Sync {
18    /// Provenance label for this source. Shown in error messages and the
19    /// resolved [`crate::HostId`].
20    fn kind(&self) -> SourceKind;
21
22    /// Attempt to produce a raw identifier.
23    ///
24    /// - `Ok(Some(probe))` — a usable identifier was found
25    /// - `Ok(None)` — this source had nothing to offer; the resolver
26    ///   continues to the next one
27    /// - `Err(_)` — a hard failure the caller should know about (permission
28    ///   denied, malformed registry entry, sentinel value like
29    ///   `uninitialized`)
30    ///
31    /// # Errors
32    ///
33    /// Returns [`Error`] when the source encounters a hard failure such as
34    /// an I/O error other than "not found" / "permission denied", a
35    /// platform-tool failure, or a sentinel value that indicates an
36    /// uninitialized identifier.
37    fn probe(&self) -> Result<Option<Probe>, Error>;
38}
39
40impl<T: Source + ?Sized> Source for Box<T> {
41    fn kind(&self) -> SourceKind {
42        (**self).kind()
43    }
44    fn probe(&self) -> Result<Option<Probe>, Error> {
45        (**self).probe()
46    }
47}
48
49/// A raw identifier returned by a [`Source`], before UUID wrapping.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Probe {
52    kind: SourceKind,
53    value: String,
54}
55
56impl Probe {
57    /// Construct a probe. The caller is responsible for trimming and
58    /// sentinel-rejection; see [`crate::sources::normalize`].
59    #[must_use]
60    pub fn new(kind: SourceKind, value: impl Into<String>) -> Self {
61        Self {
62            kind,
63            value: value.into(),
64        }
65    }
66
67    /// Which source produced this value.
68    #[must_use]
69    pub fn kind(&self) -> SourceKind {
70        self.kind
71    }
72
73    /// The raw string value.
74    #[must_use]
75    pub fn value(&self) -> &str {
76        &self.value
77    }
78
79    pub(crate) fn into_parts(self) -> (SourceKind, String) {
80        (self.kind, self.value)
81    }
82}
83
84/// Short, stable label identifying a source.
85///
86/// Covers every built-in source plus a [`SourceKind::Custom`] variant for
87/// consumer-defined sources. Displayed in error messages, logs, and on the
88/// resolved [`crate::HostId`].
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90#[non_exhaustive]
91pub enum SourceKind {
92    /// Environment-variable override.
93    EnvOverride,
94    /// File-path override.
95    FileOverride,
96    /// Container runtime ID extracted from `/proc/self/mountinfo` (Linux).
97    Container,
98    /// LXC/LXD container name from `/proc/self/cgroup` or
99    /// `/proc/self/mountinfo`, salted with `/etc/machine-id` (Linux).
100    Lxc,
101    /// `/etc/machine-id` (Linux).
102    MachineId,
103    /// `/var/lib/dbus/machine-id` (Linux).
104    DbusMachineId,
105    /// `/sys/class/dmi/id/product_uuid` — SMBIOS system UUID (Linux).
106    Dmi,
107    /// `/etc/hostid` — glibc 4-byte legacy host identifier (Linux).
108    LinuxHostId,
109    /// `IOPlatformUUID` from `IOPlatformExpertDevice` (macOS).
110    IoPlatformUuid,
111    /// `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (Windows).
112    WindowsMachineGuid,
113    /// `/etc/hostid` (FreeBSD).
114    FreeBsdHostId,
115    /// `kenv smbios.system.uuid` (FreeBSD SMBIOS).
116    KenvSmbios,
117    /// `sysctl kern.hostid` (NetBSD, OpenBSD).
118    BsdKernHostId,
119    /// `hostid(1)` (illumos, Solaris).
120    IllumosHostId,
121    /// AWS EC2 instance ID via `IMDSv2`.
122    AwsImds,
123    /// GCP Compute Engine numeric instance ID via the metadata server.
124    GcpMetadata,
125    /// Azure VM UUID via the Azure Instance Metadata Service.
126    AzureImds,
127    /// `DigitalOcean` Droplet numeric ID.
128    DigitalOceanMetadata,
129    /// Hetzner Cloud numeric server ID.
130    HetznerMetadata,
131    /// Oracle Cloud Infrastructure instance OCID.
132    OciMetadata,
133    /// `OpenStack` Nova instance UUID via the metadata service.
134    OpenStackMetadata,
135    /// Kubernetes pod UID derived from `/proc/self/mountinfo`.
136    KubernetesPodUid,
137    /// Kubernetes service-account namespace.
138    KubernetesServiceAccount,
139    /// Kubernetes downward-API projected file.
140    KubernetesDownwardApi,
141    /// Caller-supplied source; the payload is a short label for logs.
142    ///
143    /// The label appears verbatim in `Display` and
144    /// [`SourceKind::as_str`] output — a `Custom("machine-id")` renders
145    /// identically to the built-in [`SourceKind::MachineId`]. Callers
146    /// should pick labels that don't collide with the built-in
147    /// identifiers listed in [`crate::ids::source_ids`] so operators
148    /// reading logs can tell which source a probe came from.
149    Custom(&'static str),
150}
151
152/// Generate `as_str` and `from_id` from a single variant↔string table.
153/// Keeps the two methods in lockstep — adding a variant means adding
154/// one row.
155macro_rules! source_kind_ids {
156    ( $( $variant:ident => $id:literal , $desc:literal );* $(;)? ) => {
157        impl SourceKind {
158            /// Construct a [`SourceKind::Custom`] from a static string label.
159            #[must_use]
160            pub const fn custom(label: &'static str) -> Self {
161                Self::Custom(label)
162            }
163
164            /// Short, stable, lowercase name suitable for logs and telemetry.
165            #[must_use]
166            pub fn as_str(self) -> &'static str {
167                match self {
168                    $( Self::$variant => $id, )*
169                    Self::Custom(label) => label,
170                }
171            }
172
173            /// Inverse of [`SourceKind::as_str`] for the built-in identifiers.
174            ///
175            /// Returns `Some(kind)` when `id` matches one of the stable
176            /// strings returned by `as_str` for a non-`Custom` variant,
177            /// `None` otherwise. `SourceKind::Custom` intentionally never
178            /// round-trips through this — a runtime string cannot safely
179            /// become a `&'static str`.
180            #[must_use]
181            pub fn from_id(id: &str) -> Option<Self> {
182                match id {
183                    $( $id => Some(Self::$variant), )*
184                    _ => None,
185                }
186            }
187
188            /// One-line plain-text description of where this source reads
189            /// its identifier from. `Custom` returns an empty string —
190            /// callers supply their own labels.
191            #[must_use]
192            pub fn describe(self) -> &'static str {
193                match self {
194                    $( Self::$variant => $desc, )*
195                    Self::Custom(_) => "",
196                }
197            }
198        }
199    };
200}
201
202source_kind_ids! {
203    EnvOverride              => "env-override",              "environment-variable override (HOST_IDENTITY by default)";
204    FileOverride             => "file-override",             "caller-supplied file containing a host identifier";
205    Container                => "container",                 "container runtime ID from /proc/self/mountinfo (Linux)";
206    Lxc                      => "lxc",                       "LXC/LXD container name from /proc/self/cgroup or /proc/self/mountinfo, salted with /etc/machine-id (Linux)";
207    MachineId                => "machine-id",                "/etc/machine-id (Linux)";
208    DbusMachineId            => "dbus-machine-id",           "/var/lib/dbus/machine-id (Linux)";
209    Dmi                      => "dmi",                       "/sys/class/dmi/id/product_uuid — SMBIOS system UUID (Linux)";
210    LinuxHostId              => "linux-hostid",              "/etc/hostid — 32-bit legacy host identifier (Linux)";
211    IoPlatformUuid           => "io-platform-uuid",          "IOPlatformUUID from IOPlatformExpertDevice (macOS)";
212    WindowsMachineGuid       => "windows-machine-guid",      "HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid (Windows)";
213    FreeBsdHostId            => "freebsd-hostid",            "/etc/hostid (FreeBSD)";
214    KenvSmbios               => "kenv-smbios",               "kenv smbios.system.uuid — SMBIOS system UUID (FreeBSD)";
215    BsdKernHostId            => "bsd-kern-hostid",           "sysctl kern.hostid (NetBSD, OpenBSD)";
216    IllumosHostId            => "illumos-hostid",            "hostid(1) (illumos, Solaris)";
217    AwsImds                  => "aws-imds",                  "AWS EC2 instance ID via IMDSv2";
218    GcpMetadata              => "gcp-metadata",              "GCP Compute Engine numeric instance ID via the metadata server";
219    AzureImds                => "azure-imds",                "Azure VM UUID via the Azure Instance Metadata Service";
220    DigitalOceanMetadata     => "digital-ocean-metadata",    "DigitalOcean Droplet numeric ID via the metadata service";
221    HetznerMetadata          => "hetzner-metadata",          "Hetzner Cloud numeric server ID via the metadata service";
222    OciMetadata              => "oci-metadata",              "Oracle Cloud Infrastructure instance OCID via the metadata service";
223    OpenStackMetadata        => "openstack-metadata",        "OpenStack Nova instance UUID via the metadata service";
224    KubernetesPodUid         => "kubernetes-pod-uid",        "Kubernetes pod UID derived from /proc/self/mountinfo";
225    KubernetesServiceAccount => "kubernetes-service-account","Kubernetes service-account namespace from the projected token volume";
226    KubernetesDownwardApi    => "kubernetes-downward-api",   "caller-supplied Kubernetes downward-API projected file";
227}
228
229impl fmt::Display for SourceKind {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        // `pad` honors fill/align/width/precision; `write_str` would
232        // silently drop them.
233        f.pad(self.as_str())
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::SourceKind;
240
241    #[test]
242    fn display_honors_formatter_width_and_alignment() {
243        let k = SourceKind::MachineId;
244        assert_eq!(format!("[{k:<15}]"), "[machine-id     ]");
245        assert_eq!(format!("[{k:>15}]"), "[     machine-id]");
246        assert_eq!(format!("[{k:^15}]"), "[  machine-id   ]");
247        assert_eq!(format!("[{k:-<15}]"), "[machine-id-----]");
248        assert_eq!(format!("[{k:.5}]"), "[machi]");
249        assert_eq!(format!("[{k}]"), "[machine-id]");
250    }
251}