Skip to content

Commit 252041d

Browse files
feat: add module name lookup for module get and database create (#512)
Add --name flag to 'enterprise module get': - Look up modules by name instead of UID - Case-insensitive matching - Helpful suggestions on partial matches - Error with available versions if multiple matches found Add --module flag to 'enterprise database create': - Specify modules by name (e.g., --module search --module ReJSON) - Auto-resolves module names to proper module_list format - Supports module args via colon syntax (--module search:ARGS) - Can be repeated for multiple modules - Case-insensitive matching with helpful error messages Examples: # Get module by name redisctl enterprise module get --name ReJSON # Create database with modules redisctl enterprise database create --name mydb --memory 1073741824 \ --module search --module ReJSON
1 parent 76186d3 commit 252041d

File tree

5 files changed

+210
-7
lines changed

5 files changed

+210
-7
lines changed

crates/redisctl/src/cli/enterprise.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,10 @@ pub enum EnterpriseDatabaseCommands {
368368
# With specific port
369369
redisctl enterprise database create --name service-db --memory 1073741824 --port 12000
370370
371+
# With modules (auto-resolves name to module)
372+
redisctl enterprise database create --name search-db --memory 1073741824 \\
373+
--module search --module ReJSON
374+
371375
# Complete configuration from file
372376
redisctl enterprise database create --data @database.json
373377
@@ -424,6 +428,11 @@ NOTE: Memory size is in bytes. Common values:
424428
#[arg(long)]
425429
redis_password: Option<String>,
426430

431+
/// Module to enable (by name, can be repeated). Use 'module list' to see available modules.
432+
/// Format: module_name or module_name:args (e.g., --module search --module ReJSON)
433+
#[arg(long = "module", value_name = "NAME[:ARGS]")]
434+
modules: Vec<String>,
435+
427436
/// Advanced: Full database configuration as JSON string or @file.json
428437
#[arg(long)]
429438
data: Option<String>,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub async fn handle_database_command(
3535
proxy_policy,
3636
crdb,
3737
redis_password,
38+
modules,
3839
data,
3940
dry_run,
4041
} => {
@@ -52,6 +53,7 @@ pub async fn handle_database_command(
5253
proxy_policy.as_deref(),
5354
*crdb,
5455
redis_password.as_deref(),
56+
modules,
5557
data.as_deref(),
5658
*dry_run,
5759
output_format,

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub async fn create_database(
6262
proxy_policy: Option<&str>,
6363
crdb: bool,
6464
redis_password: Option<&str>,
65+
modules: &[String],
6566
data: Option<&str>,
6667
dry_run: bool,
6768
output_format: OutputFormat,
@@ -145,6 +146,107 @@ pub async fn create_database(
145146
);
146147
}
147148

149+
// Handle module resolution if --module flags were provided
150+
if !modules.is_empty() {
151+
let module_handler = redis_enterprise::ModuleHandler::new(
152+
conn_mgr.create_enterprise_client(profile_name).await?,
153+
);
154+
let available_modules = module_handler.list().await.map_err(RedisCtlError::from)?;
155+
156+
let mut module_list: Vec<Value> = Vec::new();
157+
158+
for module_spec in modules {
159+
// Parse module_name:args format
160+
let (module_name, module_args) = if let Some(idx) = module_spec.find(':') {
161+
let (name, args) = module_spec.split_at(idx);
162+
(name.trim(), Some(args[1..].trim())) // Skip the ':' character
163+
} else {
164+
(module_spec.as_str(), None)
165+
};
166+
167+
// Find matching module (case-insensitive)
168+
let matching: Vec<_> = available_modules
169+
.iter()
170+
.filter(|m| {
171+
m.module_name
172+
.as_ref()
173+
.map(|n| n.eq_ignore_ascii_case(module_name))
174+
.unwrap_or(false)
175+
})
176+
.collect();
177+
178+
match matching.len() {
179+
0 => {
180+
// No exact match - try partial match and suggest
181+
let partial_matches: Vec<_> = available_modules
182+
.iter()
183+
.filter(|m| {
184+
m.module_name
185+
.as_ref()
186+
.map(|n| n.to_lowercase().contains(&module_name.to_lowercase()))
187+
.unwrap_or(false)
188+
})
189+
.collect();
190+
191+
if partial_matches.is_empty() {
192+
return Err(RedisCtlError::InvalidInput {
193+
message: format!(
194+
"Module '{}' not found. Use 'enterprise module list' to see available modules.",
195+
module_name
196+
),
197+
});
198+
} else {
199+
let suggestions: Vec<_> = partial_matches
200+
.iter()
201+
.filter_map(|m| m.module_name.as_deref())
202+
.collect();
203+
return Err(RedisCtlError::InvalidInput {
204+
message: format!(
205+
"Module '{}' not found. Did you mean one of: {}?",
206+
module_name,
207+
suggestions.join(", ")
208+
),
209+
});
210+
}
211+
}
212+
1 => {
213+
// Build module config using the actual module name from the API
214+
let actual_name = matching[0].module_name.as_deref().unwrap_or(module_name);
215+
let mut module_config = serde_json::json!({
216+
"module_name": actual_name
217+
});
218+
if let Some(args) = module_args {
219+
module_config["module_args"] = serde_json::json!(args);
220+
}
221+
module_list.push(module_config);
222+
}
223+
_ => {
224+
// Multiple matches - show versions and ask user to be specific
225+
let versions: Vec<_> = matching
226+
.iter()
227+
.map(|m| {
228+
format!(
229+
"{} (version: {})",
230+
m.module_name.as_deref().unwrap_or("unknown"),
231+
m.semantic_version.as_deref().unwrap_or("unknown")
232+
)
233+
})
234+
.collect();
235+
return Err(RedisCtlError::InvalidInput {
236+
message: format!(
237+
"Multiple modules found matching '{}'. Available versions:\n {}",
238+
module_name,
239+
versions.join("\n ")
240+
),
241+
});
242+
}
243+
}
244+
}
245+
246+
// Add module_list to request (CLI modules override --data modules)
247+
request_obj.insert("module_list".to_string(), serde_json::json!(module_list));
248+
}
249+
148250
let path = if dry_run {
149251
"/v1/bdbs/dry-run"
150252
} else {

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
use clap::Subcommand;
1+
use clap::{ArgGroup, Subcommand};
22

33
#[derive(Debug, Subcommand)]
44
pub enum ModuleCommands {
55
/// List all available modules
66
#[command(visible_alias = "ls")]
77
List,
88

9-
/// Get module details
9+
/// Get module details by UID or name
10+
#[command(group(ArgGroup::new("identifier").required(true).args(["uid", "name"])))]
1011
Get {
1112
/// Module UID
12-
uid: String,
13+
#[arg(conflicts_with = "name")]
14+
uid: Option<String>,
15+
16+
/// Module name (e.g., "ReJSON", "search")
17+
#[arg(long, conflicts_with = "uid")]
18+
name: Option<String>,
1319
},
1420

1521
/// Upload new module

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

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,16 @@ pub async fn handle_module_commands(
2121
) -> CliResult<()> {
2222
match cmd {
2323
ModuleCommands::List => handle_list(conn_mgr, profile_name, output_format, query).await,
24-
ModuleCommands::Get { uid } => {
25-
handle_get(conn_mgr, profile_name, uid, output_format, query).await
24+
ModuleCommands::Get { uid, name } => {
25+
handle_get(
26+
conn_mgr,
27+
profile_name,
28+
uid.as_deref(),
29+
name.as_deref(),
30+
output_format,
31+
query,
32+
)
33+
.await
2634
}
2735
ModuleCommands::Upload { file } => {
2836
handle_upload(conn_mgr, profile_name, file, output_format, query).await
@@ -61,14 +69,90 @@ async fn handle_list(
6169
async fn handle_get(
6270
conn_mgr: &ConnectionManager,
6371
profile_name: Option<&str>,
64-
uid: &str,
72+
uid: Option<&str>,
73+
name: Option<&str>,
6574
output_format: OutputFormat,
6675
query: Option<&str>,
6776
) -> CliResult<()> {
6877
let client = conn_mgr.create_enterprise_client(profile_name).await?;
6978
let handler = ModuleHandler::new(client);
7079

71-
let module = handler.get(uid).await.map_err(RedisCtlError::from)?;
80+
// Resolve module UID from name if provided
81+
let resolved_uid = if let Some(module_name) = name {
82+
let modules = handler.list().await.map_err(RedisCtlError::from)?;
83+
let matching: Vec<_> = modules
84+
.iter()
85+
.filter(|m| {
86+
m.module_name
87+
.as_ref()
88+
.map(|n| n.eq_ignore_ascii_case(module_name))
89+
.unwrap_or(false)
90+
})
91+
.collect();
92+
93+
match matching.len() {
94+
0 => {
95+
// No exact match - try partial match and suggest
96+
let partial_matches: Vec<_> = modules
97+
.iter()
98+
.filter(|m| {
99+
m.module_name
100+
.as_ref()
101+
.map(|n| n.to_lowercase().contains(&module_name.to_lowercase()))
102+
.unwrap_or(false)
103+
})
104+
.collect();
105+
106+
if partial_matches.is_empty() {
107+
return Err(anyhow::anyhow!(
108+
"No module found with name '{}'. Use 'module list' to see available modules.",
109+
module_name
110+
)
111+
.into());
112+
} else {
113+
let suggestions: Vec<_> = partial_matches
114+
.iter()
115+
.filter_map(|m| m.module_name.as_deref())
116+
.collect();
117+
return Err(anyhow::anyhow!(
118+
"No module found with name '{}'. Did you mean one of: {}?",
119+
module_name,
120+
suggestions.join(", ")
121+
)
122+
.into());
123+
}
124+
}
125+
1 => matching[0].uid.clone(),
126+
_ => {
127+
// Multiple matches - show versions and ask user to be specific
128+
let versions: Vec<_> = matching
129+
.iter()
130+
.map(|m| {
131+
format!(
132+
"{} (uid: {}, version: {})",
133+
m.module_name.as_deref().unwrap_or("unknown"),
134+
m.uid,
135+
m.semantic_version.as_deref().unwrap_or("unknown")
136+
)
137+
})
138+
.collect();
139+
return Err(anyhow::anyhow!(
140+
"Multiple modules found with name '{}'. Please use --uid to specify:\n {}",
141+
module_name,
142+
versions.join("\n ")
143+
)
144+
.into());
145+
}
146+
}
147+
} else {
148+
uid.expect("Either uid or name must be provided")
149+
.to_string()
150+
};
151+
152+
let module = handler
153+
.get(&resolved_uid)
154+
.await
155+
.map_err(RedisCtlError::from)?;
72156

73157
let module_json = serde_json::to_value(&module)?;
74158
let output_data = if let Some(q) = query {

0 commit comments

Comments
 (0)