Skip to content

Commit 52672e7

Browse files
feat(enterprise): implement cluster manager settings commands (#164) (#292)
- Add Get command to retrieve all or specific settings - Add Set and SetValue commands to update settings - Add Reset command to restore defaults - Add Export/Import for backup and migration - Add Validate command to check settings before applying - Add ListCategories and GetCategory for organized access - Support for nested settings using dot notation - Comprehensive mdBook documentation with examples - Successfully tested against Docker environment
1 parent 2f08797 commit 52672e7

File tree

6 files changed

+735
-0
lines changed

6 files changed

+735
-0
lines changed

crates/redisctl/src/cli.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,9 @@ pub enum EnterpriseCommands {
968968
/// Cluster operations
969969
#[command(subcommand)]
970970
Cluster(EnterpriseClusterCommands),
971+
/// Cluster manager settings
972+
#[command(subcommand, name = "cm-settings")]
973+
CmSettings(crate::commands::enterprise::cm_settings::CmSettingsCommands),
971974

972975
/// Database operations
973976
#[command(subcommand)]
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
use anyhow::Context;
2+
use clap::Subcommand;
3+
4+
use crate::cli::OutputFormat;
5+
use crate::connection::ConnectionManager;
6+
use crate::error::Result as CliResult;
7+
8+
#[derive(Debug, Clone, Subcommand)]
9+
pub enum CmSettingsCommands {
10+
/// Get all cluster manager settings
11+
Get {
12+
/// Get specific setting by path using JMESPath
13+
#[arg(long)]
14+
setting: Option<String>,
15+
},
16+
17+
/// Update cluster manager settings
18+
Set {
19+
/// Settings data (JSON file or inline, use @filename or - for stdin)
20+
#[arg(short, long)]
21+
data: String,
22+
23+
/// Force update without confirmation
24+
#[arg(short, long)]
25+
force: bool,
26+
},
27+
28+
/// Update a specific setting
29+
#[command(name = "set-value")]
30+
SetValue {
31+
/// Setting name/path
32+
name: String,
33+
34+
/// New value for the setting
35+
#[arg(long)]
36+
value: String,
37+
38+
/// Force update without confirmation
39+
#[arg(short, long)]
40+
force: bool,
41+
},
42+
43+
/// Reset settings to defaults
44+
Reset {
45+
/// Force reset without confirmation
46+
#[arg(short, long)]
47+
force: bool,
48+
},
49+
50+
/// Export settings to file
51+
Export {
52+
/// Output file path (use - for stdout)
53+
#[arg(short, long, default_value = "-")]
54+
output: String,
55+
},
56+
57+
/// Import settings from file
58+
Import {
59+
/// Input file path (use @filename or - for stdin)
60+
#[arg(short, long)]
61+
file: String,
62+
63+
/// Force import without confirmation
64+
#[arg(short, long)]
65+
force: bool,
66+
},
67+
68+
/// Validate settings without applying
69+
Validate {
70+
/// Settings file to validate (use @filename or - for stdin)
71+
#[arg(short, long)]
72+
file: String,
73+
},
74+
75+
/// List all setting categories
76+
#[command(name = "list-categories")]
77+
ListCategories,
78+
79+
/// Get settings by category
80+
#[command(name = "get-category")]
81+
GetCategory {
82+
/// Category name
83+
category: String,
84+
},
85+
}
86+
87+
impl CmSettingsCommands {
88+
#[allow(dead_code)]
89+
pub async fn execute(
90+
&self,
91+
conn_mgr: &ConnectionManager,
92+
profile_name: Option<&str>,
93+
output_format: OutputFormat,
94+
query: Option<&str>,
95+
) -> CliResult<()> {
96+
let client = conn_mgr.create_enterprise_client(profile_name).await?;
97+
98+
match self {
99+
CmSettingsCommands::Get { setting } => {
100+
let response: serde_json::Value = client
101+
.get("/v1/cm_settings")
102+
.await
103+
.context("Failed to get cluster manager settings")?;
104+
105+
let output_data = if let Some(s) = setting {
106+
// Use the setting parameter as a JMESPath query
107+
super::utils::apply_jmespath(&response, s)?
108+
} else if let Some(q) = query {
109+
super::utils::apply_jmespath(&response, q)?
110+
} else {
111+
response
112+
};
113+
super::utils::print_formatted_output(output_data, output_format)?;
114+
}
115+
116+
CmSettingsCommands::Set { data, force } => {
117+
if !force && !super::utils::confirm_action("Update cluster manager settings?")? {
118+
return Ok(());
119+
}
120+
121+
let json_data = super::utils::read_json_data(data)?;
122+
123+
let response: serde_json::Value =
124+
client
125+
.put("/v1/cm_settings", &json_data)
126+
.await
127+
.context("Failed to update cluster manager settings")?;
128+
129+
println!("Cluster manager settings updated successfully");
130+
131+
let output_data = if let Some(q) = query {
132+
super::utils::apply_jmespath(&response, q)?
133+
} else {
134+
response
135+
};
136+
super::utils::print_formatted_output(output_data, output_format)?;
137+
}
138+
139+
CmSettingsCommands::SetValue { name, value, force } => {
140+
if !force && !super::utils::confirm_action(&format!("Update setting '{}'?", name))?
141+
{
142+
return Ok(());
143+
}
144+
145+
// Get current settings
146+
let mut settings: serde_json::Value = client
147+
.get("/v1/cm_settings")
148+
.await
149+
.context("Failed to get current settings")?;
150+
151+
// Parse value as JSON if possible, otherwise as string
152+
let parsed_value: serde_json::Value =
153+
serde_json::from_str(value).unwrap_or_else(|_| serde_json::json!(value));
154+
155+
// Update the specific setting
156+
if name.contains('.') {
157+
// Handle nested settings
158+
let parts: Vec<&str> = name.split('.').collect();
159+
let mut current = &mut settings;
160+
161+
for (i, part) in parts.iter().enumerate() {
162+
if i == parts.len() - 1 {
163+
current[part] = parsed_value.clone();
164+
} else {
165+
current = &mut current[part];
166+
}
167+
}
168+
} else {
169+
settings[name] = parsed_value;
170+
}
171+
172+
// Update settings
173+
let response: serde_json::Value = client
174+
.put("/v1/cm_settings", &settings)
175+
.await
176+
.context("Failed to update setting")?;
177+
178+
println!("Setting '{}' updated to: {}", name, value);
179+
180+
let output_data = if let Some(q) = query {
181+
super::utils::apply_jmespath(&response, q)?
182+
} else {
183+
response
184+
};
185+
super::utils::print_formatted_output(output_data, output_format)?;
186+
}
187+
188+
CmSettingsCommands::Reset { force } => {
189+
if !force
190+
&& !super::utils::confirm_action(
191+
"Reset all cluster manager settings to defaults?",
192+
)?
193+
{
194+
return Ok(());
195+
}
196+
197+
// Reset by sending empty object
198+
let response: serde_json::Value = client
199+
.put("/v1/cm_settings", &serde_json::json!({}))
200+
.await
201+
.context("Failed to reset settings")?;
202+
203+
println!("Cluster manager settings reset to defaults");
204+
205+
let output_data = if let Some(q) = query {
206+
super::utils::apply_jmespath(&response, q)?
207+
} else {
208+
response
209+
};
210+
super::utils::print_formatted_output(output_data, output_format)?;
211+
}
212+
213+
CmSettingsCommands::Export { output } => {
214+
let settings: serde_json::Value = client
215+
.get("/v1/cm_settings")
216+
.await
217+
.context("Failed to get settings for export")?;
218+
219+
if output == "-" {
220+
// Output to stdout
221+
super::utils::print_formatted_output(settings, output_format)?;
222+
} else {
223+
// Write to file
224+
let json_str = serde_json::to_string_pretty(&settings)
225+
.context("Failed to serialize settings")?;
226+
std::fs::write(output, json_str).context("Failed to write settings to file")?;
227+
println!("Settings exported to: {}", output);
228+
}
229+
}
230+
231+
CmSettingsCommands::Import { file, force } => {
232+
if !force
233+
&& !super::utils::confirm_action("Import cluster manager settings from file?")?
234+
{
235+
return Ok(());
236+
}
237+
238+
let json_data = super::utils::read_json_data(file)?;
239+
240+
let response: serde_json::Value = client
241+
.put("/v1/cm_settings", &json_data)
242+
.await
243+
.context("Failed to import settings")?;
244+
245+
println!("Settings imported successfully");
246+
247+
let output_data = if let Some(q) = query {
248+
super::utils::apply_jmespath(&response, q)?
249+
} else {
250+
response
251+
};
252+
super::utils::print_formatted_output(output_data, output_format)?;
253+
}
254+
255+
CmSettingsCommands::Validate { file } => {
256+
let json_data = super::utils::read_json_data(file)?;
257+
258+
// Try to validate by doing a dry-run (if supported)
259+
// For now, just validate JSON structure
260+
if json_data.is_object() {
261+
println!("Settings file is valid JSON");
262+
263+
// Check for known required fields if any
264+
let obj = json_data.as_object().unwrap();
265+
266+
// List known categories/fields for informational purposes
267+
println!("\nFound settings categories:");
268+
for key in obj.keys() {
269+
println!(" - {}", key);
270+
}
271+
} else {
272+
return Err(
273+
anyhow::anyhow!("Invalid settings format: expected JSON object").into(),
274+
);
275+
}
276+
}
277+
278+
CmSettingsCommands::ListCategories => {
279+
let settings: serde_json::Value = client
280+
.get("/v1/cm_settings")
281+
.await
282+
.context("Failed to get settings")?;
283+
284+
// Extract top-level keys as categories
285+
let categories = if let Some(obj) = settings.as_object() {
286+
let cats: Vec<String> = obj.keys().cloned().collect();
287+
serde_json::json!(cats)
288+
} else {
289+
serde_json::json!([])
290+
};
291+
292+
let output_data = if let Some(q) = query {
293+
super::utils::apply_jmespath(&categories, q)?
294+
} else {
295+
categories
296+
};
297+
super::utils::print_formatted_output(output_data, output_format)?;
298+
}
299+
300+
CmSettingsCommands::GetCategory { category } => {
301+
let settings: serde_json::Value = client
302+
.get("/v1/cm_settings")
303+
.await
304+
.context("Failed to get settings")?;
305+
306+
// Extract specific category
307+
let category_data = &settings[category];
308+
309+
if category_data.is_null() {
310+
return Err(anyhow::anyhow!("Category '{}' not found", category).into());
311+
}
312+
313+
let output_data = if let Some(q) = query {
314+
super::utils::apply_jmespath(category_data, q)?
315+
} else {
316+
category_data.clone()
317+
};
318+
super::utils::print_formatted_output(output_data, output_format)?;
319+
}
320+
}
321+
322+
Ok(())
323+
}
324+
}
325+
326+
#[allow(dead_code)]
327+
pub async fn handle_cm_settings_command(
328+
conn_mgr: &ConnectionManager,
329+
profile_name: Option<&str>,
330+
cm_settings_cmd: CmSettingsCommands,
331+
output_format: OutputFormat,
332+
query: Option<&str>,
333+
) -> CliResult<()> {
334+
cm_settings_cmd
335+
.execute(conn_mgr, profile_name, output_format, query)
336+
.await
337+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod actions;
44
pub mod bdb_group;
55
pub mod cluster;
66
pub mod cluster_impl;
7+
pub mod cm_settings;
78
pub mod crdb;
89
pub mod crdb_impl;
910
pub mod crdb_task;

crates/redisctl/src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,16 @@ async fn execute_enterprise_command(
216216
)
217217
.await
218218
}
219+
CmSettings(cm_settings_cmd) => {
220+
commands::enterprise::cm_settings::handle_cm_settings_command(
221+
conn_mgr,
222+
profile,
223+
cm_settings_cmd.clone(),
224+
output,
225+
query,
226+
)
227+
.await
228+
}
219229
Database(db_cmd) => {
220230
commands::enterprise::database::handle_database_command(
221231
conn_mgr, profile, db_cmd, output, query,

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
- [Overview](./enterprise/overview.md)
3030
- [Cluster](./enterprise/cluster.md)
31+
- [CM Settings](./enterprise/cm-settings.md)
3132
- [Databases](./enterprise/databases.md)
3233
- [Database Groups](./enterprise/bdb-groups.md)
3334
- [Shards](./enterprise/shards.md)

0 commit comments

Comments
 (0)