Skip to content

Commit 02788af

Browse files
committed
feat: computing the graph for pods
This commit introduces the `ResourceGraph` trait that allows a resource to compute the graph of its references - both the owners and random things that it refers to in the specification. It is initially implemented for pods but the goal is to have it be the detail screen for every resource supported.feat: draw resource connections
1 parent ec77ace commit 02788af

File tree

8 files changed

+473
-30
lines changed

8 files changed

+473
-30
lines changed

Cargo.lock

+25-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ base64 = "0.22.1"
2929
bon = "2.3.0"
3030
cata = { version = "0.1.1" }
3131
chrono = { version = "0.4.38", features = ["serde"] }
32-
clap = { version = "4.5.17", features = ["derive", "env"] }
32+
clap = { version = "4.5.18", features = ["derive", "env"] }
3333
clap-verbosity-flag = "2.2.1"
3434
clio = { version = "0.3.5", features = ["clap", "clap-parse"] }
3535
color-eyre = "0.6.3"
@@ -53,6 +53,7 @@ lazy_static = "1.5.0"
5353
local-ip-address = "0.6.3"
5454
mio = "1.0.2"
5555
ndarray = "0.16.1"
56+
petgraph = "0.6.5"
5657
pkcs8 = "0.10.2"
5758
prometheus = "0.13.4"
5859
prometheus-static-metric = "0.5.1"
@@ -78,7 +79,7 @@ strum = { version = "0.26.3", features = ["derive"] }
7879
strum_macros = "0.26.4"
7980
syntect = "5.2.0"
8081
syntect-tui = "3.0.4"
81-
tachyonfx = "0.6.0"
82+
tachyonfx = "0.7.0"
8283
tokio = { version = "1.40.0", features = ["full", "tracing"] }
8384
tokio-util = { version = "0.7.12", features = ["io-util"] }
8485
tracing = "0.1.40"

src/cli/dev.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod agent;
22
mod authz;
33
mod dashboard;
4+
mod graph;
45
mod shell;
56
mod stdin;
67

@@ -19,6 +20,7 @@ enum DevCmd {
1920
Agent(agent::Agent),
2021
Authz(authz::Authz),
2122
Dashboard(dashboard::Dashboard),
23+
Graph(graph::Cmd),
2224
Shell(shell::Shell),
2325
Stdin(stdin::Stdin),
2426
}

src/cli/dev/graph.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use cata::{Command, Container};
2+
use clap::Parser;
3+
use eyre::Result;
4+
use k8s_openapi::api::core::v1::Pod;
5+
use kube::{api::ListParams, Api};
6+
use petgraph::dot::{Config, Dot};
7+
8+
use crate::resources::ResourceGraph;
9+
10+
#[derive(Parser, Container)]
11+
pub struct Cmd {}
12+
13+
#[async_trait::async_trait]
14+
impl Command for Cmd {
15+
async fn run(&self) -> Result<()> {
16+
let client = kube::Client::try_default().await?;
17+
18+
let pods = Api::<Pod>::all(client.clone())
19+
.list(&ListParams::default())
20+
.await?;
21+
22+
for pod in pods.items {
23+
let g = pod.graph(&client).await?;
24+
25+
let ng = g.map(
26+
|_, n| {
27+
format!(
28+
"{}/{}",
29+
n.kind
30+
.as_ref()
31+
.unwrap_or(&"unknown".to_string())
32+
.to_lowercase(),
33+
n.name.as_ref().unwrap_or(&"unknown".to_string())
34+
)
35+
},
36+
|_, e| e,
37+
);
38+
39+
println!("{:#?}", Dot::with_config(&ng, &[Config::EdgeNoLabel]));
40+
}
41+
42+
Ok(())
43+
}
44+
}

src/resources.rs

+130-20
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,24 @@ pub mod file;
44
pub mod install;
55
pub mod node;
66
pub mod pod;
7+
pub mod refs;
78
pub mod status;
89
pub mod store;
910
pub mod tunnel;
1011

