kftray-portforward 0.30.1

KFtray library with port forwarding logic
Documentation
use anyhow::Context;
use k8s_openapi::api::core::v1::Pod;
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use serde::{
    Deserialize,
    Serialize,
};

impl NameSpace {
    pub fn name_any(&self) -> String {
        self.0.clone().unwrap_or_else(|| "default".to_string())
    }
}

impl From<i32> for Port {
    fn from(port: i32) -> Self {
        Self::Number(port)
    }
}

impl From<&str> for Port {
    fn from(port: &str) -> Self {
        Self::Name(port.to_string())
    }
}

impl From<IntOrString> for Port {
    fn from(port: IntOrString) -> Self {
        match port {
            IntOrString::Int(port) => Self::Number(port),
            IntOrString::String(port) => Self::Name(port),
        }
    }
}

impl TargetPod {
    pub fn new(pod_name: String, port_number: i32) -> anyhow::Result<Self> {
        let port_number = u16::try_from(port_number).context("Port not valid")?;

        Ok(Self {
            pod_name,
            port_number,
        })
    }
}
impl Target {
    pub fn new<I: Into<Option<T>>, T: Into<String>, P: Into<Port>>(
        selector: TargetSelector, port: P, namespace: I,
    ) -> Self {
        Self {
            selector,
            port: port.into(),
            namespace: NameSpace(namespace.into().map(Into::into)),
        }
    }

    pub fn find(&self, pod: &Pod, port: Option<Port>) -> anyhow::Result<TargetPod> {
        let port = port.as_ref().unwrap_or(&self.port);
        let pod_name = pod.metadata.name.as_ref().context("Pod Name is None")?;

        let port_number = match port {
            Port::Number(port) => *port,
            Port::Name(name) => {
                pod.spec
                    .as_ref()
                    .context("Pod Spec is None")?
                    .containers
                    .iter()
                    .find_map(|c| {
                        c.ports
                            .as_ref()?
                            .iter()
                            .find(|p| p.name.as_ref() == Some(name))
                    })
                    .context("Port not found")?
                    .container_port
            }
        };

        TargetPod::new(pod_name.clone(), port_number)
    }
}

#[derive(Serialize, Deserialize)]
pub struct KubeContextInfo {
    pub name: String,
}

#[derive(Serialize)]
pub struct KubeNamespaceInfo {
    pub name: String,
}

#[derive(Serialize)]
pub struct KubeServiceInfo {
    pub name: String,
}

#[derive(Serialize)]
pub struct KubeServicePortInfo {
    pub name: Option<String>,
    pub port: Option<IntOrString>,
}

#[derive(Serialize, Debug)]
pub struct PodInfo {
    pub labels_str: String,
}

#[derive(Clone)]
pub struct PortForward {
    pub target: Target,
    pub local_port: Option<u16>,
    pub local_address: Option<String>,
    pub context_name: Option<String>,
    pub kubeconfig: Option<String>,
    pub config_id: i64,
    pub workload_type: String,
    /// The API server the forward has to reach, when an earlier step of the
    /// same operation already resolved it: a relay created on one server must
    /// not be forwarded to on another the context has since come to mean.
    pub expected_destination: Option<String>,
}

#[derive(Clone, Debug, PartialEq)]
pub enum TargetSelector {
    ServiceName(String),
    PodLabel(String),
}

#[derive(Clone, Debug, PartialEq)]
pub enum Port {
    Number(i32),
    Name(String),
}

#[derive(Clone, Debug, PartialEq)]
pub struct Target {
    pub selector: TargetSelector,
    pub port: Port,
    pub namespace: NameSpace,
}

#[derive(Clone, Debug, PartialEq)]
pub struct NameSpace(pub Option<String>);

#[derive(Clone, Debug, PartialEq)]
pub struct TargetPod {
    pub pod_name: String,
    pub port_number: u16,
}

#[cfg(test)]
mod tests {
    use k8s_openapi::api::core::v1::{
        Container,
        ContainerPort,
        PodCondition,
        PodSpec,
        PodStatus,
    };
    use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;

    use super::*;

