diff --git a/crates/redis-cloud/src/fixed/databases.rs b/crates/redis-cloud/src/fixed/databases.rs index 96d96f7d..209debbe 100644 --- a/crates/redis-cloud/src/fixed/databases.rs +++ b/crates/redis-cloud/src/fixed/databases.rs @@ -1197,4 +1197,25 @@ impl FixedDatabaseHandler { ) -> Result { self.get_tags(subscription_id, database_id).await } + + // ======================================================================== + // New endpoints + // ======================================================================== + + /// Get available target Redis versions for upgrade + /// Gets a list of Redis versions that the Essentials database can be upgraded to. + /// + /// GET /fixed/subscriptions/{subscriptionId}/databases/{databaseId}/available-target-versions + pub async fn get_available_target_versions( + &self, + subscription_id: i32, + database_id: i32, + ) -> Result { + self.client + .get_raw(&format!( + "/fixed/subscriptions/{}/databases/{}/available-target-versions", + subscription_id, database_id + )) + .await + } } diff --git a/crates/redis-cloud/src/flexible/databases.rs b/crates/redis-cloud/src/flexible/databases.rs index 11ea84f1..2b360ce5 100644 --- a/crates/redis-cloud/src/flexible/databases.rs +++ b/crates/redis-cloud/src/flexible/databases.rs @@ -1548,4 +1548,43 @@ impl DatabaseHandler { ) .await } + + /// Get available target Redis versions for upgrade + /// Gets a list of Redis versions that the database can be upgraded to. + /// + /// GET /subscriptions/{subscriptionId}/databases/{databaseId}/available-target-versions + pub async fn get_available_target_versions( + &self, + subscription_id: i32, + database_id: i32, + ) -> Result { + self.client + .get_raw(&format!( + "/subscriptions/{}/databases/{}/available-target-versions", + subscription_id, database_id + )) + .await + } + + /// Flush Pro database (standard, non-Active-Active) + /// Deletes all data from the specified Pro database. + /// + /// PUT /subscriptions/{subscriptionId}/databases/{databaseId}/flush + pub async fn flush_database( + &self, + subscription_id: i32, + database_id: i32, + ) -> Result { + // Empty body for standard flush + self.client + .put_raw( + &format!( + "/subscriptions/{}/databases/{}/flush", + subscription_id, database_id + ), + serde_json::json!({}), + ) + .await + .and_then(|v| serde_json::from_value(v).map_err(Into::into)) + } } diff --git a/crates/redis-cloud/src/tasks.rs b/crates/redis-cloud/src/tasks.rs index 4592ec0e..e65d1faf 100644 --- a/crates/redis-cloud/src/tasks.rs +++ b/crates/redis-cloud/src/tasks.rs @@ -160,10 +160,18 @@ impl TasksHandler { /// Gets a list of all currently running tasks for this account. /// /// GET /tasks - pub async fn get_all_tasks(&self) -> Result<()> { + pub async fn get_all_tasks(&self) -> Result> { self.client.get("/tasks").await } + /// Get tasks (raw JSON) + /// Gets a list of all currently running tasks for this account. + /// + /// GET /tasks + pub async fn get_all_tasks_raw(&self) -> Result { + self.client.get_raw("/tasks").await + } + /// Get a single task /// Gets details and status of a single task by the Task ID. /// diff --git a/crates/redisctl/src/cli/cloud.rs b/crates/redisctl/src/cli/cloud.rs index 12bfcd8b..06284c2d 100644 --- a/crates/redisctl/src/cli/cloud.rs +++ b/crates/redisctl/src/cli/cloud.rs @@ -485,6 +485,9 @@ pub enum PrivateLinkCommands { /// Cloud Task Commands #[derive(Subcommand, Debug)] pub enum CloudTaskCommands { + /// List all tasks for this account + #[command(alias = "ls")] + List, /// Get task status and details Get { /// Task ID (UUID format) @@ -1335,6 +1338,15 @@ pub enum CloudDatabaseCommands { key: String, }, + /// Flush database (deletes all data) + Flush { + /// Database ID (format: subscription_id:database_id) + id: String, + /// Skip confirmation prompt + #[arg(long)] + force: bool, + }, + /// Flush Active-Active database FlushCrdb { /// Database ID (format: subscription_id:database_id) @@ -1344,6 +1356,12 @@ pub enum CloudDatabaseCommands { force: bool, }, + /// Get available Redis versions for upgrade + AvailableVersions { + /// Database ID (format: subscription_id:database_id) + id: String, + }, + /// Get Redis version upgrade status UpgradeStatus { /// Database ID (format: subscription_id:database_id) diff --git a/crates/redisctl/src/commands/cloud/database.rs b/crates/redisctl/src/commands/cloud/database.rs index d6c3585f..82bc221b 100644 --- a/crates/redisctl/src/commands/cloud/database.rs +++ b/crates/redisctl/src/commands/cloud/database.rs @@ -209,6 +209,17 @@ pub async fn handle_database_command( super::database_impl::delete_tag(conn_mgr, profile_name, id, key, output_format, query) .await } + CloudDatabaseCommands::Flush { id, force } => { + super::database_impl::flush_database( + conn_mgr, + profile_name, + id, + *force, + output_format, + query, + ) + .await + } CloudDatabaseCommands::FlushCrdb { id, force } => { super::database_impl::flush_crdb( conn_mgr, @@ -220,6 +231,16 @@ pub async fn handle_database_command( ) .await } + CloudDatabaseCommands::AvailableVersions { id } => { + super::database_impl::get_available_versions( + conn_mgr, + profile_name, + id, + output_format, + query, + ) + .await + } CloudDatabaseCommands::UpgradeStatus { id } => { super::database_impl::get_upgrade_status( conn_mgr, diff --git a/crates/redisctl/src/commands/cloud/database_impl.rs b/crates/redisctl/src/commands/cloud/database_impl.rs index 1e73cc99..59a24534 100644 --- a/crates/redisctl/src/commands/cloud/database_impl.rs +++ b/crates/redisctl/src/commands/cloud/database_impl.rs @@ -713,6 +713,119 @@ pub async fn delete_tag( Ok(()) } +/// Flush standard (non-Active-Active) database +pub async fn flush_database( + conn_mgr: &ConnectionManager, + profile_name: Option<&str>, + id: &str, + force: bool, + output_format: OutputFormat, + query: Option<&str>, +) -> CliResult<()> { + let (subscription_id, database_id) = parse_database_id(id)?; + + // Confirmation prompt unless --force is used + if !force { + use dialoguer::Confirm; + let confirm = Confirm::new() + .with_prompt(format!( + "Are you sure you want to flush database {}? This will delete all data!", + id + )) + .default(false) + .interact() + .map_err(|e| RedisCtlError::InvalidInput { + message: format!("Failed to read confirmation: {}", e), + })?; + + if !confirm { + println!("Flush operation cancelled"); + return Ok(()); + } + } + + let client = conn_mgr.create_cloud_client(profile_name).await?; + + let response = client + .put_raw( + &format!( + "/subscriptions/{}/databases/{}/flush", + subscription_id, database_id + ), + json!({}), + ) + .await + .context("Failed to flush database")?; + + let result = if let Some(q) = query { + apply_jmespath(&response, q)? + } else { + response + }; + + match output_format { + OutputFormat::Table => { + println!("Database flush initiated"); + if let Some(task_id) = result.get("taskId") { + println!("Task ID: {}", task_id); + } + } + _ => print_json_or_yaml(result, output_format)?, + } + + Ok(()) +} + +/// Get available Redis versions for upgrade +pub async fn get_available_versions( + conn_mgr: &ConnectionManager, + profile_name: Option<&str>, + id: &str, + output_format: OutputFormat, + query: Option<&str>, +) -> CliResult<()> { + let (subscription_id, database_id) = parse_database_id(id)?; + let client = conn_mgr.create_cloud_client(profile_name).await?; + + let response = client + .get_raw(&format!( + "/subscriptions/{}/databases/{}/available-target-versions", + subscription_id, database_id + )) + .await + .context("Failed to get available versions")?; + + let result = if let Some(q) = query { + apply_jmespath(&response, q)? + } else { + response + }; + + match output_format { + OutputFormat::Table => { + if let Some(versions) = result.as_array() { + if versions.is_empty() { + println!("No upgrade versions available"); + } else { + println!("Available Redis versions for upgrade:"); + for v in versions { + if let Some(version) = v.as_str() { + println!(" - {}", version); + } else { + println!(" - {}", v); + } + } + } + } else { + print_json_or_yaml(result, output_format)?; + } + } + _ => print_json_or_yaml(result, output_format)?, + } + + Ok(()) +} + /// Flush Active-Active database pub async fn flush_crdb( conn_mgr: &ConnectionManager, diff --git a/crates/redisctl/src/commands/cloud/task.rs b/crates/redisctl/src/commands/cloud/task.rs index 4c13dc90..4af491d2 100644 --- a/crates/redisctl/src/commands/cloud/task.rs +++ b/crates/redisctl/src/commands/cloud/task.rs @@ -23,6 +23,7 @@ pub async fn handle_task_command( query: Option<&str>, ) -> CliResult<()> { match command { + CloudTaskCommands::List => list_tasks(conn_mgr, profile_name, output_format, query).await, CloudTaskCommands::Get { id } => { get_task(conn_mgr, profile_name, id, output_format, query).await } @@ -59,6 +60,128 @@ pub async fn handle_task_command( } } +/// List all tasks for this account +async fn list_tasks( + conn_mgr: &ConnectionManager, + profile_name: Option<&str>, + output_format: OutputFormat, + query: Option<&str>, +) -> CliResult<()> { + let client = conn_mgr.create_cloud_client(profile_name).await?; + let tasks = client + .get_raw("/tasks") + .await + .with_context(|| "Failed to fetch tasks") + .map_err(|e| RedisCtlError::ApiError { + message: e.to_string(), + })?; + + // Apply JMESPath query if provided + let data = if let Some(q) = query { + super::utils::apply_jmespath(&tasks, q)? + } else { + tasks + }; + + match output_format { + OutputFormat::Auto | OutputFormat::Table => { + print_tasks_table(&data)?; + } + OutputFormat::Json => { + print_output(data, crate::output::OutputFormat::Json, None).map_err(|e| { + RedisCtlError::OutputError { + message: e.to_string(), + } + })?; + } + OutputFormat::Yaml => { + print_output(data, crate::output::OutputFormat::Yaml, None).map_err(|e| { + RedisCtlError::OutputError { + message: e.to_string(), + } + })?; + } + } + + Ok(()) +} + +/// Print tasks in table format +fn print_tasks_table(tasks: &Value) -> CliResult<()> { + use tabled::{Table, Tabled, settings::Style}; + + #[derive(Tabled)] + struct TaskRow { + #[tabled(rename = "Task ID")] + task_id: String, + #[tabled(rename = "Status")] + status: String, + #[tabled(rename = "Command")] + command: String, + #[tabled(rename = "Progress")] + progress: String, + #[tabled(rename = "Description")] + description: String, + } + + let tasks_array = match tasks.as_array() { + Some(arr) => arr, + None => { + println!("No tasks found"); + return Ok(()); + } + }; + + if tasks_array.is_empty() { + println!("No tasks found"); + return Ok(()); + } + + let rows: Vec = tasks_array + .iter() + .map(|task| { + let status = task + .get("status") + .or_else(|| task.get("state")) + .and_then(|s| s.as_str()) + .unwrap_or("unknown"); + + TaskRow { + task_id: task + .get("taskId") + .or_else(|| task.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or("-") + .to_string(), + status: format_task_state(status), + command: task + .get("commandType") + .and_then(|v| v.as_str()) + .unwrap_or("-") + .to_string(), + progress: task + .get("progress") + .and_then(|p| p.as_u64()) + .map(|p| format!("{}%", p)) + .unwrap_or_else(|| "-".to_string()), + description: task + .get("description") + .and_then(|v| v.as_str()) + .unwrap_or("-") + .chars() + .take(40) + .collect::(), + } + }) + .collect(); + + let mut table = Table::new(&rows); + table.with(Style::rounded()); + println!("{}", table); + + Ok(()) +} + /// Get task status and details async fn get_task( conn_mgr: &ConnectionManager,