Skip to content

Commit 7d34c98

Browse files
docs: add comprehensive presentation outline and rladmin comparison (#415)
* docs: add comprehensive presentation outline and rladmin comparison Add finalized presentation materials: PRESENTATION_OUTLINE.md: - Complete 20-25 minute presentation flow - Four-layer architecture walkthrough (Raw API → Human → Workflows → Tools) - Live demo sections with code examples - Time allocations for each section - Q&A preparation - Key takeaways RLADMIN_COMPARISON.md: - Comprehensive feature matrix (30+ features) - Side-by-side comparison of rladmin vs redisctl enterprise - Use case guidance (when to use each) - Example comparisons for common tasks - Conclusion: complementary tools Presentation flow: 1. Current state (Cloud UI/TF, Enterprise rladmin/API) 2. The problem (no CLI existed) 3. Enter redisctl (first CLI tool) 4. Setup and profiles 5. Four-layer architecture deep dive 6. Advanced features (JMESPath, streaming, support packages) 7. rladmin comparison 8. Library architecture (platform vision) 9. Live demo 10. Roadmap and call to action Related: #412 (presentation planning) * docs: add walkthrough page linking to presentation materials Add comprehensive walkthrough page to documentation that: - Links to presentation materials in examples/presentation/ - Provides quick overview of the presentation flow - Serves as both presentation script and self-guided tutorial - Can be used for onboarding new users The walkthrough covers: 1. The problem (why redisctl exists) 2. Enter redisctl (first CLI tool) 3. Four-layer architecture 4. Advanced features 5. Library architecture 6. Hands-on tutorial section 7. Links to deep-dive documentation Benefits: - Single source of truth for presentation content - Available in docs for anyone to read later - Links to actual presentation files in repo - Reduces duplication (references existing cookbook/reference docs) * docs: restructure walkthrough as multi-page presentation Transform walkthrough into presentation-style format: - Each page is a presentation slide - Navigate with arrow keys or sidebar - Can be used as speaker notes during presentations - Also works as self-guided tutorial Structure (10 pages + appendix): 1. The Problem - Current state, why redisctl exists 2. Enter redisctl - First CLI tool, key benefits 3. Installation & Setup - Get started, profiles 4. Raw API Layer - Direct REST access 5. Human-Friendly Layer - Better UX, type-safe 6. Workflows Layer - Multi-step orchestration 7. Advanced Features - JMESPath, support packages, streaming 8. Library Architecture - Platform vision, reusable components 9. Next Steps - Try it, get involved, roadmap Appendix - rladmin comparison Benefits: - Presentation-friendly navigation (page by page) - Each page is focused and concise - Natural flow from problem to solution - Works for both live presentations and self-study - Links to deep-dive documentation Duration: 20-25 minutes as presentation, 45-60 minutes hands-on * docs: add rladmin vs redisctl comparison to documentation - Create new Comparison section in docs - Add comprehensive rladmin vs redisctl feature comparison - Include practical examples and use cases - Show that tools are complementary, not competing - Link to planned enhancement issues (#417-420) Closes #423 * feat(enterprise): add comprehensive status command Add 'status' command that displays cluster, nodes, databases, and shards in a single view, similar to rladmin's 'status extra all' command. Features: - Single command shows all cluster information - Optional section filtering (--cluster, --nodes, --databases, --shards) - Calculates summary statistics (health, totals, active counts) - Supports JSON, YAML, and table output formats - Works with JMESPath queries for filtering - Sequential API calls for simplicity and reliability Also fixed: - Shard.node_uid type changed from u32 to String (matches API response) Closes #420 * fix: update shard tests and suppress false dead code warnings - Fix shard tests to use String for node_uid (matches API response) - Update mock data in shard_tests.rs (node_uid: 1 -> "1") - Update test assertions to compare with strings - Add #[allow(dead_code)] to status.rs structs/functions (they are used via main.rs routing but clippy doesn't detect it) All tests now pass: - cargo test --lib --all-features ✓ - cargo test --test '*' --all-features ✓ - cargo clippy --all-targets --all-features ✓ - cargo fmt --all -- --check ✓ * docs: simplify walkthrough structure and add clean demo script - Flatten walkthrough navigation (8, 8.1, 8.2, etc. instead of separate top-level items) - Create DEMO.md with clean copy-paste commands for live presentation - Organized by demo flow: problem -> solution -> features -> automation - Includes setup, recovery commands, and talking points - Commands tested and ready to run This makes the presentation cleaner and gives you a reliable script to practice and present from. * feat: add --config-file flag for alternate configuration file (#430) - Add --config-file global flag and REDISCTL_CONFIG_FILE environment variable - Add Config::load_from_path() and Config::save_to_path() methods - Update ConnectionManager to track and use alternate config path - Update all profile commands to save to the correct location - Tested with both flag and environment variable Closes #428 * docs: fix brew tap command and set mdbook folding to closed by default - Change brew tap from joshrotenberg/tap to joshrotenberg/brew - Set mdbook fold level to 0 (closed by default) * docs: add credential priority documentation and fix brew tap command - Add credential hierarchy section to troubleshooting - Add credential priority to profile setup docs - Fix brew tap command to joshrotenberg/brew - Set mdbook fold level to 0 (closed by default) - Update DEMO.md profile commands to use --deployment flag
1 parent 2f5c6a3 commit 7d34c98

File tree

25 files changed

+2836
-169
lines changed

25 files changed

+2836
-169
lines changed

crates/redis-enterprise/src/shards.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub struct MetricResponse {
2525
pub struct Shard {
2626
pub uid: String,
2727
pub bdb_uid: u32,
28-
pub node_uid: u32,
28+
pub node_uid: String,
2929
pub role: String,
3030
pub status: String,
3131
#[serde(skip_serializing_if = "Option::is_none")]

crates/redis-enterprise/tests/shard_tests.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ fn master_shard() -> serde_json::Value {
1414
json!({
1515
"uid": "shard:1:1",
1616
"bdb_uid": 1,
17-
"node_uid": 1,
17+
"node_uid": "1",
1818
"role": "master",
1919
"status": "active",
2020
"slots": "0-8191",
@@ -28,7 +28,7 @@ fn replica_shard() -> serde_json::Value {
2828
json!({
2929
"uid": "shard:1:2",
3030
"bdb_uid": 1,
31-
"node_uid": 2,
31+
"node_uid": "2",
3232
"role": "slave",
3333
"status": "active",
3434
"slots": "0-8191",
@@ -40,7 +40,7 @@ fn backup_shard() -> serde_json::Value {
4040
json!({
4141
"uid": "shard:2:1",
4242
"bdb_uid": 2,
43-
"node_uid": 1,
43+
"node_uid": "1",
4444
"role": "master",
4545
"status": "backing-up",
4646
"slots": "8192-16383",
@@ -53,7 +53,7 @@ fn importing_shard() -> serde_json::Value {
5353
json!({
5454
"uid": "shard:3:1",
5555
"bdb_uid": 3,
56-
"node_uid": 3,
56+
"node_uid": "3",
5757
"role": "master",
5858
"status": "importing",
5959
"slots": "0-16383",
@@ -66,7 +66,7 @@ fn minimal_shard() -> serde_json::Value {
6666
json!({
6767
"uid": "shard:4:1",
6868
"bdb_uid": 4,
69-
"node_uid": 1,
69+
"node_uid": "1",
7070
"role": "master",
7171
"status": "active"
7272
})
@@ -132,7 +132,7 @@ async fn test_shard_list() {
132132
let master = &shards[0];
133133
assert_eq!(master.uid, "shard:1:1");
134134
assert_eq!(master.bdb_uid, 1);
135-
assert_eq!(master.node_uid, 1);
135+
assert_eq!(master.node_uid, "1");
136136
assert_eq!(master.role, "master");
137137
assert_eq!(master.status, "active");
138138
assert_eq!(master.slots, Some("0-8191".to_string()));
@@ -219,7 +219,7 @@ async fn test_shard_get() {
219219
let shard = result.unwrap();
220220
assert_eq!(shard.uid, "shard:1:1");
221221
assert_eq!(shard.bdb_uid, 1);
222-
assert_eq!(shard.node_uid, 1);
222+
assert_eq!(shard.node_uid, "1");
223223
assert_eq!(shard.role, "master");
224224
assert_eq!(shard.status, "active");
225225
assert_eq!(shard.slots, Some("0-8191".to_string()));
@@ -252,7 +252,7 @@ async fn test_shard_get_minimal() {
252252
let shard = result.unwrap();
253253
assert_eq!(shard.uid, "shard:4:1");
254254
assert_eq!(shard.bdb_uid, 4);
255-
assert_eq!(shard.node_uid, 1);
255+
assert_eq!(shard.node_uid, "1");
256256
assert_eq!(shard.role, "master");
257257
assert_eq!(shard.status, "active");
258258
assert!(shard.slots.is_none());
@@ -453,9 +453,9 @@ async fn test_shard_list_by_node() {
453453
assert_eq!(shards.len(), 3);
454454

455455
// All shards should be on node 1
456-
assert_eq!(shards[0].node_uid, 1);
457-
assert_eq!(shards[1].node_uid, 1);
458-
assert_eq!(shards[2].node_uid, 1);
456+
assert_eq!(shards[0].node_uid, "1");
457+
assert_eq!(shards[1].node_uid, "1");
458+
assert_eq!(shards[2].node_uid, "1");
459459

460460
// Verify different databases
461461
assert_eq!(shards[0].bdb_uid, 1);

crates/redisctl/src/cli.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,25 @@ pub enum EnterpriseCommands {
13231323
#[command(subcommand)]
13241324
Stats(EnterpriseStatsCommands),
13251325

1326+
/// Comprehensive cluster status (cluster, nodes, databases, shards)
1327+
Status {
1328+
/// Show only cluster information
1329+
#[arg(long)]
1330+
cluster: bool,
1331+
1332+
/// Show only nodes information
1333+
#[arg(long)]
1334+
nodes: bool,
1335+
1336+
/// Show only databases information
1337+
#[arg(long)]
1338+
databases: bool,
1339+
1340+
/// Show only shards information
1341+
#[arg(long)]
1342+
shards: bool,
1343+
},
1344+
13261345
/// Support package generation for troubleshooting
13271346
#[command(subcommand, name = "support-package")]
13281347
SupportPackage(crate::commands::enterprise::support_package::SupportPackageCommands),

crates/redisctl/src/commands/enterprise/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub mod rbac_impl;
3535
pub mod services;
3636
pub mod shard;
3737
pub mod stats;
38+
pub mod status;
3839
pub mod suffix;
3940
pub mod support_package;
4041
pub mod usage_report;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
//! Comprehensive status command implementation for Redis Enterprise
2+
//!
3+
//! Provides a single command to view cluster, nodes, databases, and shards status,
4+
//! similar to `rladmin status extra all`.
5+
6+
use crate::cli::OutputFormat;
7+
use crate::connection::ConnectionManager;
8+
use crate::error::Result as CliResult;
9+
use anyhow::Context;
10+
use redis_enterprise::bdb::BdbHandler;
11+
use redis_enterprise::cluster::ClusterHandler;
12+
use redis_enterprise::nodes::NodeHandler;
13+
use redis_enterprise::shards::ShardHandler;
14+
use serde::{Deserialize, Serialize};
15+
use serde_json::{Value, json};
16+
17+
use super::utils::*;
18+
19+
/// Comprehensive cluster status information
20+
#[derive(Debug, Clone, Serialize, Deserialize)]
21+
#[allow(dead_code)]
22+
pub struct ClusterStatus {
23+
/// Cluster information
24+
pub cluster: Value,
25+
/// List of nodes
26+
pub nodes: Value,
27+
/// List of databases
28+
pub databases: Value,
29+
/// List of shards
30+
pub shards: Value,
31+
/// Summary statistics
32+
pub summary: StatusSummary,
33+
}
34+
35+
/// Summary statistics for cluster health
36+
#[derive(Debug, Clone, Serialize, Deserialize)]
37+
#[allow(dead_code)]
38+
pub struct StatusSummary {
39+
/// Total number of nodes
40+
pub total_nodes: usize,
41+
/// Number of healthy nodes
42+
pub healthy_nodes: usize,
43+
/// Total number of databases
44+
pub total_databases: usize,
45+
/// Number of active databases
46+
pub active_databases: usize,
47+
/// Total number of shards
48+
pub total_shards: usize,
49+
/// Cluster health status
50+
pub cluster_health: String,
51+
}
52+
53+
/// Sections to display in status output
54+
#[derive(Debug, Clone, Default)]
55+
#[allow(dead_code)]
56+
pub struct StatusSections {
57+
/// Show cluster information
58+
pub cluster: bool,
59+
/// Show nodes information
60+
pub nodes: bool,
61+
/// Show databases information
62+
pub databases: bool,
63+
/// Show shards information
64+
pub shards: bool,
65+
}
66+
67+
impl StatusSections {
68+
/// Create sections showing all information
69+
#[allow(dead_code)]
70+
pub fn all() -> Self {
71+
Self {
72+
cluster: true,
73+
nodes: true,
74+
databases: true,
75+
shards: true,
76+
}
77+
}
78+
79+
/// Check if any section is enabled
80+
#[allow(dead_code)]
81+
pub fn any_enabled(&self) -> bool {
82+
self.cluster || self.nodes || self.databases || self.shards
83+
}
84+
}
85+
86+
/// Get comprehensive cluster status
87+
#[allow(dead_code)]
88+
pub async fn get_status(
89+
conn_mgr: &ConnectionManager,
90+
profile_name: Option<&str>,
91+
sections: StatusSections,
92+
output_format: OutputFormat,
93+
query: Option<&str>,
94+
) -> CliResult<()> {
95+
let client = conn_mgr.create_enterprise_client(profile_name).await?;
96+
97+
// Use provided sections, or default to all if none specified
98+
let sections = if sections.any_enabled() {
99+
sections
100+
} else {
101+
StatusSections::all()
102+
};
103+
104+
// Collect cluster info
105+
let cluster_result = if sections.cluster {
106+
ClusterHandler::new(client.clone())
107+
.info()
108+
.await
109+
.map(|v| serde_json::to_value(v).unwrap_or(json!({})))
110+
.context("Failed to get cluster info")?
111+
} else {
112+
json!({})
113+
};
114+
115+
// Collect nodes
116+
let nodes_result = if sections.nodes {
117+
NodeHandler::new(client.clone())
118+
.list()
119+
.await
120+
.map(|v| serde_json::to_value(v).unwrap_or(json!([])))
121+
.context("Failed to list nodes")?
122+
} else {
123+
json!([])
124+
};
125+
126+
// Collect databases
127+
let databases_result = if sections.databases {
128+
BdbHandler::new(client.clone())
129+
.list()
130+
.await
131+
.map(|v| serde_json::to_value(v).unwrap_or(json!([])))
132+
.context("Failed to list databases")?
133+
} else {
134+
json!([])
135+
};
136+
137+
// Collect shards
138+
let shards_result = if sections.shards {
139+
ShardHandler::new(client.clone())
140+
.list()
141+
.await
142+
.map(|v| serde_json::to_value(v).unwrap_or(json!([])))
143+
.context("Failed to list shards")?
144+
} else {
145+
json!([])
146+
};
147+
148+
// Calculate summary statistics
149+
let summary = calculate_summary(&nodes_result, &databases_result, &shards_result);
150+
151+
// Build comprehensive status
152+
let status = ClusterStatus {
153+
cluster: cluster_result,
154+
nodes: nodes_result,
155+
databases: databases_result,
156+
shards: shards_result,
157+
summary,
158+
};
159+
160+
let status_json = serde_json::to_value(status).context("Failed to serialize cluster status")?;
161+
162+
// Apply query if provided
163+
let data = handle_output(status_json, output_format, query)?;
164+
165+
// Format and display
166+
print_formatted_output(data, output_format)?;
167+
168+
Ok(())
169+
}
170+
171+
/// Calculate summary statistics from collected data
172+
#[allow(dead_code)]
173+
fn calculate_summary(nodes: &Value, databases: &Value, shards: &Value) -> StatusSummary {
174+
let empty_vec = vec![];
175+
let nodes_array = nodes.as_array().unwrap_or(&empty_vec);
176+
let databases_array = databases.as_array().unwrap_or(&empty_vec);
177+
let shards_array = shards.as_array().unwrap_or(&empty_vec);
178+
179+
let total_nodes = nodes_array.len();
180+
let healthy_nodes = nodes_array
181+
.iter()
182+
.filter(|n| {
183+
n.get("status")
184+
.and_then(|s| s.as_str())
185+
.map(|s| s == "active" || s == "ok")
186+
.unwrap_or(false)
187+
})
188+
.count();
189+
190+
let total_databases = databases_array.len();
191+
let active_databases = databases_array
192+
.iter()
193+
.filter(|db| {
194+
db.get("status")
195+
.and_then(|s| s.as_str())
196+
.map(|s| s == "active")
197+
.unwrap_or(false)
198+
})
199+
.count();
200+
201+
let total_shards = shards_array.len();
202+
203+
// Determine cluster health
204+
let cluster_health = if healthy_nodes == total_nodes && active_databases == total_databases {
205+
"healthy".to_string()
206+
} else if healthy_nodes == 0 || active_databases == 0 {
207+
"critical".to_string()
208+
} else {
209+
"degraded".to_string()
210+
};
211+
212+
StatusSummary {
213+
total_nodes,
214+
healthy_nodes,
215+
total_databases,
216+
active_databases,
217+
total_shards,
218+
cluster_health,
219+
}
220+
}

crates/redisctl/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,21 @@ async fn execute_enterprise_command(
485485
)
486486
.await
487487
}
488+
Status {
489+
cluster,
490+
nodes,
491+
databases,
492+
shards,
493+
} => {
494+
let sections = commands::enterprise::status::StatusSections {
495+
cluster: *cluster,
496+
nodes: *nodes,
497+
databases: *databases,
498+
shards: *shards,
499+
};
500+
commands::enterprise::status::get_status(conn_mgr, profile, sections, output, query)
501+
.await
502+
}
488503
SupportPackage(support_cmd) => {
489504
commands::enterprise::support_package::handle_support_package_command(
490505
conn_mgr,

0 commit comments

Comments
 (0)