Skip to content

Commit 2c08737

Browse files
committed
feat(tunnel): show tunnel information
This introduces a new `Tunnel` widget that shows the status of tunnels in the dashboard. This allows users to see what's currently happening. There are some caveats to this though: - Egress starts up immediately and is `listening`. - Ingress is only on incoming connections and switches to `inactive` when the connection is closed. Additionally, this: - Standardizes the `Session` "writers" into a broadcast that can be either for a single channel or all channels. - No longer hangs indefinitely when a source channel is closed (via ingress tunnel) but the destination is still open (warp::serve keeps the connection open for example). - Stops trying to close all channels that EOF. This is because the tunnel channels now manage their lifecycle themselves. Unfortunately, the pty and sftp requests do not. `Session::channels` now has a `None` inserted if a channel has been consumed but it is still open. - Passing a `StringError` that is clone-able instead of `Report` which is not. This results in the source needing to render the error but it makes everything much easier to work with. - Rewords `StatusError` to no longer exist - instead being constructed as a `Report` by `PodExt`. - Moves as much of the verbose logging as possible to `debug` from `info`. See `DEVELOPMENT.md` for how to enable this. - Allow `Table` to have an optional header and selection style.
1 parent 27bbb35 commit 2c08737

32 files changed

+872
-407
lines changed

.cargo/audit.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[advisories]
2-
ignore = ["RUSTSEC-2023-0071", "RUSTSEC-2024-0320"]
2+
ignore = ["RUSTSEC-2023-0071", "RUSTSEC-2024-0320", "RUSTSEC-2024-0370"]

.github/workflows/signed.yml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: check-signed-commits
2+
on:
3+
pull_request_target:
4+
branches:
5+
- main
6+
7+
jobs:
8+
check-signed-commits:
9+
name: verify-signed-commits
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
steps:
15+
- name: Check signed commits in PR
16+
uses: 1Password/check-signed-commits-action@v1

DEVELOPMENT.md

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ be available to run inside the cluster.
4242

4343
[k3d]: https://k3d.io/v5.6.3/#releases
4444

45+
## Logging
46+
47+
The global debug level can be overly noisy. Instead of doing `-vvvv`, try:
48+
49+
```bash
50+
RUST_LOG=none,kuberift=debug
51+
```
52+
4553
## Ingress Tunnel
4654

