Skip to content

Commit c64621c

Browse files
authored
Add Describe, change 'd' to describe, change delete (#150)
1 parent dea6c9c commit c64621c

23 files changed

Lines changed: 981 additions & 231 deletions

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/configuration/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ flux9s stores its configuration in a YAML file. The location depends on your ope
2121

2222
### Read-Only Mode
2323

24-
By default, flux9s launches in readonly mode to prevent accidental changes (Delete operations always have a confirmation screen).
24+
By default, flux9s launches in readonly mode to prevent accidental changes (`Ctrl+d` delete operations always have a confirmation screen).
2525
You can change this:
2626

2727
**Via command line:**

docs/content/user-guide/_index.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,13 @@ Use these keyboard shortcuts to navigate flux9s:
6060
| `r` | Resume reconciliation |
6161
| `R` | Reconcile resource |
6262
| `y` | View resource YAML |
63+
| `d` | View describe output |
6364
| `f` | Toggle favorite |
6465
| `g` | View resource graph (Kustomization, HelmRelease, etc.) |
6566
| `h` | View reconciliation history |
6667
| `t` | Trace ownership chain |
6768
| `W` | Reconcile with source |
68-
| `d` | Delete resource |
69+
| `Ctrl+d` | Delete resource (with confirmation) |
6970
| `?` | Show/hide help |
7071
| `q` / `Esc` | Go back; shows a quit prompt when at the root view |
7172
| `Q` | Quit immediately (no prompt) |
@@ -222,7 +223,7 @@ Perform actions on selected resources:
222223
| `r` | Resume reconciliation | GitRepository, OCIRepository, HelmRepository, Kustomization, HelmRelease, ImageUpdateAutomation |
223224
| `R` | Reconcile resource | All Flux resources (cannot reconcile suspended resources) |
224225
| `W` | Reconcile with source | Kustomization, HelmRelease only |
225-
| `d` | Delete resource | All Flux resources (with confirmation) |
226+
| `Ctrl+d` | Delete resource | All Flux resources (with confirmation) |
226227

227228
**Note:** Suspend and Resume operations are only available for resources that support the `spec.suspend` field. Reconcile operations will fail if the resource is currently suspended.
228229

justfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ test-integration:
2222

2323
# Run cargo-audit to check for CVEs (ignores unmaintained warnings)
2424
audit:
25-
cargo audit --ignore RUSTSEC-2024-0436
25+
# RUSTSEC-2026-0002 is currently pulled in transitively via ratatui 0.29's lru dependency.
26+
cargo audit --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2026-0002
2627

2728
# Run all CI checks in order
2829
ci: fmt clippy audit test test-integration

src/constants.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ pub const MAX_RECONCILIATION_HISTORY: usize = 50;
1212
/// Status message timeout in seconds
1313
pub const STATUS_MESSAGE_TIMEOUT_SECS: u64 = 4;
1414

15+
/// Status message shown when a write action is attempted in readonly mode
16+
pub const READ_ONLY_WRITE_ACTION_MESSAGE: &str =
17+
"Readonly mode is enabled. Use :readonly to toggle write actions.";
18+
1519
/// Minimum terminal width required for the TUI
1620
pub const MIN_TERMINAL_WIDTH: u16 = 80;
1721

src/kube/fetch.rs

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,47 @@
11
//! Resource fetching utilities
22
//!
3-
//! Provides functions for fetching Flux resource YAML from the Kubernetes API.
3+
//! Provides functions for fetching full Kubernetes resources from the API.
44
5-
use crate::models::FluxResourceKind;
6-
use crate::watcher::{
7-
Alert, ArtifactGenerator, Bucket, ExternalArtifact, FluxInstance, FluxReport, GitRepository,
8-
HelmChart, HelmRelease, HelmRepository, ImagePolicy, ImageRepository, ImageUpdateAutomation,
9-
Kustomization, OCIRepository, Provider, Receiver, ResourceSet, ResourceSetInputProvider,
10-
};
5+
use anyhow::Context;
116
use kube::Api;
7+
use kube::core::DynamicObject;
128

13-
/// Fetch resource YAML from the Kubernetes API
14-
pub async fn fetch_resource_yaml(
9+
use crate::kube::get_api_resource_with_fallback;
10+
11+
/// Fetch a full resource object from the Kubernetes API using discovery-backed version fallback.
12+
pub async fn fetch_resource(
1513
client: &kube::Client,
1614
resource_type: &str,
1715
namespace: &str,
1816
name: &str,
1917
) -> anyhow::Result<serde_json::Value> {
20-
// Match resource type and fetch using appropriate API
21-
macro_rules! fetch_resource {
22-
($type:ty) => {{
23-
let api: Api<$type> = Api::namespaced(client.clone(), namespace);
24-
match api.get(name).await {
25-
Ok(obj) => {
26-
return Ok(serde_json::to_value(&obj)?);
27-
}
28-
Err(e) => {
29-
return Err(anyhow::anyhow!("Failed to fetch {}: {}", resource_type, e));
30-
}
31-
}
32-
}};
33-
}
18+
let api_resource = get_api_resource_with_fallback(client, resource_type, namespace, name)
19+
.await
20+
.with_context(|| {
21+
format!(
22+
"Failed to discover API resource for {}/{} in namespace {}",
23+
resource_type, name, namespace
24+
)
25+
})?;
26+
let api: Api<DynamicObject> = Api::namespaced_with(client.clone(), namespace, &api_resource);
27+
let obj = api.get(name).await.with_context(|| {
28+
format!(
29+
"Failed to fetch {}/{} in namespace {}",
30+
resource_type, name, namespace
31+
)
32+
})?;
3433

35-
match FluxResourceKind::parse_optional(resource_type) {
36-
Some(FluxResourceKind::GitRepository) => fetch_resource!(GitRepository),
37-
Some(FluxResourceKind::OCIRepository) => fetch_resource!(OCIRepository),
38-
Some(FluxResourceKind::HelmRepository) => fetch_resource!(HelmRepository),
39-
Some(FluxResourceKind::Bucket) => fetch_resource!(Bucket),
40-
Some(FluxResourceKind::HelmChart) => fetch_resource!(HelmChart),
41-
Some(FluxResourceKind::ExternalArtifact) => fetch_resource!(ExternalArtifact),
42-
Some(FluxResourceKind::ArtifactGenerator) => fetch_resource!(ArtifactGenerator),
43-
Some(FluxResourceKind::Kustomization) => fetch_resource!(Kustomization),
44-
Some(FluxResourceKind::HelmRelease) => fetch_resource!(HelmRelease),
45-
Some(FluxResourceKind::ImageRepository) => fetch_resource!(ImageRepository),
46-
Some(FluxResourceKind::ImagePolicy) => fetch_resource!(ImagePolicy),
47-
Some(FluxResourceKind::ImageUpdateAutomation) => fetch_resource!(ImageUpdateAutomation),
48-
Some(FluxResourceKind::Alert) => fetch_resource!(Alert),
49-
Some(FluxResourceKind::Provider) => fetch_resource!(Provider),
50-
Some(FluxResourceKind::Receiver) => fetch_resource!(Receiver),
51-
Some(FluxResourceKind::ResourceSet) => fetch_resource!(ResourceSet),
52-
Some(FluxResourceKind::ResourceSetInputProvider) => {
53-
fetch_resource!(ResourceSetInputProvider)
54-
}
55-
Some(FluxResourceKind::FluxReport) => fetch_resource!(FluxReport),
56-
Some(FluxResourceKind::FluxInstance) => fetch_resource!(FluxInstance),
57-
None => Err(anyhow::anyhow!("Unknown resource type: {}", resource_type)),
58-
}
34+
serde_json::to_value(&obj).context("Failed to serialize fetched resource")
35+
}
36+
37+
/// Fetch resource data for the YAML view.
38+
///
39+
/// This remains as a compatibility wrapper around the generic resource fetch path.
40+
pub async fn fetch_resource_yaml(
41+
client: &kube::Client,
42+
resource_type: &str,
43+
namespace: &str,
44+
name: &str,
45+
) -> anyhow::Result<serde_json::Value> {
46+
fetch_resource(client, resource_type, namespace, name).await
5947
}

src/kube/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub mod inventory;
1818
#[allow(unused_imports)] // Public API re-exports used by lib consumers
1919
pub use api::{get_api_resource_with_fallback, get_gvk_for_resource_type};
2020
#[allow(unused_imports)] // Public API re-exports used by lib consumers
21-
pub use fetch::fetch_resource_yaml;
21+
pub use fetch::{fetch_resource, fetch_resource_yaml};
2222

2323
use anyhow::Result;
2424
use kube::config::Kubeconfig;

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ pub use services::ClusterSession;
4848
pub use operations::{FluxOperation, OperationRegistry};
4949

5050
// Re-export kube utilities
51-
pub use kube::{fetch_resource_yaml, get_api_resource_with_fallback, get_gvk_for_resource_type};
51+
pub use kube::{
52+
fetch_resource, fetch_resource_yaml, get_api_resource_with_fallback, get_gvk_for_resource_type,
53+
};
5254

5355
// Re-export trace types
5456
pub use trace::{

src/tui/app/async_ops.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,27 @@ impl App {
6060
None
6161
}
6262

63+
/// Trigger describe fetch if pending
64+
pub fn trigger_describe_fetch(
65+
&mut self,
66+
) -> Option<(
67+
String,
68+
kube::Client,
69+
tokio::sync::oneshot::Sender<anyhow::Result<serde_json::Value>>,
70+
)> {
71+
if let Some(ref key) = self.async_state.describe_fetch_pending {
72+
if let Some(ref client) = self.kube_client {
73+
let (tx, rx) = tokio::sync::oneshot::channel();
74+
let key_clone = key.clone();
75+
let client_clone = client.clone();
76+
self.async_state.describe_fetch_pending = None;
77+
self.async_state.describe_fetch_rx = Some(rx);
78+
return Some((key_clone, client_clone, tx));
79+
}
80+
}
81+
None
82+
}
83+
6384
/// Set YAML fetch result
6485
pub fn set_yaml_fetched(&mut self, yaml: serde_json::Value) {
6586
self.async_state.yaml_fetched = Some(yaml);
@@ -71,6 +92,17 @@ impl App {
7192
self.async_state.yaml_fetch_pending = None;
7293
}
7394

95+
/// Set describe fetch result
96+
pub fn set_describe_fetched(&mut self, describe: serde_json::Value) {
97+
self.async_state.describe_fetched = Some(describe);
98+
}
99+
100+
/// Set describe fetch error
101+
pub fn set_describe_fetch_error(&mut self) {
102+
self.async_state.describe_fetched = None;
103+
self.async_state.describe_fetch_pending = None;
104+
}
105+
74106
/// Try to get YAML fetch result
75107
pub fn try_get_yaml_result(&mut self) -> Option<anyhow::Result<serde_json::Value>> {
76108
if let Some(ref mut rx) = self.async_state.yaml_fetch_rx {
@@ -91,6 +123,26 @@ impl App {
91123
None
92124
}
93125

126+
/// Try to get describe fetch result
127+
pub fn try_get_describe_result(&mut self) -> Option<anyhow::Result<serde_json::Value>> {
128+
if let Some(ref mut rx) = self.async_state.describe_fetch_rx {
129+
match rx.try_recv() {
130+
Ok(result) => {
131+
self.async_state.describe_fetch_rx = None;
132+
return Some(result);
133+
}
134+
Err(tokio::sync::oneshot::error::TryRecvError::Empty) => {
135+
return None;
136+
}
137+
Err(_) => {
138+
self.async_state.describe_fetch_rx = None;
139+
return Some(Err(anyhow::anyhow!("Describe fetch failed")));
140+
}
141+
}
142+
}
143+
None
144+
}
145+
94146
/// Trigger trace if pending
95147
pub fn trigger_trace(&mut self) -> Option<TraceRequest> {
96148
if let Some(ref rk) = self.async_state.trace_pending {

src/tui/app/core.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ impl App {
307307
}
308308

309309
#[cfg(test)]
310+
#[allow(dead_code)]
310311
pub fn set_previous_list_view(&mut self, view: View) {
311312
self.view_state.previous_list_view = view;
312313
}
@@ -372,7 +373,7 @@ impl App {
372373
let resources = self.get_filtered_resources();
373374
resources.get(self.view_state.selected_index).cloned()
374375
}
375-
View::ResourceDetail => self
376+
View::ResourceDetail | View::ResourceDescribe => self
376377
.selection_state
377378
.selected_resource_key
378379
.as_ref()
@@ -530,6 +531,10 @@ impl std::fmt::Debug for App {
530531
.field("selected_index", &self.view_state.selected_index)
531532
.field("scroll_offset", &self.view_state.scroll_offset)
532533
.field("yaml_scroll_offset", &self.view_state.yaml_scroll_offset)
534+
.field(
535+
"describe_scroll_offset",
536+
&self.view_state.describe_scroll_offset,
537+
)
533538
.field("show_help", &self.ui_state.show_help)
534539
.field("context", &self.context)
535540
.field("namespace", &self.namespace)
@@ -545,6 +550,18 @@ impl std::fmt::Debug for App {
545550
.field("yaml_fetch_pending", &self.async_state.yaml_fetch_pending)
546551
.field("yaml_fetched", &self.async_state.yaml_fetched.is_some())
547552
.field("yaml_fetch_rx", &self.async_state.yaml_fetch_rx.is_some())
553+
.field(
554+
"describe_fetch_pending",
555+
&self.async_state.describe_fetch_pending,
556+
)
557+
.field(
558+
"describe_fetched",
559+
&self.async_state.describe_fetched.is_some(),
560+
)
561+
.field(
562+
"describe_fetch_rx",
563+
&self.async_state.describe_fetch_rx.is_some(),
564+
)
548565
.field("trace_pending", &self.async_state.trace_pending)
549566
.field("trace_result", &self.async_state.trace_result.is_some())
550567
.field(

0 commit comments

Comments
 (0)