Skip to content

Commit 3258075

Browse files
Merge pull request #90 from redis-field-engineering/feat/phase-2-medium-priority
feat: implement Phase 2 medium-priority CLI commands
2 parents ca8486b + 4cb6883 commit 3258075

File tree

3 files changed

+319
-23
lines changed

3 files changed

+319
-23
lines changed

crates/redisctl/src/cli.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,14 @@ pub enum DatabaseCommands {
316316
/// Import URL
317317
url: String,
318318
},
319+
/// Export data
320+
Export {
321+
/// Database ID
322+
id: String,
323+
/// Export format (rdb, json)
324+
#[arg(long, default_value = "rdb")]
325+
format: String,
326+
},
319327
}
320328

321329
#[derive(Subcommand, Clone)]
@@ -388,6 +396,14 @@ pub enum AccountCommands {
388396
/// Account/Subscription ID
389397
id: String,
390398
},
399+
/// Show account information (different from show)
400+
Info,
401+
/// Show account owner information
402+
Owner,
403+
/// List users for this account
404+
Users,
405+
/// List payment methods
406+
PaymentMethods,
391407
}
392408

393409
#[derive(Subcommand)]
@@ -426,6 +442,29 @@ pub enum SubscriptionCommands {
426442
#[arg(long)]
427443
force: bool,
428444
},
445+
/// Get subscription pricing
446+
Pricing {
447+
/// Subscription ID
448+
id: String,
449+
},
450+
/// List subscription databases
451+
Databases {
452+
/// Subscription ID
453+
id: String,
454+
},
455+
/// Get CIDR whitelist
456+
CidrList {
457+
/// Subscription ID
458+
id: String,
459+
},
460+
/// Update CIDR whitelist
461+
CidrUpdate {
462+
/// Subscription ID
463+
id: String,
464+
/// CIDR blocks (comma-separated)
465+
#[arg(long)]
466+
cidrs: String,
467+
},
429468
}
430469

431470
#[derive(Subcommand)]
@@ -445,6 +484,29 @@ pub enum NodeCommands {
445484
#[arg(long)]
446485
external_addr: Option<String>,
447486
},
487+
/// Add a new node to the cluster
488+
Add {
489+
/// Node IP address
490+
#[arg(long)]
491+
addr: String,
492+
/// Node username (default: admin)
493+
#[arg(long, default_value = "admin")]
494+
username: String,
495+
/// Node password
496+
#[arg(long)]
497+
password: String,
498+
/// External address
499+
#[arg(long)]
500+
external_addr: Option<String>,
501+
},
502+
/// Remove a node from the cluster
503+
Remove {
504+
/// Node ID
505+
id: String,
506+
/// Skip confirmation
507+
#[arg(long)]
508+
force: bool,
509+
},
448510
}
449511

450512
#[derive(Subcommand)]
@@ -462,6 +524,14 @@ pub enum TaskCommands {
462524
/// Task ID
463525
id: String,
464526
},
527+
/// Wait for task completion
528+
Wait {
529+
/// Task ID
530+
id: String,
531+
/// Timeout in seconds (default: 300)
532+
#[arg(long, default_value = "300")]
533+
timeout: u64,
534+
},
465535
}
466536

467537
#[derive(Subcommand)]

crates/redisctl/src/commands/cloud.rs

