#![warn(missing_docs, clippy::missing_errors_doc)]
use derive_more::Debug;
use k8s_openapi::api::core::v1::Pod;
use kube::{Api, Client, api::ListParams};
use std::{
net::{IpAddr, SocketAddr},
num::NonZeroU16,
};
use tellus::cluster::SeedDiscovery;
use thiserror::Error;
use tracing::warn;
#[derive(Debug)]
pub struct K8sSeeds {
#[debug(skip)]
api: Api<Pod>,
pods: Pods,
}
impl K8sSeeds {
pub async fn new(pods: Pods) -> Result<Self, K8sSeedsError> {
validate(&pods)?;
let client = Client::try_default().await?;
Self::with_client(pods, client)
}
pub fn with_client(pods: Pods, client: Client) -> Result<Self, K8sSeedsError> {
validate(&pods)?;
let api = match &pods.namespace {
Some(namespace) => Api::namespaced(client, namespace),
None => Api::default_namespaced(client),
};
Ok(Self { api, pods })
}
}
impl SeedDiscovery for K8sSeeds {
type Error = K8sSeedsError;
async fn resolve(&mut self) -> Result<Vec<SocketAddr>, K8sSeedsError> {
let params = ListParams::default().labels(&self.pods.label_selector);
let pods = self.api.list(¶ms).await?;
let addrs = pods
.items
.iter()
.filter(|pod| live(pod))
.filter_map(|pod| addr(pod, &self.pods.port))
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(K8sSeedsError::NoAddresses {
selector: self.pods.label_selector.clone(),
})
} else {
Ok(addrs)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize),
serde(try_from = "UncheckedPods")
)]
pub struct Pods {
pub namespace: Option<String>,
pub label_selector: String,
pub port: Port,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize),
serde(rename_all = "snake_case", deny_unknown_fields)
)]
pub enum Port {
Number(NonZeroU16),
Name(String),
}
#[derive(Debug, Error)]
pub enum InvalidPods {
#[error("label_selector is blank")]
BlankLabelSelector,
#[error("namespace is blank")]
BlankNamespace,
#[error("port name is blank")]
BlankPortName,
}
#[derive(Debug, Error)]
pub enum K8sSeedsError {
#[error(transparent)]
Config(#[from] InvalidPods),
#[error(transparent)]
Client(#[from] kube::Error),
#[error("no pod address for selector {selector}")]
NoAddresses {
selector: String,
},
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct UncheckedPods {
#[serde(default)]
namespace: Option<String>,
label_selector: String,
port: Port,
}
#[cfg(feature = "serde")]
impl TryFrom<UncheckedPods> for Pods {
type Error = InvalidPods;
fn try_from(unchecked: UncheckedPods) -> Result<Self, Self::Error> {
let pods = Self {
namespace: unchecked.namespace,
label_selector: unchecked.label_selector,
port: unchecked.port,
};
validate(&pods)?;
Ok(pods)
}
}
fn validate(pods: &Pods) -> Result<(), InvalidPods> {
if pods.label_selector.trim().is_empty() {
return Err(InvalidPods::BlankLabelSelector);
}
if pods
.namespace
.as_ref()
.is_some_and(|namespace| namespace.trim().is_empty())
{
return Err(InvalidPods::BlankNamespace);
}
if matches!(&pods.port, Port::Name(name) if name.trim().is_empty()) {
return Err(InvalidPods::BlankPortName);
}
Ok(())
}
fn live(pod: &Pod) -> bool {
let phase = pod
.status
.as_ref()
.and_then(|status| status.phase.as_deref());
pod.metadata.deletion_timestamp.is_none() && !matches!(phase, Some("Succeeded" | "Failed"))
}
fn addr(pod: &Pod, port: &Port) -> Option<SocketAddr> {
let ip = ip(pod)?;
let port = match port {
Port::Number(number) => *number,
Port::Name(name) => container_port(pod, name)?,
};
Some(SocketAddr::new(ip, port.get()))
}
fn ip(pod: &Pod) -> Option<IpAddr> {
let status = pod.status.as_ref()?;
let ip = status.pod_ip.as_deref().or_else(|| {
status
.pod_ips
.as_ref()
.and_then(|ips| ips.first())
.map(|ip| ip.ip.as_str())
})?;
match ip.parse() {
Ok(ip) => Some(ip),
Err(error) => {
warn!(pod = name(pod), ip, %error, "cannot parse the pod IP");
None
}
}
}
fn container_port(pod: &Pod, port_name: &str) -> Option<NonZeroU16> {
let port = pod
.spec
.as_ref()
.into_iter()
.flat_map(|spec| spec.containers.iter())
.flat_map(|container| container.ports.iter().flatten())
.find(|port| port.name.as_deref() == Some(port_name));
match port {
Some(port) => {
let number = u16::try_from(port.container_port)
.ok()
.and_then(NonZeroU16::new);
if number.is_none() {
warn!(
pod = name(pod),
port_name,
container_port = port.container_port,
"container port out of range"
);
}
number
}
None => {
warn!(pod = name(pod), port_name, "no container port of this name");
None
}
}
}
fn name(pod: &Pod) -> &str {
pod.metadata.name.as_deref().unwrap_or("<unnamed>")
}
#[cfg(all(test, feature = "serde"))]
mod tests {
use crate::{Pods, Port};
use std::num::NonZeroU16;
#[test]
fn pods_deserialize_from_their_documented_form() {
let pods = serde_json::from_str::<Pods>(
r#"{ "label_selector": "app=tellus", "port": { "number": 7878 } }"#,
)
.expect("the numbered port form deserializes");
assert_eq!(
pods,
Pods {
namespace: None,
label_selector: "app=tellus".to_string(),
port: Port::Number(NonZeroU16::new(7_878).expect("7878 is not zero")),
}
);
let pods = serde_json::from_str::<Pods>(
r#"{ "namespace": "tellus", "label_selector": "app=tellus", "port": { "name": "tellus" } }"#,
)
.expect("the named port form deserializes");
assert_eq!(
pods,
Pods {
namespace: Some("tellus".to_string()),
label_selector: "app=tellus".to_string(),
port: Port::Name("tellus".to_string()),
}
);
assert!(
serde_json::from_str::<Pods>(
r#"{ "label_selector": "app=tellus", "port": { "number": 0 } }"#
)
.is_err()
);
assert!(
serde_json::from_str::<Pods>(
r#"{ "label_selector": " ", "port": { "number": 7878 } }"#
)
.is_err()
);
assert!(
serde_json::from_str::<Pods>(
r#"{ "namespace": "", "label_selector": "app=tellus", "port": { "number": 7878 } }"#
)
.is_err()
);
assert!(
serde_json::from_str::<Pods>(
r#"{ "label_selector": "app=tellus", "port": { "name": "" } }"#
)
.is_err()
);
assert!(
serde_json::from_str::<Pods>(
r#"{ "lable_selector": "app=tellus", "port": { "number": 7878 } }"#
)
.is_err()
);
}
}