Initial commit

This commit is contained in:
Joe Frikker
2025-12-20 21:49:17 -05:00
commit 6197a361ce
4 changed files with 1091 additions and 0 deletions

179
src/main.rs Normal file
View File

@@ -0,0 +1,179 @@
use clap::Parser;
use linefeed::{Completer, Completion, Interface, ReadResult, Terminal};
use nix::sys::signal;
use std::{
marker::PhantomData,
os::unix::process::CommandExt,
process::Command,
sync::Arc,
};
/// Interact with a kubernetes cluster
#[derive(Parser, Debug)]
#[command(bin_name="kube")]
struct Args {
env: String,
/// Run k9s (extra interactive)
#[arg(short, long)]
interactive: bool,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
if args.interactive {
run_interactive(args.env)?;
} else {
run_non_interactive(args.env)?;
}
Ok(())
}
fn run_interactive(context: String) -> anyhow::Result<()> {
cmd_lib::spawn!(k9s --context $context --readonly)?.wait()?;
Ok(())
}
fn run_non_interactive(context: String) -> anyhow::Result<()> {
let mut context = Context::new(context);
context.get_pods()?;
let term = Interface::new("kube")?;
term.set_prompt(&format!("{}> ", context.name()))?;
term.set_completer(Arc::new(K8Completer::new(context.pod_names())));
while let ReadResult::Input(line) = term.read_line()? {
let res = match line.trim() {
"" => continue,
"exit" => break,
"get pods" => context.get_pods(),
other => context.run_line(other),
};
if res.is_err() {
eprintln!("Command failed");
}
term.add_history(line);
term.set_completer(Arc::new(K8Completer::new(context.pod_names())));
}
Ok(())
}
struct Context {
name: String,
pod_names: Arc<Vec<String>>,
}
impl Context {
fn new(name: String) -> Self {
Context {
name,
pod_names: Arc::new(Vec::new()),
}
}
fn name(&self) -> &str {
&self.name
}
fn run_line(&self, line: &str) -> anyhow::Result<()> {
let args = shell_words::split(line)?;
let old_sig =
unsafe { signal::signal(signal::Signal::SIGINT, signal::SigHandler::SigIgn).unwrap() };
let mut cmd = Command::new("kubectl");
cmd.arg("--context");
cmd.arg(&self.name);
cmd.args(args);
unsafe {
cmd.pre_exec(|| {
signal::signal(signal::Signal::SIGINT, signal::SigHandler::SigDfl).unwrap();
Ok(())
});
}
let status = cmd.status()?;
unsafe {
signal::signal(signal::Signal::SIGINT, old_sig).unwrap();
}
anyhow::ensure!(status.success(), "Command exited with error");
Ok(())
}
fn get_pods(&mut self) -> anyhow::Result<()> {
let old_sig =
unsafe { signal::signal(signal::Signal::SIGINT, signal::SigHandler::SigIgn).unwrap() };
let mut cmd = Command::new("kubectl");
cmd.args(["--context", &self.name, "get", "pods"]);
unsafe {
cmd.pre_exec(|| {
signal::signal(signal::Signal::SIGINT, signal::SigHandler::SigDfl).unwrap();
Ok(())
});
}
let out = cmd.output()?;
unsafe {
signal::signal(signal::Signal::SIGINT, old_sig).unwrap();
}
let out_str = String::from_utf8_lossy(&out.stdout);
let err_str = String::from_utf8_lossy(&out.stderr);
print!("{}", out_str);
eprint!("{}", err_str);
self.pod_names = out_str
.lines()
.filter_map(|line| line.find(' ').map(|end| line[0..end].to_string()))
.collect::<Vec<_>>()
.into();
anyhow::ensure!(out.status.success(), "Command exited with error");
Ok(())
}
fn pod_names(&self) -> Arc<Vec<String>> {
self.pod_names.clone()
}
}
struct K8Completer<Term: Terminal> {
pods: Arc<Vec<String>>,
term: PhantomData<Term>,
}
impl<Term: Terminal> K8Completer<Term> {
fn new(pods: Arc<Vec<String>>) -> Self {
K8Completer {
pods,
term: PhantomData,
}
}
}
impl<Term: Terminal> Completer<Term> for K8Completer<Term> {
fn complete(
&self,
word: &str,
_prompter: &linefeed::Prompter<Term>,
_start: usize,
_end: usize,
) -> Option<Vec<linefeed::Completion>> {
Some(
self.pods
.iter()
.filter(|pod| pod.starts_with(word))
.map(|pod| Completion::simple(pod.clone()))
.collect(),
)
}
}