Lines changed: 206 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,37 @@ pub async fn handle_database_command(
169169
DatabaseCommands::Backup { id: _ } => {
170170
anyhow::bail!("Backup operations not yet implemented for Cloud databases");
171171
}
172-
DatabaseCommands::Import { id: _, url: _ } => {
173-
anyhow::bail!("Import operations not yet implemented for Cloud databases");
172+
DatabaseCommands::Import { id, url } => {
173+
let (subscription_id, database_id) = parse_database_id(&id)?;
174+
let import_data = serde_json::json!({
175+
"sourceUri": url
176+
});
177+
let task = client
178+
.post_raw(
179+
&format!(
180+
"/subscriptions/{}/databases/{}/import",
181+
subscription_id, database_id
182+
),
183+
import_data,
184+
)
185+
.await?;
186+
print_output(task, output_format, query)?;
187+
}
188+
DatabaseCommands::Export { id, format } => {
189+
let (subscription_id, database_id) = parse_database_id(&id)?;
190+
let export_data = serde_json::json!({
191+
"format": format
192+
});
193+
let task = client
194+
.post_raw(
195+
&format!(
196+
"/subscriptions/{}/databases/{}/export",
197+
subscription_id, database_id
198+
),
199+
export_data,
200+
)
201+
.await?;
202+
print_output(task, output_format, query)?;
174203
}
175204
}
176205

@@ -195,17 +224,72 @@ pub async fn handle_subscription_command(
195224
print_output(subscription, output_format, query)?;
196225
}
197226
SubscriptionCommands::Create {
198-
name: _,
199-
provider: _,
200-
region: _,
227+
name,
228+
provider,
229+
region,
201230
} => {
202-
anyhow::bail!("Subscription creation not yet implemented");
231+
let create_data = serde_json::json!({
232+
"name": name,
233+
"cloudProvider": provider,
234+
"region": region
235+
});
236+
let subscription = client.post_raw("/subscriptions", create_data).await?;
237+
print_output(subscription, output_format, query)?;
203238
}
204-
SubscriptionCommands::Update { id: _, name: _ } => {
205-
anyhow::bail!("Subscription update not yet implemented");
239+
SubscriptionCommands::Update { id, name } => {
240+
let mut update_data = serde_json::Map::new();
241+
if let Some(name) = name {
242+
update_data.insert("name".to_string(), serde_json::Value::String(name));
243+
}
244+
if update_data.is_empty() {
245+
anyhow::bail!("No update fields provided");
246+
}
247+
let subscription = client
248+
.put_raw(
249+
&format!("/subscriptions/{}", id),
250+
serde_json::Value::Object(update_data),
251+
)
252+
.await?;
253+
print_output(subscription, output_format, query)?;
206254
}
207-
SubscriptionCommands::Delete { id: _, force: _ } => {
208-
anyhow::bail!("Subscription deletion not yet implemented");
255+
SubscriptionCommands::Delete { id, force } => {
256+
if !force {
257+
println!(
258+
"Are you sure you want to delete subscription {}? Use --force to skip confirmation.",
259+
id
260+
);
261+
return Ok(());
262+
}
263+
client.delete_raw(&format!("/subscriptions/{}", id)).await?;
264+
println!("Subscription {} deleted successfully", id);
265+
}
266+
SubscriptionCommands::Pricing { id } => {
267+
let pricing = client
268+
.get_raw(&format!("/subscriptions/{}/pricing", id))
269+
.await?;
270+
print_output(pricing, output_format, query)?;
271+
}
272+
SubscriptionCommands::Databases { id } => {
273+
let databases = client
274+
.get_raw(&format!("/subscriptions/{}/databases", id))
275+
.await?;
276+
print_output(databases, output_format, query)?;
277+
}
278+
SubscriptionCommands::CidrList { id } => {
279+
let cidr = client
280+
.get_raw(&format!("/subscriptions/{}/cidr", id))
281+
.await?;
282+
print_output(cidr, output_format, query)?;
283+
}
284+
SubscriptionCommands::CidrUpdate { id, cidrs } => {
285+
let cidr_list: Vec<&str> = cidrs.split(',').map(|s| s.trim()).collect();
286+
let update_data = serde_json::json!({
287+
"cidr": cidr_list
288+
});
289+
let cidr = client
290+
.put_raw(&format!("/subscriptions/{}/cidr", id), update_data)
291+
.await?;
292+
print_output(cidr, output_format, query)?;
209293
}
210294
}
211295

@@ -229,6 +313,22 @@ pub async fn handle_account_command(
229313
let account = client.get_raw(&format!("/accounts/{}", id)).await?;
230314
print_output(account, output_format, query)?;
231315
}
316+
AccountCommands::Info => {
317+
let account_info = client.get_raw("/accounts/info").await?;
318+
print_output(account_info, output_format, query)?;
319+
}
320+
AccountCommands::Owner => {
321+
let owner = client.get_raw("/accounts/owner").await?;
322+
print_output(owner, output_format, query)?;
323+
}
324+
AccountCommands::Users => {
325+
let users = client.get_raw("/accounts/users").await?;
326+
print_output(users, output_format, query)?;
327+
}
328+
AccountCommands::PaymentMethods => {
329+
let payment_methods = client.get_raw("/accounts/payment-methods").await?;
330+
print_output(payment_methods, output_format, query)?;
331+
}
232332
}
233333

234334
Ok(())
@@ -252,22 +352,63 @@ pub async fn handle_user_command(
252352
print_output(user, output_format, query)?;
253353
}
254354
UserCommands::Create {
255-
name: _,
256-
email: _,
257-
password: _,
258-
roles: _,
355+
name,
356+
email,
357+
password,
358+
roles,
259359
} => {
260-
anyhow::bail!("User creation not yet implemented");
360+
let mut create_data = serde_json::json!({
361+
"name": name
362+
});
363+
364+
if let Some(email) = email {
365+
create_data["email"] = serde_json::Value::String(email);
366+
}
367+
if let Some(password) = password {
368+
create_data["password"] = serde_json::Value::String(password);
369+
}
370+
if !roles.is_empty() {
371+
create_data["roles"] = serde_json::Value::Array(
372+
roles.into_iter().map(serde_json::Value::String).collect(),
373+
);
374+
}
375+
376+
let user = client.post_raw("/users", create_data).await?;
377+
print_output(user, output_format, query)?;
261378
}
262379
UserCommands::Update {
263-
id: _,
264-
email: _,
265-
password: _,
380+
id,
381+
email,
382+
password,
266383
} => {
267-
anyhow::bail!("User update not yet implemented");
384+
let mut update_data = serde_json::Map::new();
385+
if let Some(email) = email {
386+
update_data.insert("email".to_string(), serde_json::Value::String(email));
387+
}
388+
if let Some(password) = password {
389+
update_data.insert("password".to_string(), serde_json::Value::String(password));
390+
}
391+
if update_data.is_empty() {
392+
anyhow::bail!("No update fields provided");
393+
}
394+
let user = client
395+
.put_raw(
396+
&format!("/users/{}", id),
397+
serde_json::Value::Object(update_data),
398+
)
399+
.await?;
400+
print_output(user, output_format, query)?;
268401
}
269-
UserCommands::Delete { id: _, force: _ } => {
270-
anyhow::bail!("User deletion not yet implemented");
402+
UserCommands::Delete { id, force } => {
403+
if !force {
404+
println!(
405+
"Are you sure you want to delete user {}? Use --force to skip confirmation.",
406+
id
407+
);
408+
return Ok(());
409+
}
410+
client.delete_raw(&format!("/users/{}", id)).await?;
411+
println!("User {} deleted successfully", id);
271412
}
272413
}
273414

@@ -309,6 +450,50 @@ pub async fn handle_task_command(
309450
let task = client.get_raw(&format!("/tasks/{}", id)).await?;
310451
print_output(task, output_format, query)?;
311452
}
453+
TaskCommands::Wait { id, timeout } => {
454+
use std::time::{Duration, Instant};
455+
use tokio::time::sleep;
456+
457+
let start = Instant::now();
458+
let timeout_duration = Duration::from_secs(timeout);
459+
460+
loop {
461+
let task = client.get_raw(&format!("/tasks/{}", id)).await?;
462+
463+
// Check if task has a status field and if it's completed
464+
if let Some(status) = task.get("status").and_then(|s| s.as_str()) {
465+
match status {
466+
"completed" => {
467+
println!("Task {} completed successfully", id);
468+
print_output(task, output_format, query)?;
469+
break;
470+
}
471+
"failed" => {
472+
println!("Task {} failed", id);
473+
print_output(task, output_format, query)?;
474+
anyhow::bail!("Task failed");
475+
}
476+
_ => {
477+
// Task still running, check timeout
478+
if start.elapsed() > timeout_duration {
479+
println!(
480+
"Timeout waiting for task {} after {} seconds",
481+
id, timeout
482+
);
483+
print_output(task, output_format, query)?;
484+
anyhow::bail!("Task wait timeout");
485+
}
486+
// Wait 5 seconds before checking again
487+
sleep(Duration::from_secs(5)).await;
488+
}
489+
}
490+
} else {
491+
// No status field, print task and break
492+
print_output(task, output_format, query)?;
493+
break;
494+
}
495+
}
496+
}
312497
}
313498

314499
Ok(())

0 commit comments

Comments
 (0)