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}