WP-12: Kubernetes overview and workload actions
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

kube-rs gateway (nodes, namespaces, deployments/statefulsets/daemonsets with
images, PVCs; rollout restart, scale, set image via patches) using the
microk8s kubeconfig by default, fake gateway for dev, /api/cluster routes,
Kubernetes page with node cards, workload table with admin actions and PVC
list. Also implements the streaming command runner that WP-11 relied on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:33:57 +02:00
parent 51ec216910
commit 8f36a54da0
15 changed files with 807 additions and 26 deletions

View File

@ -43,11 +43,52 @@ impl CommandRunner for SystemCommandRunner {
async fn run_streaming(
&self,
_program: &str,
_args: &[&str],
_out: &dyn LineSink,
program: &str,
args: &[&str],
out: &dyn LineSink,
) -> Result<bool, DomainError> {
todo!()
use tokio::io::{AsyncBufReadExt, BufReader};
let mut child = tokio::process::Command::new(program)
.args(args)
.env("DEBIAN_FRONTEND", "noninteractive")
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
let mut readers = Vec::new();
if let Some(stdout) = child.stdout.take() {
let tx = tx.clone();
readers.push(tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(l)) = lines.next_line().await {
let _ = tx.send(l);
}
}));
}
if let Some(stderr) = child.stderr.take() {
let tx = tx.clone();
readers.push(tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
while let Ok(Some(l)) = lines.next_line().await {
let _ = tx.send(l);
}
}));
}
drop(tx);
while let Some(line) = rx.recv().await {
out.line(&line);
}
for r in readers {
let _ = r.await;
}
let status = child
.wait()
.await
.map_err(|e| DomainError::Unavailable(format!("{program}: {e}")))?;
Ok(status.success())
}
async fn read_file(&self, path: &str) -> Result<Option<String>, DomainError> {