host_identity/sources/generic.rs
1//! Portable sources: overrides and custom callbacks.
2//!
3//! # Identity scope
4//!
5//! `EnvOverride`, `FileOverride`, and `FnSource` are **caller-scoped**:
6//! whoever sets the variable, writes the file, or supplies the closure
7//! picks the scope. Setting `HOST_IDENTITY` in a host-level systemd
8//! unit gives host scope to every container that inherits the
9//! environment; setting the same variable in a Kubernetes pod spec
10//! (via `valueFrom.fieldRef.fieldPath: metadata.uid`) gives per-pod
11//! scope. The crate cannot tell which you intended — pick
12//! intentionally.
13//!
14//! For per-pod identity sourced from a projected file, prefer the
15//! purpose-built [`crate::sources::KubernetesDownwardApi`] over
16//! wiring `FileOverride` at a downward-API path: the k8s source
17//! labels the probe with `SourceKind::KubernetesDownwardApi` so the
18//! resulting [`crate::HostId`] records the correct provenance.
19//! Reserve `FileOverride` for genuinely caller-pinned paths (a
20//! per-instance volume, an HSM-written file, a provisioning agent's
21//! output).
22//!
23//! See `docs/algorithm.md` → "Identity scope" for the host-vs-
24//! container trap these sources are most often used to work around.
25
26use std::path::{Path, PathBuf};
27
28use crate::error::Error;
29use crate::source::{Probe, Source, SourceKind};
30use crate::sources::util::{normalize, read_capped};
31
32/// Read the identifier from an environment variable.
33///
34/// Skipped (`Ok(None)`) when the variable is unset, empty, or holds only
35/// whitespace.
36#[derive(Debug, Clone)]
37pub struct EnvOverride {
38 var: String,
39}
40
41impl EnvOverride {
42 /// Read from the named environment variable.
43 #[must_use]
44 pub fn new(var: impl Into<String>) -> Self {
45 Self { var: var.into() }
46 }
47}
48
49impl Source for EnvOverride {
50 fn kind(&self) -> SourceKind {
51 SourceKind::EnvOverride
52 }
53
54 fn probe(&self) -> Result<Option<Probe>, Error> {
55 match std::env::var(&self.var) {
56 Ok(value) => Ok(normalize(&value).map(|v| Probe::new(SourceKind::EnvOverride, v))),
57 Err(_) => Ok(None),
58 }
59 }
60}
61
62/// Read the identifier from a single-line file.
63///
64/// Skipped when the file does not exist, is empty, or contains only
65/// whitespace. Returns an I/O error for permission problems or other
66/// unexpected read failures.
67#[derive(Debug, Clone)]
68pub struct FileOverride {
69 path: PathBuf,
70}
71
72impl FileOverride {
73 /// Read from the given file path.
74 #[must_use]
75 pub fn new(path: impl Into<PathBuf>) -> Self {
76 Self { path: path.into() }
77 }
78
79 /// The configured path.
80 #[must_use]
81 pub fn path(&self) -> &Path {
82 &self.path
83 }
84}
85
86impl Source for FileOverride {
87 fn kind(&self) -> SourceKind {
88 SourceKind::FileOverride
89 }
90
91 fn probe(&self) -> Result<Option<Probe>, Error> {
92 match read_capped(&self.path) {
93 Ok(content) => Ok(normalize(&content).map(|v| Probe::new(SourceKind::FileOverride, v))),
94 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
95 Err(source) => Err(Error::Io {
96 source_kind: SourceKind::FileOverride,
97 path: self.path.clone(),
98 source,
99 }),
100 }
101 }
102}
103
104/// A [`Source`] backed by a user-supplied closure.
105///
106/// Use to plug in arbitrary identity providers (cloud metadata, Kubernetes
107/// downward API, hardware security modules, …) without implementing the
108/// trait manually.
109pub struct FnSource<F> {
110 kind: SourceKind,
111 f: F,
112}
113
114impl<F> FnSource<F>
115where
116 F: Fn() -> Result<Option<String>, Error> + Send + Sync,
117{
118 /// Build a source from a closure that returns an optional raw identifier.
119 pub fn new(kind: SourceKind, f: F) -> Self {
120 Self { kind, f }
121 }
122}
123
124impl<F> std::fmt::Debug for FnSource<F> {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct("FnSource")
127 .field("kind", &self.kind)
128 .finish_non_exhaustive()
129 }
130}
131
132impl<F> Source for FnSource<F>
133where
134 F: Fn() -> Result<Option<String>, Error> + Send + Sync,
135{
136 fn kind(&self) -> SourceKind {
137 self.kind
138 }
139
140 fn probe(&self) -> Result<Option<Probe>, Error> {
141 Ok((self.f)()?
142 .as_deref()
143 .and_then(normalize)
144 .map(|v| Probe::new(self.kind, v)))
145 }
146}