Skip to main content

tatara_lisp_script/stdlib/
kube.rs

1//! Kubernetes auth + URL helpers.
2//!
3//! Tatara-script doesn't bundle a full kube-rs equivalent — instead it
4//! exposes the auth primitives a script needs to call the K8s API via
5//! the existing `http-get-json` / `http-post-json` stdlib. This keeps
6//! the binary size small and follows the Unix-philosophy "small,
7//! composable primitives" the user named.
8//!
9//! Surface:
10//!
11//!   (kube-in-cluster?)            → bool — running inside a Pod?
12//!   (kube-bearer-token)           → string — service account token
13//!   (kube-ca-cert)                → string — cluster CA cert (PEM)
14//!   (kube-namespace)              → string — pod's own namespace
15//!   (kube-api-base)               → string — "https://kubernetes.default.svc:443"
16//!
17//! Use together with http-get-json:
18//!
19//!   (define ns (kube-namespace))
20//!   (define url (string-append (kube-api-base)
21//!                              "/api/v1/namespaces/"
22//!                              ns
23//!                              "/configmaps"))
24//!   (define headers (list (list "Authorization"
25//!                               (string-append "Bearer " (kube-bearer-token)))))
26//!   (define cms (http-get-json url headers))
27
28use std::sync::Arc;
29
30use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
31
32use crate::script_ctx::ScriptCtx;
33
34const SA_DIR: &str = "/var/run/secrets/kubernetes.io/serviceaccount";
35
36pub fn install(interp: &mut Interpreter<ScriptCtx>) {
37    interp.register_fn(
38        "kube-in-cluster?",
39        Arity::Exact(0),
40        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| Ok(Value::Bool(in_cluster())),
41    );
42
43    interp.register_fn(
44        "kube-bearer-token",
45        Arity::Exact(0),
46        |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
47            read_sa_file("token")
48                .map(|s| Value::Str(Arc::from(s.trim_end().to_string())))
49                .map_err(|e| {
50                    EvalError::native_fn("kube-bearer-token", format!("read SA token: {e}"), sp)
51                })
52        },
53    );
54
55    interp.register_fn(
56        "kube-ca-cert",
57        Arity::Exact(0),
58        |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
59            read_sa_file("ca.crt")
60                .map(|s| Value::Str(Arc::from(s)))
61                .map_err(|e| {
62                    EvalError::native_fn("kube-ca-cert", format!("read SA ca.crt: {e}"), sp)
63                })
64        },
65    );
66
67    interp.register_fn(
68        "kube-namespace",
69        Arity::Exact(0),
70        |_args: &[Value], _ctx: &mut ScriptCtx, sp| {
71            // Order: env var (downward API) → SA namespace file → "default".
72            if let Ok(ns) = std::env::var("POD_NAMESPACE") {
73                return Ok(Value::Str(Arc::from(ns)));
74            }
75            match read_sa_file("namespace") {
76                Ok(s) => Ok(Value::Str(Arc::from(s.trim_end().to_string()))),
77                Err(_) => {
78                    if !in_cluster() {
79                        return Ok(Value::Str(Arc::from("default")));
80                    }
81                    Err(EvalError::native_fn(
82                        "kube-namespace",
83                        "no namespace from env or SA file",
84                        sp,
85                    ))
86                }
87            }
88        },
89    );
90
91    interp.register_fn(
92        "kube-api-base",
93        Arity::Exact(0),
94        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
95            // Override via env (e.g. local dev with kubectl proxy on 8001).
96            if let Ok(url) = std::env::var("KUBE_API_BASE") {
97                return Ok(Value::Str(Arc::from(url)));
98            }
99            // In-cluster default — works because of automatic
100            // kubernetes.default.svc DNS + auto-mounted CA cert.
101            let host = std::env::var("KUBERNETES_SERVICE_HOST")
102                .unwrap_or_else(|_| "kubernetes.default.svc".to_string());
103            let port =
104                std::env::var("KUBERNETES_SERVICE_PORT").unwrap_or_else(|_| "443".to_string());
105            Ok(Value::Str(Arc::from(format!("https://{host}:{port}"))))
106        },
107    );
108
109    interp.register_fn(
110        "kube-pod-name",
111        Arity::Exact(0),
112        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
113            // Downward API — operator must mount POD_NAME into the env.
114            // Falls back to hostname (the kubelet sets it to the Pod name
115            // by default for non-hostNetwork pods).
116            if let Ok(pod) = std::env::var("POD_NAME") {
117                return Ok(Value::Str(Arc::from(pod)));
118            }
119            let host = hostname_safe();
120            Ok(Value::Str(Arc::from(host)))
121        },
122    );
123
124    interp.register_fn(
125        "kube-cluster-name",
126        Arity::Exact(0),
127        |_args: &[Value], _ctx: &mut ScriptCtx, _sp| {
128            // Cluster name is purely operator-supplied via env (no
129            // canonical kubelet API surface for it). Default to "unknown"
130            // so programs work locally without breaking.
131            Ok(Value::Str(Arc::from(
132                std::env::var("CLUSTER_NAME").unwrap_or_else(|_| "unknown".to_string()),
133            )))
134        },
135    );
136}
137
138fn read_sa_file(name: &str) -> Result<String, std::io::Error> {
139    let path = format!("{SA_DIR}/{name}");
140    std::fs::read_to_string(path)
141}
142
143fn in_cluster() -> bool {
144    std::path::Path::new(SA_DIR).is_dir() && std::fs::metadata(format!("{SA_DIR}/token")).is_ok()
145}
146
147fn hostname_safe() -> String {
148    // Best-effort. Read /etc/hostname (Linux container default) or
149    // env HOSTNAME.
150    if let Ok(s) = std::fs::read_to_string("/etc/hostname") {
151        return s.trim_end().to_string();
152    }
153    std::env::var("HOSTNAME").unwrap_or_else(|_| "unknown".to_string())
154}