From 7fbdc58f3129b5a73401106c78ef03aed5ede229 Mon Sep 17 00:00:00 2001 From: Peter Gundel Date: Sat, 16 Jun 2018 10:57:46 +0200 Subject: [PATCH 1/3] Add one-off command This command takes the image of the container of the current pod and runs the specified command in a one-off kubectl run command. If the pod has multiple containers and none is specified, it will fail asking for the specific container. --- src/cmd.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 2 files changed, 194 insertions(+) diff --git a/src/cmd.rs b/src/cmd.rs index 16ae18e..8738201 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1145,6 +1145,199 @@ command!( } ); +command!( + OneOff, + "one-off", + "exec specified command in a new container of the active pod", + |clap: App<'static, 'static>| clap.arg( + Arg::with_name("command") + .help("The command to execute") + .required(true) + .index(1) + ).arg( + Arg::with_name("container") + .short("c") + .long("container") + .help("Exec in the image of the specified container") + .takes_value(true) + ) + .arg( + Arg::with_name("terminal") + .short("t") + .long("terminal") + .help( + "Run the command in a new terminal. With --terminal ARG, ARG is used as the \ + terminal command, otherwise the default is used ('set terminal ' to \ + specify default)" + ) + .takes_value(true) + .min_values(0) + ), + vec!["one-off"], + |args: Vec<&str>, env: &Env| if args.len() <= 1 { + let mut v = Vec::new(); + let argstart = args.get(0); + match env.current_object { + ::KObj::Pod { + name: _, + ref containers, + } => for cont in containers.iter() { + if argstart.is_none() || cont.starts_with(argstart.unwrap()) { + v.push(cont.clone()); + } + }, + _ => {} + } + ( + match argstart { + Some(line) => line.len(), + None => 0, + }, + v, + ) + } else { + (0, Vec::new()) + }, + |matches, env, writer| { + let cont = match matches.value_of("container") { + Some(c) => match env.current_object { + ::KObj::Pod { + name: _, + ref containers, + } => { + if !containers.contains(&c.to_string()) { + clickwrite!( + writer, + "Specified container not found. Please specify one of: \ + {:?}\n", + containers + ); + return; + } + c + } + _ => { + clickwrite!(writer, "Need an active pod to run one-off command.\n"); + return; + } + }, + None => match env.current_object { + ::KObj::Pod { + name: _, + ref containers, + } => { + if containers.len() == 1 { + containers[0].as_str() + } else { + clickwrite!( + writer, + "Pod has multiple containers, please specify one of: \ + {:?}\n", + containers + ); + return; + } + } + _ => { + clickwrite!(writer, "Need an active pod to run one-off command.\n"); + return; + } + }, + }; + + let cmd = matches.value_of("command").unwrap(); // safe as required + if let (Some(ref kluster), Some(ref ns), Some(ref pod)) = ( + env.kluster.as_ref(), + env.current_object_namespace.as_ref(), + env.current_pod(), + ) { + let url = format!("/api/v1/namespaces/{}/pods/{}", ns, pod); + let pod_opt: Option = env.run_on_kluster(|k| k.get(url.as_str())); + let container_image = if let Some(pod) = pod_opt { + let mut image = ""; + if let Some(ref container_statuses) = pod.status.container_statuses { + for container_status in container_statuses.iter() { + if cont == container_status.name { + image = &container_status.image; + } + } + if image == "" { + clickwrite!(writer, "Couldon't get container image\n"); + return; + } else { + String::from(image) + } + } else { + clickwrite!(writer, "Couldon't get container image\n"); + return; + } + } else { + clickwrite!(writer, "Couldon't get container image\n"); + return; + }; + + if matches.is_present("terminal") { + let terminal = if let Some(t) = matches.value_of("terminal") { + t + } else if let Some(ref t) = env.click_config.terminal { + t + } else { + "xterm -e" + }; + let mut targs: Vec<&str> = terminal.split_whitespace().collect(); + let mut kubectl_args = vec![ + "kubectl", + "--namespace", + ns, + "--context", + &kluster.name, + "run", + "one-off-shell", + "--restart=Never", + "-i", + "--rm", + "--tty", + "--image", + container_image.as_str(), + ]; + targs.append(&mut kubectl_args); + targs.push(cmd); + clickwrite!(writer, "Starting in terminal\n"); + if let Err(e) = duct::cmd(targs[0], &targs[1..]).start() { + clickwrite!( + writer, + "Could not launch in terminal: {}\n", + e.description() + ); + } + } else { + clickwrite!(writer, "Starting one-off container with image {:?}\n", container_image); + let status = Command::new("kubectl") + .arg("--namespace") + .arg(ns) + .arg("--context") + .arg(&kluster.name) + .arg("run") + .arg("one-off-shell") + .arg("--restart=Never") + .arg("-i") + .arg("--rm") + .arg("--tty") + .arg("--image") + .arg(container_image.as_str()) + .arg(cmd) + .status() + .expect("failed to execute kubectl"); + if !status.success() { + clickwrite!(writer, "kubectl exited abnormally\n"); + } + } + } else { + clickwrite!(writer, "Need an active pod in order to run one-off command.\n"); + } + } +); + command!( Describe, "describe", diff --git a/src/main.rs b/src/main.rs index ad35801..935db5e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -620,6 +620,7 @@ fn main() { commands.push(Box::new(cmd::Secrets::new())); commands.push(Box::new(cmd::PortForward::new())); commands.push(Box::new(cmd::PortForwards::new())); + commands.push(Box::new(cmd::OneOff::new())); let mut rl = Editor::::new(); rl.load_history(hist_path.as_path()).unwrap_or_default(); From 8f11d2f2ceb2e568ba2e857a3ee2811da1dd38b6 Mon Sep 17 00:00:00 2001 From: Peter Gundel Date: Sat, 7 Jul 2018 10:01:39 +0200 Subject: [PATCH 2/3] Rename OneOff to Run --- src/cmd.rs | 19 ++++++++++--------- src/main.rs | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index 8738201..d4b5ca4 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -1146,8 +1146,8 @@ command!( ); command!( - OneOff, - "one-off", + Run, + "run", "exec specified command in a new container of the active pod", |clap: App<'static, 'static>| clap.arg( Arg::with_name("command") @@ -1173,7 +1173,7 @@ command!( .takes_value(true) .min_values(0) ), - vec!["one-off"], + vec!["run"], |args: Vec<&str>, env: &Env| if args.len() <= 1 { let mut v = Vec::new(); let argstart = args.get(0); @@ -1217,7 +1217,7 @@ command!( c } _ => { - clickwrite!(writer, "Need an active pod to run one-off command.\n"); + clickwrite!(writer, "Need an active pod to run an one-off command.\n"); return; } }, @@ -1239,7 +1239,7 @@ command!( } } _ => { - clickwrite!(writer, "Need an active pod to run one-off command.\n"); + clickwrite!(writer, "Need an active pod to run an one-off command.\n"); return; } }, @@ -1275,6 +1275,7 @@ command!( clickwrite!(writer, "Couldon't get container image\n"); return; }; + let pod_name = format!("one-off-shell-{}", container_image.as_str()); if matches.is_present("terminal") { let terminal = if let Some(t) = matches.value_of("terminal") { @@ -1292,7 +1293,7 @@ command!( "--context", &kluster.name, "run", - "one-off-shell", + pod_name.as_str(), "--restart=Never", "-i", "--rm", @@ -1311,14 +1312,14 @@ command!( ); } } else { - clickwrite!(writer, "Starting one-off container with image {:?}\n", container_image); + clickwrite!(writer, "Starting an one-off container with image {:?}\n", container_image); let status = Command::new("kubectl") .arg("--namespace") .arg(ns) .arg("--context") .arg(&kluster.name) .arg("run") - .arg("one-off-shell") + .arg(pod_name) .arg("--restart=Never") .arg("-i") .arg("--rm") @@ -1333,7 +1334,7 @@ command!( } } } else { - clickwrite!(writer, "Need an active pod in order to run one-off command.\n"); + clickwrite!(writer, "Need an active pod in order to run an one-off command.\n"); } } ); diff --git a/src/main.rs b/src/main.rs index 935db5e..771194f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -620,7 +620,7 @@ fn main() { commands.push(Box::new(cmd::Secrets::new())); commands.push(Box::new(cmd::PortForward::new())); commands.push(Box::new(cmd::PortForwards::new())); - commands.push(Box::new(cmd::OneOff::new())); + commands.push(Box::new(cmd::Run::new())); let mut rl = Editor::::new(); rl.load_history(hist_path.as_path()).unwrap_or_default(); From 7078ddb7078f49f420f6a9892c362556c705a6ea Mon Sep 17 00:00:00 2001 From: Peter Gundel Date: Sun, 12 Aug 2018 18:24:52 +0200 Subject: [PATCH 3/3] Ensure proper pod name for Run command From k8s: ``` must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' ``` --- src/cmd.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/cmd.rs b/src/cmd.rs index d4b5ca4..05cf284 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -324,6 +324,20 @@ fn shorten_to(s: String, max_len: usize) -> String { } } +/// Replace all characters that are not allowed as part of a pod name +/// Regex for validation is: +/// '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*' +fn ensure_pod_name(s: String) -> String { + let pod_name_regex = Regex::new(r"[^a-z0-9-]").unwrap(); + let result = pod_name_regex.replace_all(&s.to_ascii_lowercase().to_string(), "-").to_string(); + + if &result[0..1] == "-" || &result[58..59] == "-" { + format!("x{}x", &result[0..57]) + } else { + format!("{}", &result[0..59]) + } +} + /// Print out the specified list of pods in a pretty format fn print_podlist( podlist: PodList, @@ -1275,7 +1289,7 @@ command!( clickwrite!(writer, "Couldon't get container image\n"); return; }; - let pod_name = format!("one-off-shell-{}", container_image.as_str()); + let pod_name = format!("run-{}", ensure_pod_name(container_image.to_string())); if matches.is_present("terminal") { let terminal = if let Some(t) = matches.value_of("terminal") {