use anyhow::{Result, bail};
use std::process::Command;
use crate::network::kubeconfig::resolve_kube_context;
use crate::network::types::ProviderSpec;
pub(crate) fn build_command(
spec: &ProviderSpec,
local_host: &str,
local_port: u16,
) -> Result<Command> {
match spec {
ProviderSpec::Kubectl {
cluster_endpoint,
context_selector,
namespace,
kind,
name,
remote_port,
pod_running_timeout,
} => {
let context = resolve_kube_context(cluster_endpoint, Some(context_selector.as_str()))?;
let mut cmd = Command::new("kubectl");
cmd.arg("--context")
.arg(context)
.arg("-n")
.arg(namespace)
.arg("port-forward")
.arg(format!("{kind}/{name}"))
.arg(format!("{local_port}:{remote_port}"))
.arg("--address")
.arg(local_host);
if let Some(timeout) = pod_running_timeout {
cmd.arg("--pod-running-timeout").arg(timeout);
}
Ok(cmd)
}
ProviderSpec::Kubefwd {
cluster_endpoint,
context_selector,
namespace,
kind,
name,
service_port,
domain,
selector,
} => {
if kind != "service" {
bail!("kubefwd currently supports only resource kind 'service'");
}
let context = resolve_kube_context(cluster_endpoint, Some(context_selector.as_str()))?;
let mut cmd = Command::new("kubefwd");
cmd.arg("svc")
.arg("-n")
.arg(namespace)
.arg("-x")
.arg(context)
.arg("-m")
.arg(format!("{service_port}:{local_port}"))
.arg("-f")
.arg(format!("metadata.name={name}"));
if let Some(domain) = domain {
cmd.arg("-d").arg(domain);
}
if let Some(selector) = selector {
cmd.arg("-l").arg(selector);
}
Ok(cmd)
}
ProviderSpec::TshKubeCluster {
teleport_proxy,
namespace,
kind,
name,
remote_port,
..
} => {
let mut cmd = Command::new("tsh");
cmd.arg(format!("--proxy={teleport_proxy}"))
.arg("kubectl")
.arg("-n")
.arg(namespace)
.arg("port-forward")
.arg(format!("{kind}/{name}"))
.arg(format!("{local_port}:{remote_port}"))
.arg("--address")
.arg(local_host);
Ok(cmd)
}
ProviderSpec::TshApp {
teleport_proxy,
app_name,
target_port,
} => {
if local_host != "127.0.0.1" && local_host != "localhost" {
bail!("tsh app proxy supports only 127.0.0.1 or localhost local hosts");
}
let port = target_port
.map(|target_port| format!("{local_port}:{target_port}"))
.unwrap_or_else(|| local_port.to_string());
let mut cmd = Command::new("tsh");
cmd.arg(format!("--proxy={teleport_proxy}"))
.arg("proxy")
.arg("app")
.arg(format!("--port={port}"))
.arg(app_name);
Ok(cmd)
}
ProviderSpec::Ssh {
jump_host,
jump_port,
remote_host,
remote_port,
} => {
let mut cmd = Command::new("ssh");
cmd.arg("-N")
.arg("-o")
.arg("ExitOnForwardFailure=yes")
.arg("-L")
.arg(format!(
"{local_host}:{local_port}:{remote_host}:{remote_port}"
))
.arg("-p")
.arg(jump_port.to_string())
.arg(jump_host);
Ok(cmd)
}
}
}
pub(crate) fn ensure_provider_preflight(spec: &ProviderSpec) -> Result<()> {
if let ProviderSpec::TshKubeCluster {
teleport_proxy,
kube_cluster,
..
} = spec
{
let status = Command::new("tsh")
.arg(format!("--proxy={teleport_proxy}"))
.arg("kube")
.arg("login")
.arg(kube_cluster)
.status()?;
if !status.success() {
bail!("tsh kube login failed for cluster '{}'", kube_cluster);
}
}
if let ProviderSpec::Ssh { .. } = spec {
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_app_proxy_command() {
let spec = ProviderSpec::TshApp {
teleport_proxy: "teleport.example.com:443".to_string(),
app_name: "example-app".to_string(),
target_port: None,
};
let command = build_command(&spec, "127.0.0.1", 18000).expect("command");
let args = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
args,
[
"--proxy=teleport.example.com:443",
"proxy",
"app",
"--port=18000",
"example-app"
]
);
}
#[test]
fn builds_app_proxy_command_with_target_port() {
let spec = ProviderSpec::TshApp {
teleport_proxy: "teleport.example.com:443".to_string(),
app_name: "example-app".to_string(),
target_port: Some(3000),
};
let command = build_command(&spec, "127.0.0.1", 18000).expect("command");
let args = command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
args,
[
"--proxy=teleport.example.com:443",
"proxy",
"app",
"--port=18000:3000",
"example-app"
]
);
}
#[test]
fn rejects_non_loopback_app_proxy_host() {
let spec = ProviderSpec::TshApp {
teleport_proxy: "teleport.example.com:443".to_string(),
app_name: "example-app".to_string(),
target_port: None,
};
let err = build_command(&spec, "0.0.0.0", 18000).expect_err("must reject host");
assert!(err.to_string().contains("supports only"));
}
}