4755
If testing port forwarding and running the service locally (aka not on the

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ You can:
2828
kuberift itself.
2929
- [Metrics](docs/metrics.md) - List of the possible metrics exported via.
3030
prometheus.
31-
- [TODO](TODO.md) - A selection of outstanding functionality.
3231

3332
[auth]: docs/auth.md
3433

TODO.md

-8
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,8 @@
5454

5555
## Ingress Tunnel
5656

57-
- Add an information banner for tunnels that have been requested and are active.
58-
5957
## Egress Tunnel
6058

61-
- Add an information banner for tunnels that have been requested and are active.
62-
- Keep the task running even if the first connect fails - need to be able to
63-
retry it without restarting the session.
64-
- Display error for when the `tcpip_forward` address is `localhost`.
65-
- Need some way to do cleanup and lifecycle management of endpoints and
66-
services.
6759
- Cleanup services/endpoints on:
6860
- Shutdown - especially termination of the channel.
6961
- Startup - because we can't do cross-namespace owner references, anything

docs/deployment.md

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ using helm, there are some things to be aware of:
5151
using the [gateway api][gateway-api] or configuring your ingress controller to
5252
route TCP.
5353

54+
Note: if you're debugging something, instead of setting global verbosity with
55+
`-vv`, use `RUST_LOG=none,kuberift=debug`. That'll keep other crates that are
56+
especially noisy out of the output.
57+
5458
### Helm
5559

5660
There is a provided `getting-started.yaml` set of values. To install this on

src/broadcast.rs

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use std::{collections::HashMap, sync::Arc};
2+
3+
use eyre::{eyre, Result};
4+
use russh::ChannelId;
5+
use tokio::sync::{mpsc::UnboundedSender, Mutex};
6+
7+
use crate::events::Event;
8+
9+
#[derive(Debug, Clone, Default)]
10+
pub struct Broadcast {
11+
channels: Arc<Mutex<HashMap<ChannelId, UnboundedSender<Event>>>>,
12+
}
13+
14+
impl Broadcast {
15+
pub async fn add(&mut self, id: ChannelId, tx: UnboundedSender<Event>) -> Result<()> {
16+
self.channels.lock().await.insert(id, tx);
17+
18+
Ok(())
19+
}
20+
21+
pub async fn remove(&mut self, id: &ChannelId) -> Option<UnboundedSender<Event>> {
22+
self.channels.lock().await.remove(id)
23+
}
24+
25+
pub async fn send(&self, id: &ChannelId, event: Event) -> Result<()> {
26+
let mut channels = self.channels.lock().await;
27+
if let Some(sender) = channels.get_mut(id) {
28+
sender
29+
.send(event)
30+
.map_err(|_| eyre!("failed to send event"))?;
31+
}
32+
Ok(())
33+
}
34+
35+
pub async fn all(&self, event: Event) -> Result<()> {
36+
let mut channels = self.channels.lock().await;
37+
for sender in channels.values_mut() {
38+
sender.send(event.clone())?;
39+
}
40+
41+
Ok(())
42+
}
43+
}

src/cli/dev/dashboard.rs

-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,6 @@ impl AsyncRead for Stdin {
6969

7070
let n = std::io::stdin().read(buf.initialize_unfilled())?;
7171
buf.advance(n);
72-
73-
tracing::info!("buf: {:?}", buf.filled());
7472
}
7573

7674
if !buf.filled().is_empty() {

src/cli/serve.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ impl Serve {
112112
};
113113

114114
let ctrl = ControllerBuilder::default()
115-
.current(
115+
.server(
116116
CurrentPodBuilder::default()
117117
.namespace(cfg.default_namespace.clone())
118118
.name(self.pod_name.clone())

src/dashboard.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tokio::{
1212
use tokio_util::io::ReaderStream;
1313

1414
use crate::{
15-
events::{Broadcast, Event, Input, Keypress},
15+
events::{Broadcast, Event, Input, Keypress, StringError},
1616
io::{backend::Backend, Writer},
1717
widget::{apex::Apex, Raw, Widget},
1818
};
@@ -185,7 +185,9 @@ async fn run(
185185
let raw_result =
186186
draw_raw(raw_widget, &mut term, &mut rx, stdout.non_blocking()).await;
187187

188-
let result = current_widget.dispatch(&Event::Finished(raw_result))?;
188+
let result = current_widget.dispatch(&Event::Finished(
189+
raw_result.map_err(|e| StringError(format!("{e:?}"))),
190+
))?;
189191

190192
state.ui();
191193

src/events.rs

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::str;
1+
use std::{error::Error, str};
22

33
use eyre::Result;
44
use ratatui::backend::WindowSize;
55
use tokio_util::bytes::Bytes;
66

7-
use crate::widget::Raw;
7+
use crate::{resources::tunnel, widget::Raw};
88

99
#[derive(Debug)]
1010
pub enum Broadcast {
@@ -14,15 +14,15 @@ pub enum Broadcast {
1414
Raw(Box<dyn Raw>),
1515
}
1616

17-
#[derive(Debug)]
17+
#[derive(Debug, Clone)]
1818
pub enum Event {
1919
Input(Input),
2020
Resize(WindowSize),
2121
Goto(Vec<String>),
22-
Error(String),
2322
Shutdown,
2423
Render,
25-
Finished(Result<()>),
24+
Finished(Result<(), StringError>),
25+
Tunnel(Result<tunnel::Tunnel, tunnel::Error>),
2626
}
2727

2828
impl Event {
@@ -49,7 +49,18 @@ impl From<Bytes> for Event {
4949
}
5050
}
5151

52-
#[derive(Debug)]
52+
#[derive(Debug, Clone)]
53+
pub struct StringError(pub String);
54+
55+
impl Error for StringError {}
56+
57+
impl std::fmt::Display for StringError {
58+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59+
write!(f, "{}", self.0)
60+
}
61+
}
62+
63+
#[derive(Debug, Clone)]
5364
pub struct Input {
5465
pub key: Keypress,
5566
raw: Bytes,

src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! # kuberift
22
3+
mod broadcast;
34
#[warn(dead_code)]
45
mod cli;
56
mod dashboard;

src/resources.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod age;
22
pub mod container;
33
pub mod file;
44
pub mod pod;
5+
pub mod status;
56
pub mod store;
67
pub mod tunnel;
78

@@ -18,7 +19,7 @@ use kube::{
1819
};
1920
use regex::Regex;
2021
use serde::Serialize;
21-
use tracing::info;
22+
pub use tunnel::Tunnel;
2223

2324
use crate::identity;
2425

@@ -32,11 +33,11 @@ pub(crate) async fn create(
3233
client: &Api<CustomResourceDefinition>,
3334
update: bool,
3435
) -> Result<Vec<CustomResourceDefinition>> {
35-
info!(update = update, "updating CRD definitions...");
36+
tracing::info!(update = update, "updating CRD definitions...");
3637

3738
let results: Vec<_> = futures::stream::iter(all())
3839
.map(|resource| async move {
39-
info!("creating/updating CRD: {}", resource.name_any());
40+
tracing::info!("creating/updating CRD: {}", resource.name_any());
4041

4142
if update {
4243
client

src/resources/container.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -197,15 +197,15 @@ impl ContainerExt for Container {
197197
}
198198

199199
impl<'a> TableRow<'a> for Container {
200-
fn header() -> Row<'a> {
201-
Row::new(vec![
200+
fn header() -> Option<Row<'a>> {
201+
Some(Row::new(vec![
202202
Cell::from("Name"),
203203
Cell::from("Image"),
204204
Cell::from("Ready"),
205205
Cell::from("State"),
206206
Cell::from("Restarts"),
207207
Cell::from("Age"),
208-
])
208+
]))
209209
}
210210

211211
fn constraints() -> Vec<Constraint> {

src/resources/pod.rs

+7-47
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
pub mod proc;
22

3-
use std::{borrow::Borrow, cmp::Ordering, error::Error, fmt::Display, net::IpAddr, sync::Arc};
3+
use std::{borrow::Borrow, cmp::Ordering, net::IpAddr, sync::Arc};
44

55
use chrono::{TimeDelta, Utc};
6-
use k8s_openapi::{
7-
api::core::v1::{
8-
ContainerState, ContainerStateTerminated, ContainerStateWaiting, ContainerStatus, Pod,
9-
PodStatus,
10-
},
11-
apimachinery::pkg::apis::meta::v1,
6+
use k8s_openapi::api::core::v1::{
7+
ContainerState, ContainerStateTerminated, ContainerStateWaiting, ContainerStatus, Pod,
8+
PodStatus,
129
};
1310
use kube::ResourceExt;
1411
pub use proc::Proc;
@@ -27,43 +24,6 @@ use crate::widget::{
2724
TableRow,
2825
};
2926

30-
// TODO: There's probably a better debug implementation than this.
31-
#[derive(Clone, Debug)]
32-
pub struct StatusError {
33-
pub message: String,
34-
}
35-
36-
// Because this is a golang error that's being returned, there's really no good
37-
// way to convert this into something that is moderately usable. The rest of the
38-
// `Status` struct is empty of anything useful. The decision is to be naive here
39-
// and let other display handlers figure out if they would like to deal with the
40-
// message.
41-
impl StatusError {
42-
pub fn new(inner: v1::Status) -> Self {
43-
Self {
44-
message: inner.message.unwrap_or("unknown status".to_string()),
45-
}
46-
}
47-
}
48-
49-
impl Error for StatusError {}
50-
51-
impl Display for StatusError {
52-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53-
write!(f, "{}", self.message)
54-
}
55-
}
56-
57-
pub trait StatusExt {
58-
fn is_success(&self) -> bool;
59-
}
60-
61-
impl StatusExt for v1::Status {
62-
fn is_success(&self) -> bool {
63-
self.status == Some("Success".to_string())
64-
}
65-
}
66-
6727
pub enum Phase {
6828
Pending,
6929
Running,
@@ -268,15 +228,15 @@ impl PodExt for Pod {
268228
}
269229

270230
impl<'a> TableRow<'a> for Arc<Pod> {
271-
fn header() -> Row<'a> {
272-
Row::new(vec![
231+
fn header() -> Option<Row<'a>> {
232+
Some(Row::new(vec![
273233
Cell::from("Namespace"),
274234
Cell::from("Name"),
275235
Cell::from("Ready"),
276236
Cell::from("Status"),
277237
Cell::from("Restarts"),
278238
Cell::from("Age"),
279-
])
239+
]))
280240
}
281241

282242
fn constraints() -> Vec<Constraint> {

src/resources/pod/proc.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ use k8s_openapi::api::core::v1::Pod;
33
use kube::api::{Api, AttachParams};
44
use tokio::io::AsyncReadExt;
55

6-
use super::{StatusError, StatusExt};
7-
use crate::resources::container::{Container, ContainerExt};
6+
use crate::resources::{
7+
container::{Container, ContainerExt},
8+
status::StatusExt,
9+
};
810

911
pub struct Proc {
1012
container: Container,
@@ -47,7 +49,7 @@ impl Proc {
4749

4850
if let Some(status) = status.await {
4951
if !status.is_success() {
50-
return Err(eyre!(StatusError::new(status)));
52+
return Err(status.into_report());
5153
}
5254
}
5355

0 commit comments

Comments
 (0)