diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..5ded35ad --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Docker Compose environment variables for Redis Enterprise + +# Redis Enterprise Docker image +# Default: redislabs/redis:latest +REDIS_ENTERPRISE_IMAGE=redislabs/redis:latest + +# Docker platform +# Default: linux/amd64 +# For ARM64 (Apple Silicon): linux/arm64 +REDIS_ENTERPRISE_PLATFORM=linux/amd64 + +# Note: For ARM64 systems (Apple Silicon Macs), you'll need to: +# 1. Use an ARM64-compatible Redis Enterprise image +# 2. Set REDIS_ENTERPRISE_PLATFORM=linux/arm64 diff --git a/crates/redis-enterprise/src/client.rs b/crates/redis-enterprise/src/client.rs index 4923396a..31a2c1fd 100644 --- a/crates/redis-enterprise/src/client.rs +++ b/crates/redis-enterprise/src/client.rs @@ -478,4 +478,26 @@ impl EnterpriseClient { } } } + + /// Execute a Redis command on a specific database (internal use only) + /// This uses the /v1/bdbs/{uid}/command endpoint which may not be publicly documented + pub async fn execute_command(&self, db_uid: u32, command: &str) -> Result { + let url = format!("{}/v1/bdbs/{}/command", self.base_url, db_uid); + let body = serde_json::json!({ + "command": command + }); + + debug!("Executing command on database {}: {}", db_uid, command); + + let response = self + .client + .post(&url) + .basic_auth(&self.username, Some(&self.password)) + .json(&body) + .send() + .await + .map_err(|e| self.map_reqwest_error(e, &url))?; + + self.handle_response(response).await + } } diff --git a/crates/redisctl/src/cli.rs b/crates/redisctl/src/cli.rs index 7293a171..e8bcb035 100644 --- a/crates/redisctl/src/cli.rs +++ b/crates/redisctl/src/cli.rs @@ -1000,11 +1000,54 @@ pub enum EnterpriseCommands { #[command(subcommand)] Module(crate::commands::enterprise::module::ModuleCommands), + /// Workflow operations for multi-step tasks + #[command(subcommand)] + Workflow(EnterpriseWorkflowCommands), + /// Statistics and metrics operations #[command(subcommand)] Stats(EnterpriseStatsCommands), } +/// Enterprise workflow commands +#[derive(Debug, Subcommand)] +pub enum EnterpriseWorkflowCommands { + /// List available workflows + List, + + /// Initialize a Redis Enterprise cluster + #[command(name = "init-cluster")] + InitCluster { + /// Cluster name + #[arg(long, default_value = "redis-cluster")] + name: String, + + /// Admin username + #[arg(long, default_value = "admin@redis.local")] + username: String, + + /// Admin password (required) + #[arg(long, env = "REDIS_ENTERPRISE_INIT_PASSWORD")] + password: String, + + /// Skip creating a default database after initialization + #[arg(long)] + skip_database: bool, + + /// Name for the default database + #[arg(long, default_value = "default-db")] + database_name: String, + + /// Memory size for the default database in GB + #[arg(long, default_value = "1")] + database_memory_gb: i64, + + /// Async operation options + #[command(flatten)] + async_ops: crate::commands::cloud::async_utils::AsyncOperationArgs, + }, +} + // Placeholder command structures - will be expanded in later PRs #[derive(Subcommand, Debug)] diff --git a/crates/redisctl/src/connection.rs b/crates/redisctl/src/connection.rs index a5e6a1bf..54a8f20c 100644 --- a/crates/redisctl/src/connection.rs +++ b/crates/redisctl/src/connection.rs @@ -7,6 +7,7 @@ use tracing::{debug, info, trace}; /// Connection manager for creating authenticated clients #[allow(dead_code)] // Used by binary target +#[derive(Clone)] pub struct ConnectionManager { pub config: Config, } diff --git a/crates/redisctl/src/main.rs b/crates/redisctl/src/main.rs index c12ae254..e0648a4d 100644 --- a/crates/redisctl/src/main.rs +++ b/crates/redisctl/src/main.rs @@ -10,6 +10,7 @@ mod config; mod connection; mod error; mod output; +mod workflows; use cli::{Cli, Commands}; use config::Config; @@ -255,6 +256,9 @@ async fn execute_enterprise_command( ) .await } + Workflow(workflow_cmd) => { + handle_enterprise_workflow_command(conn_mgr, profile, workflow_cmd, output).await + } Stats(stats_cmd) => { commands::enterprise::stats::handle_stats_command( conn_mgr, profile, stats_cmd, output, query, @@ -264,6 +268,127 @@ async fn execute_enterprise_command( } } +async fn handle_enterprise_workflow_command( + conn_mgr: &ConnectionManager, + profile: Option<&str>, + workflow_cmd: &cli::EnterpriseWorkflowCommands, + output: cli::OutputFormat, +) -> Result<(), RedisCtlError> { + use cli::EnterpriseWorkflowCommands::*; + use workflows::{WorkflowArgs, WorkflowContext, WorkflowRegistry}; + + match workflow_cmd { + List => { + let registry = WorkflowRegistry::new(); + let workflows = registry.list(); + + match output { + cli::OutputFormat::Json | cli::OutputFormat::Yaml => { + let workflow_list: Vec = workflows + .into_iter() + .map(|(name, description)| { + serde_json::json!({ + "name": name, + "description": description + }) + }) + .collect(); + let output_format = match output { + cli::OutputFormat::Json => output::OutputFormat::Json, + cli::OutputFormat::Yaml => output::OutputFormat::Yaml, + _ => output::OutputFormat::Table, + }; + crate::output::print_output( + serde_json::json!(workflow_list), + output_format, + None, + )?; + } + _ => { + println!("Available Enterprise Workflows:"); + println!(); + for (name, description) in workflows { + println!(" {} - {}", name, description); + } + } + } + Ok(()) + } + InitCluster { + name, + username, + password, + skip_database, + database_name, + database_memory_gb, + async_ops, + } => { + let mut args = WorkflowArgs::new(); + args.insert("name", name); + args.insert("username", username); + args.insert("password", password); + args.insert("create_database", !skip_database); + args.insert("database_name", database_name); + args.insert("database_memory_gb", database_memory_gb); + + let output_format = match output { + cli::OutputFormat::Json => output::OutputFormat::Json, + cli::OutputFormat::Yaml => output::OutputFormat::Yaml, + cli::OutputFormat::Table | cli::OutputFormat::Auto => output::OutputFormat::Table, + }; + + let context = WorkflowContext { + conn_mgr: conn_mgr.clone(), + profile_name: profile.map(String::from), + output_format, + wait_timeout: if async_ops.wait { + async_ops.wait_timeout + } else { + 0 + }, + }; + + let registry = WorkflowRegistry::new(); + let workflow = registry + .get("init-cluster") + .ok_or_else(|| RedisCtlError::ApiError { + message: "Workflow not found".to_string(), + })?; + + let result = + workflow + .execute(context, args) + .await + .map_err(|e| RedisCtlError::ApiError { + message: e.to_string(), + })?; + + if !result.success { + return Err(RedisCtlError::ApiError { + message: result.message, + }); + } + + // Print result as JSON/YAML if requested + match output { + cli::OutputFormat::Json | cli::OutputFormat::Yaml => { + let result_json = serde_json::json!({ + "success": result.success, + "message": result.message, + "outputs": result.outputs, + }); + crate::output::print_output(&result_json, output_format, None)?; + } + _ => { + // Human output was already printed by the workflow + } + } + + Ok(()) + } + } +} + async fn execute_profile_command( profile_cmd: &cli::ProfileCommands, conn_mgr: &ConnectionManager, diff --git a/crates/redisctl/src/workflows/enterprise/init_cluster.rs b/crates/redisctl/src/workflows/enterprise/init_cluster.rs new file mode 100644 index 00000000..c490a877 --- /dev/null +++ b/crates/redisctl/src/workflows/enterprise/init_cluster.rs @@ -0,0 +1,312 @@ +//! Initialize Redis Enterprise cluster workflow +//! +//! This workflow automates the process of setting up a new Redis Enterprise cluster, +//! including bootstrap, waiting for initialization, creating admin user, and +//! optionally creating a default database. + +use crate::workflows::{Workflow, WorkflowArgs, WorkflowContext, WorkflowResult}; +use anyhow::{Context, Result}; +use indicatif::{ProgressBar, ProgressStyle}; +use redis_enterprise::EnterpriseClient; +use serde_json::json; +use std::future::Future; +use std::pin::Pin; +use std::time::Duration; +use tokio::time::sleep; + +pub struct InitClusterWorkflow; + +impl InitClusterWorkflow { + pub fn new() -> Self { + Self + } +} + +impl Workflow for InitClusterWorkflow { + fn name(&self) -> &str { + "init-cluster" + } + + fn description(&self) -> &str { + "Initialize a Redis Enterprise cluster with bootstrap and optional database creation" + } + + fn execute( + &self, + context: WorkflowContext, + args: WorkflowArgs, + ) -> Pin> + Send>> { + Box::pin(async move { + use crate::output::OutputFormat; + + // Only print human-readable output for Table format + let is_human_output = matches!(context.output_format, OutputFormat::Table); + + if is_human_output { + println!("Initializing Redis Enterprise cluster..."); + } + + // Get parameters + let cluster_name = args + .get_string("name") + .unwrap_or_else(|| "redis-cluster".to_string()); + let username = args + .get_string("username") + .unwrap_or_else(|| "admin@redis.local".to_string()); + let password = args + .get_string("password") + .context("Password is required for cluster initialization")?; + let create_db = args.get_bool("create_database").unwrap_or(true); + let db_name = args + .get_string("database_name") + .unwrap_or_else(|| "default-db".to_string()); + let db_memory_gb = args.get_i64("database_memory_gb").unwrap_or(1); + + // Create client + let client = context + .conn_mgr + .create_enterprise_client(context.profile_name.as_deref()) + .await + .context("Failed to create Enterprise client")?; + + // Step 1: Check if cluster is already initialized + let needs_bootstrap = check_if_needs_bootstrap(&client).await?; + + if !needs_bootstrap { + if is_human_output { + println!("Cluster is already initialized"); + } + return Ok(WorkflowResult::success("Cluster already initialized") + .with_output("cluster_name", &cluster_name) + .with_output("already_initialized", true)); + } + + // Step 2: Bootstrap the cluster + let bootstrap_data = json!({ + "action": "create_cluster", + "cluster": { + "name": cluster_name + }, + "credentials": { + "username": username, + "password": password + }, + "flash_enabled": false + }); + + let bootstrap_result = client + .post_bootstrap("/v1/bootstrap/create_cluster", &bootstrap_data) + .await + .context("Failed to bootstrap cluster")?; + + // Check if bootstrap returned an action ID (async operation) + if let Some(action_id) = bootstrap_result.get("action_uid").and_then(|v| v.as_str()) { + // Wait for bootstrap to complete + wait_for_action(&client, action_id, "cluster bootstrap").await?; + } else { + // Bootstrap was synchronous, just wait a bit for cluster to stabilize + sleep(Duration::from_secs(5)).await; + } + + if is_human_output { + println!("Bootstrap completed successfully"); + } + + // Step 3: Cluster should be ready after bootstrap + // Wait longer for cluster to fully stabilize + sleep(Duration::from_secs(10)).await; + if is_human_output { + println!("Cluster is ready"); + } + + // After bootstrap, we need to create a new client with the credentials we just set + // Get the base URL from environment or use default + let base_url = std::env::var("REDIS_ENTERPRISE_URL") + .unwrap_or_else(|_| "https://localhost:9443".to_string()); + let insecure = std::env::var("REDIS_ENTERPRISE_INSECURE") + .unwrap_or_else(|_| "true".to_string()) + .parse::() + .unwrap_or(true); + + let authenticated_client = redis_enterprise::EnterpriseClient::builder() + .base_url(base_url) + .username(username.clone()) + .password(password.clone()) + .insecure(insecure) + .build() + .context("Failed to create authenticated client after bootstrap")?; + + // Step 4: Optionally create a default database + if create_db { + if is_human_output { + println!("Creating default database '{}'...", db_name); + } + + let db_data = json!({ + "name": db_name, + "memory_size": db_memory_gb * 1024 * 1024 * 1024, // Convert GB to bytes + "type": "redis", + "replication": false + }); + + match authenticated_client.post_raw("/v1/bdbs", db_data).await { + Ok(db_result) => { + // Check for async operation + if let Some(action_id) = + db_result.get("action_uid").and_then(|v| v.as_str()) + { + wait_for_action(&authenticated_client, action_id, "database creation") + .await?; + } + + let db_uid = db_result + .get("uid") + .or_else(|| db_result.get("resource_id")) + .and_then(|v| v.as_i64()) + .unwrap_or(0) as u32; + + if is_human_output { + println!("Database created successfully (ID: {})", db_uid); + } + + // Verify database connectivity with PING command + if db_uid > 0 { + // Wait a moment for database to be fully ready + sleep(Duration::from_secs(2)).await; + + match authenticated_client.execute_command(db_uid, "PING").await { + Ok(response) => { + if let Some(result) = response.get("response") { + // The command endpoint returns {"response": true} for successful PING + if (result.as_bool() == Some(true) + || result.as_str() == Some("PONG")) + && is_human_output + { + println!( + "Database connectivity verified (PING successful)" + ); + } + } + } + Err(e) => { + if is_human_output { + eprintln!( + "Note: Could not verify database connectivity: {}", + e + ); + } + } + } + } + } + Err(e) => { + // Database creation failed, but cluster is initialized + if is_human_output { + eprintln!("Warning: Failed to create database: {}", e); + eprintln!("Cluster is initialized but database creation failed."); + eprintln!("You can create a database manually later."); + } + } + } + } else if is_human_output { + println!("Skipping database creation (--skip-database flag set)"); + } + + // Final summary (only for human output) + if is_human_output { + println!(); + println!("Cluster initialization completed successfully"); + println!(); + println!("Cluster name: {}", cluster_name); + println!("Admin user: {}", username); + if create_db { + println!("Database: {} ({}GB)", db_name, db_memory_gb); + } + println!(); + println!("Access endpoints:"); + println!(" Web UI: https://localhost:8443"); + println!(" API: https://localhost:9443"); + } + + Ok(WorkflowResult::success("Cluster initialized successfully") + .with_output("cluster_name", &cluster_name) + .with_output("username", &username) + .with_output("database_created", create_db) + .with_output("database_name", &db_name)) + }) + } +} + +/// Check if the cluster needs bootstrap +async fn check_if_needs_bootstrap(client: &EnterpriseClient) -> Result { + match client.get_raw("/v1/bootstrap").await { + Ok(status) => { + // Check if cluster is already bootstrapped + if let Some(state) = status.get("state").and_then(|v| v.as_str()) { + Ok(state == "unconfigured" || state == "new") + } else { + // If we can't determine state, assume it needs bootstrap + Ok(true) + } + } + Err(_) => { + // If we can't get status, cluster might not be initialized + Ok(true) + } + } +} + +/// Wait for an async action to complete +async fn wait_for_action( + client: &EnterpriseClient, + action_id: &str, + operation_name: &str, +) -> Result<()> { + let pb = ProgressBar::new_spinner(); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .unwrap(), + ); + pb.set_message(format!("Waiting for {} to complete...", operation_name)); + + let max_attempts = 120; // 10 minutes with 5 second intervals + for attempt in 1..=max_attempts { + pb.set_message(format!( + "Waiting for {} to complete... (attempt {}/{})", + operation_name, attempt, max_attempts + )); + + match client.get_raw(&format!("/v1/actions/{}", action_id)).await { + Ok(action) => { + if let Some(status) = action.get("status").and_then(|v| v.as_str()) { + match status { + "completed" | "done" => { + pb.finish_and_clear(); + return Ok(()); + } + "failed" | "error" => { + pb.finish_and_clear(); + let error_msg = action + .get("error") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error"); + anyhow::bail!("{} failed: {}", operation_name, error_msg); + } + _ => { + // Still in progress + } + } + } + } + Err(_) => { + // Action might not be available yet + } + } + + sleep(Duration::from_secs(5)).await; + } + + pb.finish_and_clear(); + anyhow::bail!("{} did not complete within 10 minutes", operation_name) +} diff --git a/crates/redisctl/src/workflows/enterprise/mod.rs b/crates/redisctl/src/workflows/enterprise/mod.rs new file mode 100644 index 00000000..08cd950e --- /dev/null +++ b/crates/redisctl/src/workflows/enterprise/mod.rs @@ -0,0 +1,5 @@ +//! Enterprise-specific workflows + +mod init_cluster; + +pub use init_cluster::InitClusterWorkflow; diff --git a/crates/redisctl/src/workflows/mod.rs b/crates/redisctl/src/workflows/mod.rs new file mode 100644 index 00000000..008a0291 --- /dev/null +++ b/crates/redisctl/src/workflows/mod.rs @@ -0,0 +1,147 @@ +//! Workflow system for multi-step operations +//! +//! Workflows orchestrate complex operations that require multiple API calls, +//! waiting for async operations, and conditional logic. + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::future::Future; +use std::pin::Pin; + +pub mod enterprise; + +/// Common trait for all workflows +pub trait Workflow: Send + Sync { + /// Unique identifier for the workflow + fn name(&self) -> &str; + + /// Human-readable description + fn description(&self) -> &str; + + /// Execute the workflow with the given arguments + fn execute( + &self, + context: WorkflowContext, + args: WorkflowArgs, + ) -> Pin> + Send>>; +} + +/// Context provided to workflows for accessing API clients and configuration +#[derive(Clone)] +pub struct WorkflowContext { + pub conn_mgr: crate::connection::ConnectionManager, + pub profile_name: Option, + pub output_format: crate::output::OutputFormat, + #[allow(dead_code)] // Will be used by future workflows + pub wait_timeout: u64, +} + +/// Arguments passed to a workflow +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct WorkflowArgs { + params: HashMap, +} + +impl WorkflowArgs { + pub fn new() -> Self { + Self { + params: HashMap::new(), + } + } + + pub fn insert(&mut self, key: impl Into, value: impl Serialize) { + self.params + .insert(key.into(), serde_json::to_value(value).unwrap()); + } + + pub fn get Deserialize<'de>>(&self, key: &str) -> Option { + self.params + .get(key) + .and_then(|v| serde_json::from_value(v.clone()).ok()) + } + + pub fn get_string(&self, key: &str) -> Option { + self.get(key) + } + + pub fn get_bool(&self, key: &str) -> Option { + self.get(key) + } + + pub fn get_i64(&self, key: &str) -> Option { + self.get(key) + } +} + +/// Result of a workflow execution +#[derive(Debug, Serialize, Deserialize)] +pub struct WorkflowResult { + pub success: bool, + pub message: String, + pub outputs: HashMap, +} + +impl WorkflowResult { + pub fn success(message: impl Into) -> Self { + Self { + success: true, + message: message.into(), + outputs: HashMap::new(), + } + } + + #[allow(dead_code)] // Will be used by future workflows + pub fn failure(message: impl Into) -> Self { + Self { + success: false, + message: message.into(), + outputs: HashMap::new(), + } + } + + pub fn with_output(mut self, key: impl Into, value: impl Serialize) -> Self { + self.outputs + .insert(key.into(), serde_json::to_value(value).unwrap()); + self + } +} + +/// Registry of available workflows +pub struct WorkflowRegistry { + workflows: HashMap>, +} + +impl WorkflowRegistry { + pub fn new() -> Self { + let mut registry = Self { + workflows: HashMap::new(), + }; + + // Register all built-in workflows + registry.register(Box::new(enterprise::InitClusterWorkflow::new())); + + registry + } + + pub fn register(&mut self, workflow: Box) { + self.workflows.insert(workflow.name().to_string(), workflow); + } + + pub fn get(&self, name: &str) -> Option<&dyn Workflow> { + self.workflows.get(name).map(|w| w.as_ref()) + } + + pub fn list(&self) -> Vec<(&str, &str)> { + self.workflows + .values() + .map(|w| (w.name(), w.description())) + .collect() + } +} + +impl Default for WorkflowRegistry { + fn default() -> Self { + Self::new() + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..fd202deb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.8" + +services: + redis-enterprise: + image: ${REDIS_ENTERPRISE_IMAGE:-redislabs/redis:latest} + platform: ${REDIS_ENTERPRISE_PLATFORM:-linux/amd64} + container_name: redis-enterprise + tty: true + cap_add: + - ALL + ports: + - "12000:12000" + - "12001:12001" + - "9443:9443" + - "8443:8443" + networks: + - radar-network + healthcheck: + test: ["CMD", "curl", "-f", "-k", "https://localhost:9443/v1/bootstrap"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + + # Once we have a published Docker image, we can use this service to auto-initialize + # redis-enterprise-init: + # image: ghcr.io/joshrotenberg/redisctl:latest + # container_name: redis-enterprise-init + # depends_on: + # redis-enterprise: + # condition: service_healthy + # networks: + # - radar-network + # environment: + # REDIS_ENTERPRISE_URL: "https://redis-enterprise:9443" + # REDIS_ENTERPRISE_INSECURE: "true" + # command: ["enterprise", "workflow", "init-cluster", "--name", "docker-cluster", "--username", "admin@redis.local", "--password", "Redis123!"] + +networks: + radar-network: + driver: bridge diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index f31c530d..0b1c64c1 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -34,6 +34,7 @@ - [Modules](./enterprise/modules.md) - [Logs](./enterprise/logs.md) - [Active-Active (CRDB)](./enterprise/crdb.md) +- [Workflows](./enterprise/workflows.md) - [Raw API Access](./enterprise/api-access.md) # Core Features @@ -42,6 +43,7 @@ - [Output Formats](./features/output-formats.md) - [Profile Management](./features/profiles.md) - [Smart Commands](./features/smart-commands.md) +- [Workflows](./features/workflows.md) # Tutorials diff --git a/docs/src/enterprise/overview.md b/docs/src/enterprise/overview.md index 2bbb7092..0b4cd8bf 100644 --- a/docs/src/enterprise/overview.md +++ b/docs/src/enterprise/overview.md @@ -32,6 +32,7 @@ Resources include: - `user` - User management - `role` - Role-based access control - `alert` - Alert configuration +- `workflow` - Multi-step automated operations ## Common Operations @@ -47,10 +48,16 @@ redisctl enterprise database get 1 # List nodes redisctl enterprise node list + +# Initialize a new cluster (workflow) +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "SecurePassword" ``` ## Next Steps - [Human-Friendly Commands](./human-commands.md) - High-level command reference +- [Workflows](./workflows.md) - Automated multi-step operations - [Raw API Access](./api-access.md) - Direct API endpoint access - [Examples](./examples.md) - Real-world usage examples \ No newline at end of file diff --git a/docs/src/enterprise/workflows.md b/docs/src/enterprise/workflows.md new file mode 100644 index 00000000..f3bc2ee9 --- /dev/null +++ b/docs/src/enterprise/workflows.md @@ -0,0 +1,147 @@ +# Enterprise Workflows + +Workflows are multi-step operations that automate complex Redis Enterprise management tasks. They combine multiple API calls, handle asynchronous operations, and provide progress feedback. + +## Available Workflows + +### List Workflows + +```bash +# List all available workflows +redisctl enterprise workflow list + +# JSON output for scripting +redisctl enterprise workflow list --output json +``` + +### Initialize Cluster + +The `init-cluster` workflow automates the complete setup of a new Redis Enterprise cluster, including bootstrapping and optional database creation. + +```bash +# Initialize with default settings +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "YourSecurePassword" + +# Initialize with custom cluster name and database +redisctl enterprise workflow init-cluster \ + --name "production-cluster" \ + --username "admin@redis.local" \ + --password "YourSecurePassword" \ + --database-name "my-database" \ + --database-memory-gb 2 + +# Skip database creation +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "YourSecurePassword" \ + --skip-database +``` + +#### Parameters + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `--name` | Cluster name | `redis-cluster` | +| `--username` | Admin username | `admin@redis.local` | +| `--password` | Admin password (required) | - | +| `--skip-database` | Skip creating default database | `false` | +| `--database-name` | Name for default database | `default-db` | +| `--database-memory-gb` | Memory size in GB for database | `1` | +| `--wait` | Wait for operations to complete | `true` | +| `--wait-timeout` | Maximum wait time in seconds | `600` | + +#### What it does + +1. **Checks cluster status** - Verifies if cluster needs initialization +2. **Bootstraps cluster** - Creates cluster with specified name and credentials +3. **Waits for stabilization** - Ensures cluster is ready for operations +4. **Creates database** (optional) - Sets up initial database with specified configuration +5. **Verifies connectivity** - Tests database with PING command + +## Output Formats + +Workflows support structured output for automation: + +```bash +# JSON output +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "Redis123" \ + --output json + +# YAML output +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "Redis123" \ + --output yaml +``` + +Example JSON output: +```json +{ + "success": true, + "message": "Cluster initialized successfully", + "outputs": { + "cluster_name": "redis-cluster", + "username": "admin@cluster.local", + "database_created": true, + "database_name": "default-db" + } +} +``` + +## Docker Development + +For testing workflows with Docker: + +```bash +# Start Redis Enterprise container +docker compose up -d + +# Wait for container to be ready +sleep 10 + +# Initialize cluster +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "Redis123" + +# Clean up +docker compose down -v +``` + +## Environment Variables + +Workflows respect standard environment variables: + +```bash +export REDIS_ENTERPRISE_URL="https://localhost:9443" +export REDIS_ENTERPRISE_INSECURE="true" + +# Password can be set via environment +export REDIS_ENTERPRISE_INIT_PASSWORD="Redis123" + +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" +``` + +## Error Handling + +Workflows provide clear error messages and maintain partial progress: + +- If cluster is already initialized, workflow reports success without re-bootstrapping +- If database creation fails, cluster remains initialized and can be managed manually +- Network failures include retry logic with configurable timeouts + +## Future Workflows + +Additional workflows are planned for common operations: + +- **upgrade-cluster** - Orchestrate cluster version upgrades +- **backup-restore** - Automated backup and restore operations +- **migrate-database** - Database migration between clusters +- **security-hardening** - Apply security best practices + +See the [Workflows Feature Guide](../features/workflows.md) for architectural details and information about creating custom workflows. \ No newline at end of file diff --git a/docs/src/features/workflows.md b/docs/src/features/workflows.md new file mode 100644 index 00000000..44ef092a --- /dev/null +++ b/docs/src/features/workflows.md @@ -0,0 +1,251 @@ +# Workflows + +Workflows are a powerful feature of redisctl that automate complex, multi-step operations. Instead of running multiple commands manually and managing the state between them, workflows handle the entire process with proper error handling, progress tracking, and rollback capabilities. + +## Overview + +Workflows solve common challenges when managing Redis deployments: + +- **Complex operations** requiring multiple API calls in sequence +- **Asynchronous operations** that need polling and status checking +- **Error recovery** with proper cleanup and state management +- **Progress visibility** for long-running operations +- **Reproducibility** through consistent execution patterns + +## How Workflows Work + +Each workflow is a self-contained operation that: + +1. **Validates prerequisites** - Checks current state before making changes +2. **Executes steps sequentially** - Performs operations in the correct order +3. **Handles async operations** - Waits for tasks to complete with progress feedback +4. **Manages errors gracefully** - Provides clear error messages and recovery options +5. **Returns structured results** - Outputs can be consumed programmatically + +## Available Workflows + +### Redis Enterprise + +- **init-cluster** - Complete cluster initialization with bootstrap and database setup + +### Redis Cloud (Future) + +- **provision-subscription** - Create subscription with databases and networking +- **setup-aa-database** - Configure Active-Active database across regions + +## Using Workflows + +### Interactive Mode + +Run workflows with human-readable output: + +```bash +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "SecurePass123" +``` + +Output: +``` +Initializing Redis Enterprise cluster... +Bootstrap completed successfully +Cluster is ready +Creating default database 'default-db'... +Database created successfully (ID: 1) +Database connectivity verified (PING successful) + +Cluster initialization completed successfully + +Cluster name: redis-cluster +Admin user: admin@cluster.local +Database: default-db (1GB) + +Access endpoints: + Web UI: https://localhost:8443 + API: https://localhost:9443 +``` + +### Programmatic Mode + +Use structured output for automation: + +```bash +# Get JSON output +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "SecurePass123" \ + --output json \ + --skip-database +``` + +```json +{ + "success": true, + "message": "Cluster initialized successfully", + "outputs": { + "cluster_name": "redis-cluster", + "username": "admin@cluster.local", + "database_created": false, + "database_name": "default-db" + } +} +``` + +### CI/CD Integration + +Workflows are ideal for CI/CD pipelines: + +```yaml +# GitHub Actions example +- name: Initialize Redis Enterprise + run: | + redisctl enterprise workflow init-cluster \ + --username "${{ secrets.REDIS_USER }}" \ + --password "${{ secrets.REDIS_PASSWORD }}" \ + --output json \ + --wait-timeout 300 +``` + +## Async Operation Handling + +Workflows handle asynchronous operations transparently: + +```bash +# Workflows support standard async flags +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "SecurePass123" \ + --wait \ + --wait-timeout 600 +``` + +The workflow will: +- Submit operations asynchronously +- Poll for completion status +- Show progress indicators +- Handle timeouts gracefully + +## Error Handling + +Workflows provide robust error handling: + +### Partial Success +If a workflow partially completes (e.g., cluster initialized but database creation fails): +- The successful steps are preserved +- Clear error messages explain what failed +- Recovery instructions are provided + +### Idempotency +Workflows check current state before making changes: +- Running init-cluster on an initialized cluster returns success without re-bootstrapping +- Operations are safe to retry + +### Validation +Prerequisites are checked before execution: +- Required permissions are verified +- Resource availability is confirmed +- Configuration validity is checked + +## Workflow Architecture + +### Trait-Based Design + +Workflows implement a common trait for consistency: + +```rust +pub trait Workflow: Send + Sync { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn execute(&self, context: WorkflowContext, args: WorkflowArgs) + -> Pin> + Send>>; +} +``` + +### Registry Pattern + +Workflows are registered at startup: + +```rust +let registry = WorkflowRegistry::new(); +registry.register(InitClusterWorkflow::new()); +registry.register(UpgradeClusterWorkflow::new()); +``` + +### Context and Arguments + +Each workflow receives: +- **Context**: Connection manager, profile, output format, timeouts +- **Arguments**: User-provided parameters as key-value pairs + +### Results + +Workflows return structured results: +- **Success/failure status** +- **Human-readable message** +- **Structured outputs** for programmatic consumption + +## Best Practices + +### When to Use Workflows + +Use workflows for: +- **Initial setup** - Bootstrapping new environments +- **Complex migrations** - Multi-step data or configuration changes +- **Disaster recovery** - Automated failover and recovery procedures +- **Routine maintenance** - Standardized update and backup procedures + +### When to Use Direct Commands + +Use direct commands for: +- **Simple queries** - Getting status or configuration +- **Single operations** - Creating one resource +- **Debugging** - Investigating specific issues +- **Custom scripts** - Operations not covered by workflows + +## Creating Custom Workflows + +While redisctl provides built-in workflows, you can create custom workflows by: + +1. **Scripting existing commands** - Combine redisctl commands in bash/python +2. **Using the libraries** - Build Rust applications with redis-cloud/redis-enterprise crates +3. **Contributing workflows** - Submit PRs for commonly needed workflows + +Example custom workflow script: + +```bash +#!/bin/bash +# Custom workflow: setup-monitoring.sh + +# Create monitoring database +DB_ID=$(redisctl enterprise database create \ + --name "monitoring" \ + --memory-gb 1 \ + --output json | jq -r '.uid') + +# Configure alerts +redisctl enterprise database update $DB_ID \ + --alert-settings '{"memory_threshold": 80}' + +# Setup metrics export +redisctl enterprise stats config \ + --database $DB_ID \ + --export-interval 60 + +echo "Monitoring setup complete for database $DB_ID" +``` + +## Future Enhancements + +Planned workflow improvements: + +- **Workflow templates** - Parameterized workflows for common patterns +- **Conditional logic** - Branching based on state or user input +- **Rollback support** - Automatic undo for failed operations +- **Workflow composition** - Building complex workflows from simpler ones +- **Progress streaming** - Real-time updates for long operations + +## See Also + +- [Enterprise Workflows](../enterprise/workflows.md) - Enterprise-specific workflow documentation +- [Async Operations](./async-operations.md) - Understanding async operation handling +- [Output Formats](./output-formats.md) - Working with structured output \ No newline at end of file diff --git a/docs/src/getting-started/docker.md b/docs/src/getting-started/docker.md index eaea2324..2671c313 100644 --- a/docs/src/getting-started/docker.md +++ b/docs/src/getting-started/docker.md @@ -14,17 +14,21 @@ Our Docker environment includes: ## Quick Start ```bash -# Start Redis Enterprise and initialize cluster -make docker-up +# For Intel/AMD systems (default) +docker compose up -d -# Access interactive CLI -make docker-cli +# For Apple Silicon (M1/M2/M3) Macs +cp .env.example .env +# Edit .env to uncomment ARM settings +docker compose up -d -# Run tests against the cluster -make docker-test +# Access the cluster +export REDIS_ENTERPRISE_URL="https://localhost:9443" +export REDIS_ENTERPRISE_INSECURE="true" +redisctl enterprise cluster info # Clean up -make docker-down +docker compose down -v ``` ## Service Profiles @@ -168,6 +172,21 @@ make docker-cleanup ## Environment Variables +Configure the Docker environment via `.env` file: + +```bash +# Copy example environment file +cp .env.example .env + +# Edit .env to set: +# - REDIS_ENTERPRISE_IMAGE: Docker image to use +# - REDIS_ENTERPRISE_PLATFORM: Platform architecture + +# For Apple Silicon Macs: +# Set REDIS_ENTERPRISE_PLATFORM=linux/arm64 +# Use an ARM64-compatible Redis Enterprise image +``` + Control logging and behavior: ```bash @@ -175,10 +194,7 @@ Control logging and behavior: RUST_LOG=debug docker compose up # Component-specific logging -RUST_LOG="redis_enterprise=trace,redis_enterprise_cli=debug" docker compose up - -# Use different Redis Enterprise image -ENTERPRISE_IMAGE=redislabs/redis:latest docker compose up +RUST_LOG="redis_enterprise=trace,redisctl=debug" docker compose up ``` ## Development Workflow @@ -277,8 +293,15 @@ docker compose down **ARM Mac Issues:** ```bash -# Ensure using ARM-compatible image -ENTERPRISE_IMAGE=kurtfm/rs-arm:latest docker compose up +# Copy and configure environment file +cp .env.example .env + +# Edit .env for ARM64: +# REDIS_ENTERPRISE_PLATFORM=linux/arm64 +# Set REDIS_ENTERPRISE_IMAGE to an ARM64-compatible image + +# Start with ARM configuration +docker compose up -d ``` **Permission Issues:** diff --git a/docs/src/getting-started/quickstart.md b/docs/src/getting-started/quickstart.md index 040b8313..9e64caa3 100644 --- a/docs/src/getting-started/quickstart.md +++ b/docs/src/getting-started/quickstart.md @@ -77,7 +77,26 @@ redisctl api enterprise get /v1/bdbs redisctl api enterprise get /v1/nodes ``` -## Step 4: Explore More +## Step 4: Using Workflows + +### Initialize Enterprise Cluster + +For new Redis Enterprise installations, use the init-cluster workflow: + +```bash +# Complete cluster setup with one command +redisctl enterprise workflow init-cluster \ + --username "admin@cluster.local" \ + --password "YourSecurePassword" + +# This workflow will: +# 1. Bootstrap the cluster +# 2. Set up authentication +# 3. Create a default database +# 4. Verify connectivity +``` + +## Step 5: Explore More ### Cloud Operations @@ -116,4 +135,5 @@ redisctl database list -q "[].{name:name,memory:memory_size}" - [Redis Cloud Guide](../cloud/overview.md) - Cloud-specific operations - [Redis Enterprise Guide](../enterprise/overview.md) - Enterprise-specific operations +- [Workflows](../features/workflows.md) - Automating complex operations - [Examples](../cloud/examples.md) - More detailed examples \ No newline at end of file