use std::time::Duration;
use anyhow::{Context as _, Result};
use k8s_openapi::api::core::v1::Pod;
use kube::{Client, api::Api};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::UnboundedSender;
use tokio::task::JoinHandle;
use polyc_controller::control_plane;
use crate::action::Action;
pub(crate) use polyc_controller::control_plane::{AGENT_PORT, FORENSICS_PORT};
const ACCEPT_RETRY_BACKOFF: Duration = Duration::from_secs(1);
pub(crate) struct Forward {
pub url: String,
task: JoinHandle<()>,
}
impl Drop for Forward {
fn drop(&mut self) {
self.task.abort();
}
}
pub(crate) fn start(
client: Client,
namespace: String,
remote_port: u16,
label: &'static str,
tx: UnboundedSender<Action>,
) -> Result<Forward> {
let std_listener =
std::net::TcpListener::bind("127.0.0.1:0").context("bind local forward listener")?;
std_listener
.set_nonblocking(true)
.context("set forward listener non-blocking")?;
let local_port = std_listener.local_addr()?.port();
let listener = TcpListener::from_std(std_listener).context("adopt forward listener")?;
let task = tokio::spawn(accept_loop(
client,
namespace,
listener,
remote_port,
label,
tx,
));
Ok(Forward {
url: format!("http://127.0.0.1:{local_port}"),
task,
})
}
async fn accept_loop(
client: Client,
namespace: String,
listener: TcpListener,
remote_port: u16,
label: &'static str,
tx: UnboundedSender<Action>,
) {
loop {
match listener.accept().await {
Ok((local, _peer)) => {
tokio::spawn(bridge_conn(
client.clone(),
namespace.clone(),
local,
remote_port,
label,
tx.clone(),
));
}
Err(err) => {
let _ = tx.send(Action::Error(format!("{label} forward accept: {err}")));
tokio::time::sleep(ACCEPT_RETRY_BACKOFF).await;
}
}
}
}
async fn bridge_conn(
client: Client,
namespace: String,
mut local: TcpStream,
remote_port: u16,
label: &'static str,
tx: UnboundedSender<Action>,
) {
let pod = match control_plane::pick_control_plane_pod(&client, &namespace).await {
Ok(pod) => pod,
Err(err) => {
let _ = tx.send(Action::Error(format!("{label} forward: {err}")));
return;
}
};
let pods: Api<Pod> = Api::namespaced(client, &namespace);
let mut pf = match pods.portforward(&pod, &[remote_port]).await {
Ok(pf) => pf,
Err(err) => {
let _ = tx.send(Action::Error(format!(
"{label} port-forward to {pod}: {err}"
)));
return;
}
};
let Some(mut upstream) = pf.take_stream(remote_port) else {
let _ = tx.send(Action::Error(format!(
"{label} port-forward returned no stream for :{remote_port}"
)));
return;
};
let error_fut = pf.take_error(remote_port);
let _ = tokio::io::copy_bidirectional(&mut local, &mut upstream).await;
if let Some(fut) = error_fut
&& let Some(msg) = fut.await
{
let _ = tx.send(Action::Error(format!(
"{label} port-forward channel: {msg}"
)));
}
}