    fn create_test_pod(name: &str) -> Pod {
        Pod {
            metadata: ObjectMeta {
                name: Some(name.to_string()),
                ..Default::default()
            },
            spec: Some(PodSpec {
                containers: vec![Container {
                    name: "test-container".to_string(),
                    ports: Some(vec![
                        ContainerPort {
                            name: Some("http".to_string()),
                            container_port: 8080,
                            ..Default::default()
                        },
                        ContainerPort {
                            name: Some("grpc".to_string()),
                            container_port: 9090,
                            ..Default::default()
                        },
                    ]),
                    ..Default::default()
                }],
                ..Default::default()
            }),
            status: Some(PodStatus {
                conditions: Some(vec![PodCondition {
                    type_: "Ready".to_string(),
                    status: "True".to_string(),
                    ..Default::default()
                }]),
                ..Default::default()
            }),
        }
    }

    #[test]
    fn test_namespace_name_any() {
        let ns1 = NameSpace(Some("test-namespace".to_string()));
        let ns2 = NameSpace(None);

        assert_eq!(ns1.name_any(), "test-namespace");
        assert_eq!(ns2.name_any(), "default");
    }

    #[test]
    fn test_port_conversions() {
        let port1: Port = 8080.into();
        assert_eq!(port1, Port::Number(8080));

        let port2: Port = "http".into();
        assert_eq!(port2, Port::Name("http".to_string()));

        let port3: Port = IntOrString::Int(9090).into();
        assert_eq!(port3, Port::Number(9090));

        let port4: Port = IntOrString::String("grpc".to_string()).into();
        assert_eq!(port4, Port::Name("grpc".to_string()));
    }

    #[test]
    fn test_target_pod_new() {
        let result = TargetPod::new("test-pod".to_string(), 8080);
        assert!(result.is_ok());
        let pod = result.unwrap();
        assert_eq!(pod.pod_name, "test-pod");
        assert_eq!(pod.port_number, 8080);

        let result = TargetPod::new("test-pod".to_string(), 70000);
        assert!(result.is_err());
    }

    #[test]
    fn test_target_new() {
        let target = Target::new(
            TargetSelector::ServiceName("svc1".to_string()),
            8080,
            "default",
        );

        assert_eq!(
            target.selector,
            TargetSelector::ServiceName("svc1".to_string())
        );
        assert_eq!(target.port, Port::Number(8080));
        assert_eq!(target.namespace, NameSpace(Some("default".to_string())));

        let target_no_ns = Target::new::<Option<String>, String, &str>(
            TargetSelector::PodLabel("app=web".to_string()),
            "http",
            None,
        );

        assert_eq!(
            target_no_ns.selector,
            TargetSelector::PodLabel("app=web".to_string())
        );
        assert_eq!(target_no_ns.port, Port::Name("http".to_string()));
        assert_eq!(target_no_ns.namespace, NameSpace(None));
    }

    #[test]
    fn test_target_find() {
        let pod = create_test_pod("test-pod");

        let target1 = Target::new(
            TargetSelector::PodLabel("app=web".to_string()),
            8080,
            "default",
        );

        let result = target1.find(&pod, None);
        assert!(result.is_ok());
        let target_pod = result.unwrap();
        assert_eq!(target_pod.pod_name, "test-pod");
        assert_eq!(target_pod.port_number, 8080);

        let target2 = Target::new(
            TargetSelector::PodLabel("app=web".to_string()),
            "http",
            "default",
        );

        let result = target2.find(&pod, None);
        assert!(result.is_ok());
        let target_pod = result.unwrap();
        assert_eq!(target_pod.pod_name, "test-pod");
        assert_eq!(target_pod.port_number, 8080);

        let target3 = Target::new(
            TargetSelector::PodLabel("app=web".to_string()),
            "grpc",
            "default",
        );

        let result = target3.find(&pod, None);
        assert!(result.is_ok());
        let target_pod = result.unwrap();
        assert_eq!(target_pod.pod_name, "test-pod");
        assert_eq!(target_pod.port_number, 9090);

        let target4 = Target::new(
            TargetSelector::PodLabel("app=web".to_string()),
            "http",
            "default",
        );

        let result = target4.find(&pod, Some(Port::Number(5000)));
        assert!(result.is_ok());
        let target_pod = result.unwrap();
        assert_eq!(target_pod.pod_name, "test-pod");
        assert_eq!(target_pod.port_number, 5000);

        let target5 = Target::new(
            TargetSelector::PodLabel("app=web".to_string()),
            "nonexistent",
            "default",
        );

        let result = target5.find(&pod, None);
        assert!(result.is_err());
    }
}