1112
use color_eyre::Section;
12-
use eyre::{eyre, Result};
13+
use eyre::{eyre, Report, Result};
1314
pub use file::File;
14-
use futures::StreamExt;
15+
use futures::{Stream, StreamExt};
1516
use itertools::Itertools;
1617
use json_value_merge::Merge;
17-
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
18+
use k8s_openapi::{
19+
api::core::v1::ObjectReference,
20+
apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition,
21+
apimachinery::pkg::apis::meta::v1::OwnerReference,
22+
};
1823
use kube::{
24+
api,
1925
api::{
2026
Api, DynamicObject, GroupVersionKind, ObjectMeta, PartialObjectMetaExt, PatchParams,
2127
PostParams, ResourceExt,
@@ -24,6 +30,7 @@ use kube::{
2430
discovery::pinned_kind,
2531
CustomResourceExt, Resource,
2632
};
33+
use petgraph::Graph;
2734
use regex::Regex;
2835
use serde::Serialize;
2936
pub use tunnel::Tunnel;
@@ -131,6 +138,22 @@ pub trait Compare {
131138
fn cmp(&self, right: &Self) -> std::cmp::Ordering;
132139
}
133140

141+
pub trait GetGv {
142+
fn gv(&self) -> (String, String);
143+
}
144+
145+
impl GetGv for String {
146+
fn gv(&self) -> (String, String) {
147+
let version: Vec<_> = self.splitn(2, '/').collect();
148+
149+
if version.len() == 1 {
150+
(String::new(), version[0].to_string())
151+
} else {
152+
(version[0].to_string(), version[1].to_string())
153+
}
154+
}
155+
}
156+
134157
pub trait GetGvk {
135158
fn gvk(&self) -> Result<GroupVersionKind>;
136159
}
@@ -141,13 +164,7 @@ impl GetGvk for DynamicObject {
141164
return Err(eyre!("no types found"));
142165
};
143166

144-
let version: Vec<_> = types.api_version.splitn(2, '/').collect();
145-
146-
let (group, version) = if version.len() == 1 {
147-
(String::new(), version[0].to_string())
148-
} else {
149-
(version[0].to_string(), version[1].to_string())
150-
};
167+
let (group, version) = types.api_version.gv();
151168

152169
Ok(GroupVersionKind {
153170
group,
@@ -157,22 +174,115 @@ impl GetGvk for DynamicObject {
157174
}
158175
}
159176

177+
impl GetGvk for OwnerReference {
178+
fn gvk(&self) -> Result<GroupVersionKind> {
179+
let (group, version) = self.api_version.gv();
180+
181+
Ok(GroupVersionKind {
182+
group,
183+
version,
184+
kind: self.kind.clone(),
185+
})
186+
}
187+
}
188+
189+
pub trait ApiResource {
190+
fn api_resource(&self) -> api::ApiResource;
191+
}
192+
193+
impl ApiResource for DynamicObject {
194+
fn api_resource(&self) -> api::ApiResource {
195+
api::ApiResource::from_gvk(&self.gvk().unwrap())
196+
}
197+
}
198+
199+
async fn dynamic_client(
200+
client: kube::Client,
201+
namespace: &str,
202+
gvk: &GroupVersionKind,
203+
) -> Result<Api<DynamicObject>> {
204+
let (ar, caps) = pinned_kind(&client, gvk).await?;
205+
206+
if matches!(caps.scope, Scope::Namespaced) {
207+
Ok(Api::namespaced_with(client, namespace, &ar))
208+
} else {
209+
Ok(Api::all_with(client, &ar))
210+
}
211+
}
212+
160213
pub trait DynamicClient {
161214
async fn dynamic(&self, client: kube::Client) -> Result<Api<DynamicObject>>;
162215
}
163216

164217
impl DynamicClient for DynamicObject {
165218
async fn dynamic(&self, client: kube::Client) -> Result<Api<DynamicObject>> {
166-
let (ar, caps) = pinned_kind(&client, &self.gvk()?).await?;
167-
168-
if matches!(caps.scope, Scope::Namespaced) {
169-
Ok(Api::namespaced_with(
170-
client,
171-
self.namespace().unwrap_or_default().as_str(),
172-
&ar,
173-
))
174-
} else {
175-
Ok(Api::all_with(client, &ar))
219+
dynamic_client(
220+
client,
221+
self.namespace().unwrap_or_default().as_str(),
222+
&self.gvk()?,
223+
)
224+
.await
225+
}
226+
}
227+
228+
#[async_trait::async_trait]
229+
pub(crate) trait GetOwners {
230+
fn get_owners(&self, client: kube::Client) -> impl Stream<Item = Result<DynamicObject>>;
231+
}
232+
233+
#[async_trait::async_trait]
234+
impl GetOwners for ObjectMeta {
235+
fn get_owners(&self, client: kube::Client) -> impl Stream<Item = Result<DynamicObject>> {
236+
futures::stream::iter(self.owner_references.clone().unwrap_or_default())
237+
.map(move |reference| {
238+
let client = client.clone();
239+
let namespace = self.namespace.clone().unwrap_or_default();
240+
241+
async move {
242+
let resource = dynamic_client(client, namespace.as_str(), &reference.gvk()?)
243+
.await?
244+
.get(reference.name.as_str())
245+
.await?;
246+
247+
Ok::<DynamicObject, Report>(resource)
248+
}
249+
})
250+
.buffered(100)
251+
}
252+
}
253+
254+
pub(crate) trait NamedReference {
255+
fn named_ref<N, NS>(name: N, namespace: Option<NS>) -> ObjectReference
256+
where
257+
N: Into<String>,
258+
NS: Into<String>;
259+
}
260+
261+
impl<K> NamedReference for K
262+
where
263+
K: Resource,
264+
<K as Resource>::DynamicType: Default,
265+
{
266+
fn named_ref<N, NS>(name: N, namespace: Option<NS>) -> ObjectReference
267+
where
268+
N: Into<String>,
269+
NS: Into<String>,
270+
{
271+
let namespace = namespace.map(Into::into);
272+
273+
ObjectReference {
274+
api_version: Some(K::api_version(&K::DynamicType::default()).to_string()),
275+
field_path: None,
276+
kind: Some(K::kind(&K::DynamicType::default()).to_string()),
277+
name: Some(name.into()),
278+
namespace,
279+
resource_version: None,
280+
uid: None,
176281
}
177282
}
178283
}
284+
285+
#[async_trait::async_trait]
286+
pub(crate) trait ResourceGraph {
287+
async fn graph(&self, client: &kube::Client) -> Result<Graph<ObjectReference, ()>>;
288+
}

src/resources/pod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod graph;
12
pub mod proc;
23

34
use std::{borrow::Borrow, cmp::Ordering, net::IpAddr, sync::Arc};

0 commit comments

Comments
 (0)