Skip to main content

fakecloud_k8s/
backend.rs

1//! Backend selection: Docker (default) vs native Kubernetes Pods.
2//!
3//! A FakeCloud server can run every container-backed service on Docker
4//! (the default) or on Kubernetes. The choice is per-service with a
5//! global fallback:
6//!
7//! - `FAKECLOUD_<SERVICE>_BACKEND=k8s|docker` — explicit per-service
8//!   override (e.g. `FAKECLOUD_ELASTICACHE_BACKEND`,
9//!   `FAKECLOUD_LAMBDA_BACKEND`). An explicit value always wins.
10//! - `FAKECLOUD_CONTAINER_BACKEND=k8s|docker` — global default applied
11//!   to any service whose own variable is unset.
12//! - Unset everywhere -> [`Backend::Docker`].
13//!
14//! This lets an operator flip the whole stack to Kubernetes with one
15//! variable while still pinning an individual service back to Docker.
16
17/// Which execution backend a service should use.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Backend {
20    /// Shell out to a local Docker/Podman daemon (the default).
21    Docker,
22    /// Spawn native Pods in a Kubernetes cluster.
23    K8s,
24}
25
26/// The global default backend variable, consulted when a service's own
27/// variable is unset.
28pub const GLOBAL_BACKEND_ENV: &str = "FAKECLOUD_CONTAINER_BACKEND";
29
30/// Resolve the backend for a service given the name of its per-service
31/// override variable (e.g. `"FAKECLOUD_ELASTICACHE_BACKEND"`).
32///
33/// An explicit per-service value wins over the global default; only
34/// `k8s` and `docker` are recognized (case-insensitive), and any other
35/// value is ignored (treated as unset) so a typo can't silently leave a
36/// service on an unexpected backend without falling through to the
37/// documented precedence.
38pub fn backend_choice(per_service_env: &str) -> Backend {
39    match read_choice(per_service_env) {
40        Some(b) => b,
41        None => read_choice(GLOBAL_BACKEND_ENV).unwrap_or(Backend::Docker),
42    }
43}
44
45/// Parse a single backend variable. Returns `None` when unset or set to
46/// an unrecognized value so the caller can fall through to the next
47/// level of precedence.
48fn read_choice(var: &str) -> Option<Backend> {
49    match std::env::var(var)
50        .ok()?
51        .trim()
52        .to_ascii_lowercase()
53        .as_str()
54    {
55        "k8s" | "kubernetes" => Some(Backend::K8s),
56        "docker" | "podman" => Some(Backend::Docker),
57        _ => None,
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use std::sync::Mutex;
65
66    // Env vars are process-global; serialize the tests that mutate them.
67    static ENV_LOCK: Mutex<()> = Mutex::new(());
68
69    /// Restores saved env vars on drop, so a panicking test closure can't
70    /// leak its mutated environment into the next test.
71    struct EnvGuard(Vec<(String, Option<String>)>);
72    impl Drop for EnvGuard {
73        fn drop(&mut self) {
74            for (k, v) in &self.0 {
75                match v {
76                    Some(v) => std::env::set_var(k, v),
77                    None => std::env::remove_var(k),
78                }
79            }
80        }
81    }
82
83    fn with_env(vars: &[(&str, Option<&str>)], f: impl FnOnce()) {
84        // Tolerate a poisoned lock from an earlier panicking test — the
85        // EnvGuard already restored the environment, so the data is sound.
86        let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
87        let _restore = EnvGuard(
88            vars.iter()
89                .map(|(k, _)| (k.to_string(), std::env::var(k).ok()))
90                .collect(),
91        );
92        for (k, v) in vars {
93            match v {
94                Some(v) => std::env::set_var(k, v),
95                None => std::env::remove_var(k),
96            }
97        }
98        f();
99    }
100
101    const SVC: &str = "FAKECLOUD_ELASTICACHE_BACKEND";
102
103    #[test]
104    fn defaults_to_docker_when_unset() {
105        with_env(&[(SVC, None), (GLOBAL_BACKEND_ENV, None)], || {
106            assert_eq!(backend_choice(SVC), Backend::Docker);
107        });
108    }
109
110    #[test]
111    fn per_service_k8s_wins() {
112        with_env(&[(SVC, Some("k8s")), (GLOBAL_BACKEND_ENV, None)], || {
113            assert_eq!(backend_choice(SVC), Backend::K8s);
114        });
115    }
116
117    #[test]
118    fn global_k8s_applies_when_service_unset() {
119        with_env(&[(SVC, None), (GLOBAL_BACKEND_ENV, Some("k8s"))], || {
120            assert_eq!(backend_choice(SVC), Backend::K8s);
121        });
122    }
123
124    #[test]
125    fn per_service_docker_overrides_global_k8s() {
126        with_env(
127            &[(SVC, Some("docker")), (GLOBAL_BACKEND_ENV, Some("k8s"))],
128            || {
129                assert_eq!(backend_choice(SVC), Backend::Docker);
130            },
131        );
132    }
133
134    #[test]
135    fn case_insensitive_and_trimmed() {
136        with_env(
137            &[(SVC, Some("  K8s  ")), (GLOBAL_BACKEND_ENV, None)],
138            || {
139                assert_eq!(backend_choice(SVC), Backend::K8s);
140            },
141        );
142    }
143
144    #[test]
145    fn unrecognized_service_value_falls_through_to_global() {
146        with_env(
147            &[(SVC, Some("banana")), (GLOBAL_BACKEND_ENV, Some("k8s"))],
148            || {
149                assert_eq!(backend_choice(SVC), Backend::K8s);
150            },
151        );
152    }
153
154    #[test]
155    fn unrecognized_everywhere_is_docker() {
156        with_env(
157            &[(SVC, Some("banana")), (GLOBAL_BACKEND_ENV, Some("nope"))],
158            || {
159                assert_eq!(backend_choice(SVC), Backend::Docker);
160            },
161        );
162    }
163}