Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for terminal size when executing command inside a container #983

Merged
merged 11 commits into from Nov 13, 2022
5 changes: 5 additions & 0 deletions examples/Cargo.toml
Expand Up @@ -53,6 +53,7 @@ backoff = "0.4.0"
clap = { version = "3.1.9", default-features = false, features = ["std", "cargo", "derive"] }
edit = "0.1.3"
tokio-stream = { version = "0.1.9", features = ["net"] }
crossterm = {version = "0.25.0", features = ["event-stream"]}

[[example]]
name = "configmapgen_controller"
Expand Down Expand Up @@ -202,3 +203,7 @@ path = "custom_client_trace.rs"
[[example]]
name = "secret_syncer"
path = "secret_syncer.rs"

[[example]]
name = "pod_exec_terminal_size"
path = "pod_exec_terminal_size.rs"
125 changes: 125 additions & 0 deletions examples/pod_exec_terminal_size.rs
@@ -0,0 +1,125 @@
use futures::{channel::mpsc::Sender, SinkExt, StreamExt};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should rename this to pod_shell_crossterm to match the other example and highlight functionality

use k8s_openapi::api::core::v1::Pod;

use kube::{
api::{
Api, AttachParams, AttachedProcess, DeleteParams, PostParams, ResourceExt, TerminalSize,
},
Client, runtime::wait::{await_condition, conditions::is_pod_running},
};
use tokio::{io::AsyncWriteExt, select, signal};
use crossterm::event::{EventStream, Event};

// send the new terminal size to channel when it change
async fn handle_terminal_size(mut channel: Sender<TerminalSize>) -> Result<(), anyhow::Error> {
let (width, height) = crossterm::terminal::size()?;
channel
.send(TerminalSize { height, width })
.await?;
clux marked this conversation as resolved.
Show resolved Hide resolved

// create a stream to catch SIGWINCH signal
let mut sig = signal::unix::signal(signal::unix::SignalKind::window_change())?;
loop {
if sig.recv().await == None {
return Ok(())
}

let (width, height) = crossterm::terminal::size()?;
channel
.send(TerminalSize { height, width })
.await?;
}
Ok(())
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::try_default().await?;

let pods: Api<Pod> = Api::default_namespaced(client);
let p: Pod = serde_json::from_value(serde_json::json!({
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "example" },
"spec": {
"containers": [{
"name": "example",
"image": "alpine",
// Do nothing
"command": ["tail", "-f", "/dev/null"],
}],
}
}))?;
// Create pod if don't exist
pods.create(&PostParams::default(), &p).await?;

// Wait until the pod is running, otherwise we get 500 error.
kazk marked this conversation as resolved.
Show resolved Hide resolved
let running = await_condition(pods.clone(), "example", is_pod_running());
let _ = tokio::time::timeout(std::time::Duration::from_secs(15), running).await?;

{
// Here we we put the terminal in 'raw' mode to directly get the input from the user and sending it to the server and getting the result from the server to display directly.
// We also watch for change in your terminal size and send it to the server so that application that use the size work properly.
crossterm::terminal::enable_raw_mode()?;
let mut attached: AttachedProcess = pods
.exec(
"example",
vec!["sh"],
&AttachParams::default().stdin(true).tty(true).stderr(false),
)
.await?;

let mut stdin = tokio_util::io::ReaderStream::new(tokio::io::stdin());
let mut stdout = tokio::io::stdout();

let mut output = tokio_util::io::ReaderStream::new(attached.stdout().unwrap());
let mut input = attached.stdin().unwrap();

let term_tx = attached.terminal_size().unwrap();

let mut handle_terminal_size_handle = tokio::spawn(handle_terminal_size(term_tx));

loop {
select! {
message = stdin.next() => {
clux marked this conversation as resolved.
Show resolved Hide resolved
match message {
Some(Ok(message)) => {
input.write(&message).await?;
}
_ => {
break;
},
}
},
message = output.next() => {
match message {
Some(Ok(message)) => {
stdout.write(&message).await?;
stdout.flush().await?;
},
_ => {
break
},
}
},
result = &mut handle_terminal_size_handle => {
match result {
Ok(_) => println!("End of terminal size stream"),
Err(e) => println!("Error getting terminal size: {e:?}")
}
},
};
}
clux marked this conversation as resolved.
Show resolved Hide resolved
crossterm::terminal::disable_raw_mode()?;
}

// Delete it
pods.delete("example", &DeleteParams::default())
.await?
.map_left(|pdel| {
assert_eq!(pdel.name_any(), "example");
});

println!("");
Ok(())
}
2 changes: 1 addition & 1 deletion kube-client/src/api/mod.rs
Expand Up @@ -5,7 +5,7 @@ mod core_methods;
#[cfg(feature = "ws")] mod remote_command;
use std::fmt::Debug;

#[cfg(feature = "ws")] pub use remote_command::AttachedProcess;
#[cfg(feature = "ws")] pub use remote_command::{AttachedProcess, TerminalSize};
#[cfg(feature = "ws")] mod portforward;
#[cfg(feature = "ws")] pub use portforward::Portforwarder;

Expand Down