#[derive(Debug, Clone)]
pub struct K8sEnv {
pub namespace: String,
pub self_url: String,
pub self_host: String,
pub self_port: u16,
pub ecr_host: String,
pub ecr_port: u16,
pub pull_secret: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum K8sEnvError {
#[error("FAKECLOUD_K8S_SELF_URL must be set when using the Kubernetes backend")]
MissingSelfUrl,
#[error("FAKECLOUD_K8S_SELF_URL is not a valid URL: {0}")]
InvalidSelfUrl(String),
#[error("FAKECLOUD_K8S_ECR_URL is not a valid URL: {0}")]
InvalidEcrUrl(String),
}
impl K8sEnv {
pub fn from_env(default_port: u16) -> Result<Self, K8sEnvError> {
let self_url =
std::env::var("FAKECLOUD_K8S_SELF_URL").map_err(|_| K8sEnvError::MissingSelfUrl)?;
let parsed = reqwest::Url::parse(&self_url)
.map_err(|e| K8sEnvError::InvalidSelfUrl(e.to_string()))?;
let self_host = parsed
.host_str()
.ok_or_else(|| K8sEnvError::InvalidSelfUrl("missing host".into()))?
.to_string();
let self_port = parsed.port().unwrap_or(default_port);
let (ecr_host, ecr_port) = match std::env::var("FAKECLOUD_K8S_ECR_URL").ok() {
Some(raw) => {
let u = reqwest::Url::parse(&raw)
.map_err(|e| K8sEnvError::InvalidEcrUrl(e.to_string()))?;
let h = u
.host_str()
.ok_or_else(|| K8sEnvError::InvalidEcrUrl("missing host".into()))?
.to_string();
let p = u.port().unwrap_or(default_port);
(h, p)
}
None => (self_host.clone(), self_port),
};
let namespace = std::env::var("FAKECLOUD_K8S_NAMESPACE")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "default".to_string());
let pull_secret = std::env::var("FAKECLOUD_K8S_PULL_SECRET")
.ok()
.filter(|s| !s.trim().is_empty());
Ok(Self {
namespace,
self_url,
self_host,
self_port,
ecr_host,
ecr_port,
pull_secret,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const VARS: &[&str] = &[
"FAKECLOUD_K8S_SELF_URL",
"FAKECLOUD_K8S_ECR_URL",
"FAKECLOUD_K8S_NAMESPACE",
"FAKECLOUD_K8S_PULL_SECRET",
];
struct EnvGuard(Vec<(String, Option<String>)>);
impl Drop for EnvGuard {
fn drop(&mut self) {
for (k, v) in &self.0 {
match v {
Some(v) => std::env::set_var(k, v),
None => std::env::remove_var(k),
}
}
}
}
fn with_env(set: &[(&str, &str)], f: impl FnOnce()) {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let _restore = EnvGuard(
VARS.iter()
.map(|k| (k.to_string(), std::env::var(k).ok()))
.collect(),
);
for k in VARS {
std::env::remove_var(k);
}
for (k, v) in set {
std::env::set_var(k, v);
}
f();
}
#[test]
fn missing_self_url_errors() {
with_env(&[], || {
assert!(matches!(
K8sEnv::from_env(4566),
Err(K8sEnvError::MissingSelfUrl)
));
});
}
#[test]
fn portless_url_falls_back_to_default_port_not_scheme_default() {
with_env(
&[("FAKECLOUD_K8S_SELF_URL", "http://fakecloud.svc")],
|| {
let env = K8sEnv::from_env(4566).unwrap();
assert_eq!(env.self_port, 4566);
assert_eq!(env.self_host, "fakecloud.svc");
assert_eq!(env.ecr_host, "fakecloud.svc");
assert_eq!(env.ecr_port, 4566);
},
);
}
#[test]
fn explicit_port_is_respected() {
with_env(
&[("FAKECLOUD_K8S_SELF_URL", "http://fakecloud.svc:4566")],
|| {
let env = K8sEnv::from_env(9999).unwrap();
assert_eq!(env.self_port, 4566);
},
);
}
#[test]
fn ecr_url_overrides_and_defaults_namespace_pull_secret() {
with_env(
&[
("FAKECLOUD_K8S_SELF_URL", "http://fakecloud.svc:4566"),
("FAKECLOUD_K8S_ECR_URL", "http://registry.svc:5000"),
("FAKECLOUD_K8S_NAMESPACE", "fc"),
("FAKECLOUD_K8S_PULL_SECRET", "ecr-secret"),
],
|| {
let env = K8sEnv::from_env(4566).unwrap();
assert_eq!(env.ecr_host, "registry.svc");
assert_eq!(env.ecr_port, 5000);
assert_eq!(env.namespace, "fc");
assert_eq!(env.pull_secret.as_deref(), Some("ecr-secret"));
},
);
}
#[test]
fn empty_namespace_and_pull_secret_treated_as_unset() {
with_env(
&[
("FAKECLOUD_K8S_SELF_URL", "http://fakecloud.svc:4566"),
("FAKECLOUD_K8S_NAMESPACE", " "),
("FAKECLOUD_K8S_PULL_SECRET", ""),
],
|| {
let env = K8sEnv::from_env(4566).unwrap();
assert_eq!(env.namespace, "default");
assert_eq!(env.pull_secret, None);
},
);
}
#[test]
fn namespace_defaults_to_default() {
with_env(
&[("FAKECLOUD_K8S_SELF_URL", "http://fakecloud.svc:4566")],
|| {
assert_eq!(K8sEnv::from_env(4566).unwrap().namespace, "default");
},
);
}
}