Skip to main content

agentd/
identity.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! Instance identity from the Kubernetes downward API.
3//!
4//! agentd surfaces pod identity by reading **operator-injected environment
5//! variables** (set from `valueFrom.fieldRef`) — never by calling the kube API.
6//! There is no kube client, no in-cluster config, no service-account read; that
7//! coupling belongs in agentctl, not here (the minimalism moat). Env in,
8//! identity out.
9//!
10//! Every k8s field is optional and descriptive, never load-bearing: outside
11//! Kubernetes the vars are simply unset and the fields are `None`. Their absence
12//! is never a config error. `run_id` is always present (minted by config when
13//! unset), because every log line and durable record correlates on it.
14
15/// The instance's correlation identity. `run_id` is always present; the k8s
16/// fields are populated from the downward-API env when injected, else `None`.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Identity {
19    /// `AGENTD_RUN_ID`, or the run id minted at startup. Always present.
20    pub run_id: String,
21    /// `metadata.name` via `AGENTD_POD_NAME`.
22    pub instance: Option<String>,
23    /// `metadata.uid` via `AGENTD_POD_UID`.
24    pub uid: Option<String>,
25    /// `metadata.namespace` via `AGENTD_POD_NAMESPACE`.
26    pub namespace: Option<String>,
27    /// `spec.nodeName` via `AGENTD_NODE_NAME`.
28    pub node: Option<String>,
29}
30
31impl Identity {
32    /// Read identity from the environment (getenv only — no syscalls beyond
33    /// that, no validation side effects). `run_id` comes from the already
34    /// resolved config; the k8s fields each read their downward-API var and
35    /// resolve to `None` when absent.
36    pub fn from_env(run_id: &str) -> Identity {
37        // The neutral `AGENT_*` downward-API vars are read FIRST, with the
38        // branded `AGENTD_*` spelling as a fallback, so a manifest may inject
39        // either. `var()` coerces empty to unset, so an empty neutral var also
40        // falls through to the branded one rather than masking it.
41        Identity {
42            run_id: run_id.to_string(),
43            instance: var("AGENT_POD_NAME").or_else(|| var("AGENTD_POD_NAME")),
44            uid: var("AGENT_POD_UID").or_else(|| var("AGENTD_POD_UID")),
45            namespace: var("AGENT_POD_NAMESPACE").or_else(|| var("AGENTD_POD_NAMESPACE")),
46            node: var("AGENT_NODE_NAME").or_else(|| var("AGENTD_NODE_NAME")),
47        }
48    }
49}
50
51/// A non-empty environment variable, or `None`. An empty value is treated as
52/// unset (an operator clearing a fieldRef should not surface as `""`).
53fn var(key: &str) -> Option<String> {
54    std::env::var(key).ok().filter(|v| !v.is_empty())
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    // The downward-API vars are process-global, so the env-present and
62    // env-absent cases share one test to avoid cross-test races.
63    #[test]
64    fn from_env_populates_when_set_and_none_when_absent() {
65        // The downward-API vars (branded + neutral) are process-global, so the
66        // present/absent/branded/neutral cases share one test to avoid races.
67        let all = [
68            "AGENT_POD_NAME",
69            "AGENT_POD_UID",
70            "AGENT_POD_NAMESPACE",
71            "AGENT_NODE_NAME",
72            "AGENTD_POD_NAME",
73            "AGENTD_POD_UID",
74            "AGENTD_POD_NAMESPACE",
75            "AGENTD_NODE_NAME",
76        ];
77        // Absent: with every spelling unset, each k8s field is None; run_id holds.
78        for k in all {
79            unsafe { std::env::remove_var(k) };
80        }
81        let id = Identity::from_env("run-abc");
82        assert_eq!(id.run_id, "run-abc");
83        assert_eq!(id.instance, None);
84        assert_eq!(id.uid, None);
85        assert_eq!(id.namespace, None);
86        assert_eq!(id.node, None);
87
88        // Branded present: each `AGENTD_*` var maps to its field via the
89        // branded fallback (no neutral var set).
90        unsafe {
91            std::env::set_var("AGENTD_POD_NAME", "agent-pod-abc");
92            std::env::set_var("AGENTD_POD_UID", "f3c1-uid");
93            std::env::set_var("AGENTD_POD_NAMESPACE", "agents");
94            std::env::set_var("AGENTD_NODE_NAME", "node-3");
95        }
96        let id = Identity::from_env("run-xyz");
97        assert_eq!(id.run_id, "run-xyz");
98        assert_eq!(id.instance.as_deref(), Some("agent-pod-abc"));
99        assert_eq!(id.uid.as_deref(), Some("f3c1-uid"));
100        assert_eq!(id.namespace.as_deref(), Some("agents"));
101        assert_eq!(id.node.as_deref(), Some("node-3"));
102
103        // Neutral present, branded cleared: the `AGENT_*` spelling is accepted
104        // on its own.
105        for k in [
106            "AGENTD_POD_NAME",
107            "AGENTD_POD_UID",
108            "AGENTD_POD_NAMESPACE",
109            "AGENTD_NODE_NAME",
110        ] {
111            unsafe { std::env::remove_var(k) };
112        }
113        unsafe {
114            std::env::set_var("AGENT_POD_NAME", "neutral-pod");
115            std::env::set_var("AGENT_POD_UID", "neutral-uid");
116            std::env::set_var("AGENT_POD_NAMESPACE", "neutral-ns");
117            std::env::set_var("AGENT_NODE_NAME", "neutral-node");
118        }
119        let id = Identity::from_env("r");
120        assert_eq!(id.instance.as_deref(), Some("neutral-pod"));
121        assert_eq!(id.uid.as_deref(), Some("neutral-uid"));
122        assert_eq!(id.namespace.as_deref(), Some("neutral-ns"));
123        assert_eq!(id.node.as_deref(), Some("neutral-node"));
124
125        // Both present ⇒ neutral-first wins (the read order is AGENT_* then AGENTD_*).
126        unsafe { std::env::set_var("AGENTD_POD_NAME", "branded-pod") };
127        assert_eq!(
128            Identity::from_env("r").instance.as_deref(),
129            Some("neutral-pod")
130        );
131
132        // An empty neutral var reads as unset and falls through to the branded one.
133        unsafe { std::env::set_var("AGENT_POD_NAME", "") };
134        assert_eq!(
135            Identity::from_env("r").instance.as_deref(),
136            Some("branded-pod")
137        );
138        // …and with BOTH empty/unset, the field is None.
139        unsafe { std::env::remove_var("AGENTD_POD_NAME") };
140        assert_eq!(Identity::from_env("r").instance, None);
141
142        for k in all {
143            unsafe { std::env::remove_var(k) };
144        }
145    }
146
147    #[test]
148    fn run_id_is_always_present() {
149        let id = Identity::from_env("only-run-id");
150        assert_eq!(id.run_id, "only-run-id");
151    }
152}