Skip to content

Commit 7137c87

Browse files
feat: implement workflow system with enterprise init-cluster workflow (#261)
* feat: implement workflow system with init-cluster workflow - Add workflow trait and registry system for multi-step operations - Implement enterprise init-cluster workflow for cluster bootstrapping - Add CLI commands for listing and running workflows - Support async operations with progress indicators - Include optional database creation after cluster init This provides a foundation for automating complex multi-step operations that require multiple API calls, waiting, and conditional logic. Part of #260 * fix: address clippy warnings in workflow implementation - Add #[allow(dead_code)] for fields/methods that will be used in future workflows - Collapse nested if statement in wait_for_cluster_ready function - All clippy checks now pass with --all-targets --all-features * fix: update init-cluster workflow with correct bootstrap endpoint and simplified flow - Use correct /v1/bootstrap/create_cluster endpoint with proper payload structure - Fix JSON payload format with cluster, credentials, and flash_enabled fields - Simplify cluster ready check to avoid authentication issues - Change create_database flag to skip_database for better UX - Add #[allow(dead_code)] for unused wait_for_cluster_ready function Successfully tested against Redis Enterprise Docker container - workflow completes and creates cluster * feat(docker): add docker-compose setup with init service placeholder - Add Redis Enterprise container with proper configuration - Include redis-enterprise-init service (commented for future Docker image) - Create docker-init.sh script for host-based initialization - Document architecture mismatch issue and workaround * fix(workflows): remove unused function and fix CLI argument documentation - Remove unused wait_for_cluster_ready function - Fix command documentation (--name not --cluster-name) - Workflow now works correctly with proper argument names * feat(workflows): add structured output support and remove emojis - Remove all emojis from workflow output for professional appearance - Add JSON/YAML output support for programmatic use - Human-readable output only shown for Table format - Add AsyncOperationArgs to workflow commands for consistent --wait pattern - Support JSON/YAML output for workflow list command - Pass wait_timeout through workflow context for future use - Test with fresh Docker containers shows all formats working correctly * feat(enterprise): add Redis command execution via REST API - Add execute_command method to Enterprise client using /v1/bdbs/{uid}/command endpoint - Integrate PING command in workflow to verify database connectivity - Handle both boolean and string responses from command endpoint - Keep feature undocumented as it's an internal capability * fix(workflows): create authenticated client after bootstrap for database operations - Create new authenticated client with bootstrap credentials for database operations - Remove unsupported 'persistence' field from database creation payload - Increase stabilization wait time from 5 to 10 seconds after bootstrap - Successfully tested full workflow: bootstrap → database creation → PING verification * chore: remove docker-init.sh script Script is no longer needed as the workflow can be run directly from the host * docs: add comprehensive workflow documentation - Add enterprise/workflows.md with detailed init-cluster documentation - Add features/workflows.md explaining workflow architecture and patterns - Update SUMMARY.md with workflow sections - Add workflow example to quickstart guide - Update enterprise overview to mention workflows - Document all workflow parameters, output formats, and error handling - Include Docker development examples and CI/CD integration * feat(docker): use standard Redis Enterprise image with environment configuration - Replace hardcoded kurtfm/rs-arm:latest with configurable image via environment variables - Add REDIS_ENTERPRISE_IMAGE and REDIS_ENTERPRISE_PLATFORM environment variables - Default to standard redislabs/redis:latest image for Intel/AMD systems - Provide .env.example with configuration for ARM64 (Apple Silicon) systems - Update documentation to explain environment-based configuration - Maintain backwards compatibility with ARM Macs through .env configuration * chore: remove references to non-public Docker image - Remove kurtfm image references from .env.example - Update documentation to reference ARM64-compatible images generically - Users can set their own ARM64 image via environment variables * fix: resolve CI failures - clippy and dead code warnings - Add #[allow(dead_code)] for wait_timeout field (will be used by future workflows) - Fix collapsible else-if warning - Fix collapsible if statement warning - Remove needless borrow in print_output call
1 parent 903a570 commit 7137c87

File tree

15 files changed

+1174
-14
lines changed

15 files changed

+1174
-14
lines changed

.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Docker Compose environment variables for Redis Enterprise
2+
3+
# Redis Enterprise Docker image
4+
# Default: redislabs/redis:latest
5+
REDIS_ENTERPRISE_IMAGE=redislabs/redis:latest
6+
7+
# Docker platform
8+
# Default: linux/amd64
9+
# For ARM64 (Apple Silicon): linux/arm64
10+
REDIS_ENTERPRISE_PLATFORM=linux/amd64
11+
12+
# Note: For ARM64 systems (Apple Silicon Macs), you'll need to:
13+
# 1. Use an ARM64-compatible Redis Enterprise image
14+
# 2. Set REDIS_ENTERPRISE_PLATFORM=linux/arm64

crates/redis-enterprise/src/client.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,26 @@ impl EnterpriseClient {
478478
}
479479
}
480480
}
481+
482+
/// Execute a Redis command on a specific database (internal use only)
483+
/// This uses the /v1/bdbs/{uid}/command endpoint which may not be publicly documented
484+
pub async fn execute_command(&self, db_uid: u32, command: &str) -> Result<serde_json::Value> {
485+
let url = format!("{}/v1/bdbs/{}/command", self.base_url, db_uid);
486+
let body = serde_json::json!({
487+
"command": command
488+
});
489+
490+
debug!("Executing command on database {}: {}", db_uid, command);
491+
492+
let response = self
493+
.client
494+
.post(&url)
495+
.basic_auth(&self.username, Some(&self.password))
496+
.json(&body)
497+
.send()
498+
.await
499+
.map_err(|e| self.map_reqwest_error(e, &url))?;
500+
501+
self.handle_response(response).await
502+
}
481503
}

crates/redisctl/src/cli.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,11 +1000,54 @@ pub enum EnterpriseCommands {
10001000
#[command(subcommand)]
10011001
Module(crate::commands::enterprise::module::ModuleCommands),
10021002

1003+
/// Workflow operations for multi-step tasks
1004+
#[command(subcommand)]
1005+
Workflow(EnterpriseWorkflowCommands),
1006+
10031007
/// Statistics and metrics operations
10041008
#[command(subcommand)]
10051009
Stats(EnterpriseStatsCommands),
10061010
}
10071011

1012+
/// Enterprise workflow commands
1013+
#[derive(Debug, Subcommand)]
1014+
pub enum EnterpriseWorkflowCommands {
1015+
/// List available workflows
1016+
List,
1017+
1018+
/// Initialize a Redis Enterprise cluster
1019+
#[command(name = "init-cluster")]
1020+
InitCluster {
1021+
/// Cluster name
1022+
#[arg(long, default_value = "redis-cluster")]
1023+
name: String,
1024+
1025+
/// Admin username
1026+
#[arg(long, default_value = "admin@redis.local")]
1027+
username: String,
1028+
1029+
/// Admin password (required)
1030+
#[arg(long, env = "REDIS_ENTERPRISE_INIT_PASSWORD")]
1031+
password: String,
1032+
1033+
/// Skip creating a default database after initialization
1034+
#[arg(long)]
1035+
skip_database: bool,
1036+
1037+
/// Name for the default database
1038+
#[arg(long, default_value = "default-db")]
1039+
database_name: String,
1040+
1041+
/// Memory size for the default database in GB
1042+
#[arg(long, default_value = "1")]
1043+
database_memory_gb: i64,
1044+
1045+
/// Async operation options
1046+
#[command(flatten)]
1047+
async_ops: crate::commands::cloud::async_utils::AsyncOperationArgs,
1048+
},
1049+
}
1050+
10081051
// Placeholder command structures - will be expanded in later PRs
10091052

10101053
#[derive(Subcommand, Debug)]

crates/redisctl/src/connection.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use tracing::{debug, info, trace};
77

88
/// Connection manager for creating authenticated clients
99
#[allow(dead_code)] // Used by binary target
10+
#[derive(Clone)]
1011
pub struct ConnectionManager {
1112
pub config: Config,
1213
}

crates/redisctl/src/main.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod config;
1010
mod connection;
1111
mod error;
1212
mod output;
13+
mod workflows;
1314

1415
use cli::{Cli, Commands};
1516
use config::Config;
@@ -255,6 +256,9 @@ async fn execute_enterprise_command(
255256
)
256257
.await
257258
}
259+
Workflow(workflow_cmd) => {
260+
handle_enterprise_workflow_command(conn_mgr, profile, workflow_cmd, output).await
261+
}
258262
Stats(stats_cmd) => {
259263
commands::enterprise::stats::handle_stats_command(
260264
conn_mgr, profile, stats_cmd, output, query,
@@ -264,6 +268,127 @@ async fn execute_enterprise_command(
264268
}
265269
}
266270

271+
async fn handle_enterprise_workflow_command(
272+
conn_mgr: &ConnectionManager,
273+
profile: Option<&str>,
274+
workflow_cmd: &cli::EnterpriseWorkflowCommands,
275+
output: cli::OutputFormat,
276+
) -> Result<(), RedisCtlError> {
277+
use cli::EnterpriseWorkflowCommands::*;
278+
use workflows::{WorkflowArgs, WorkflowContext, WorkflowRegistry};
279+
280+
match workflow_cmd {
281+
List => {
282+
let registry = WorkflowRegistry::new();
283+
let workflows = registry.list();
284+
285+
match output {
286+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
287+
let workflow_list: Vec<serde_json::Value> = workflows
288+
.into_iter()
289+
.map(|(name, description)| {
290+
serde_json::json!({
291+
"name": name,
292+
"description": description
293+
})
294+
})
295+
.collect();
296+
let output_format = match output {
297+
cli::OutputFormat::Json => output::OutputFormat::Json,
298+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
299+
_ => output::OutputFormat::Table,
300+
};
301+
crate::output::print_output(
302+
serde_json::json!(workflow_list),
303+
output_format,
304+
None,
305+
)?;
306+
}
307+
_ => {
308+
println!("Available Enterprise Workflows:");
309+
println!();
310+
for (name, description) in workflows {
311+
println!(" {} - {}", name, description);
312+
}
313+
}
314+
}
315+
Ok(())
316+
}
317+
InitCluster {
318+
name,
319+
username,
320+
password,
321+
skip_database,
322+
database_name,
323+
database_memory_gb,
324+
async_ops,
325+
} => {
326+
let mut args = WorkflowArgs::new();
327+
args.insert("name", name);
328+
args.insert("username", username);
329+
args.insert("password", password);
330+
args.insert("create_database", !skip_database);
331+
args.insert("database_name", database_name);
332+
args.insert("database_memory_gb", database_memory_gb);
333+
334+
let output_format = match output {
335+
cli::OutputFormat::Json => output::OutputFormat::Json,
336+
cli::OutputFormat::Yaml => output::OutputFormat::Yaml,
337+
cli::OutputFormat::Table | cli::OutputFormat::Auto => output::OutputFormat::Table,
338+
};
339+
340+
let context = WorkflowContext {
341+
conn_mgr: conn_mgr.clone(),
342+
profile_name: profile.map(String::from),
343+
output_format,
344+
wait_timeout: if async_ops.wait {
345+
async_ops.wait_timeout
346+
} else {
347+
0
348+
},
349+
};
350+
351+
let registry = WorkflowRegistry::new();
352+
let workflow = registry
353+
.get("init-cluster")
354+
.ok_or_else(|| RedisCtlError::ApiError {
355+
message: "Workflow not found".to_string(),
356+
})?;
357+
358+
let result =
359+
workflow
360+
.execute(context, args)
361+
.await
362+
.map_err(|e| RedisCtlError::ApiError {
363+
message: e.to_string(),
364+
})?;
365+
366+
if !result.success {
367+
return Err(RedisCtlError::ApiError {
368+
message: result.message,
369+
});
370+
}
371+
372+
// Print result as JSON/YAML if requested
373+
match output {
374+
cli::OutputFormat::Json | cli::OutputFormat::Yaml => {
375+
let result_json = serde_json::json!({
376+
"success": result.success,
377+
"message": result.message,
378+
"outputs": result.outputs,
379+
});
380+
crate::output::print_output(&result_json, output_format, None)?;
381+
}
382+
_ => {
383+
// Human output was already printed by the workflow
384+
}
385+
}
386+
387+
Ok(())
388+
}
389+
}
390+
}
391+
267392
async fn execute_profile_command(
268393
profile_cmd: &cli::ProfileCommands,
269394
conn_mgr: &ConnectionManager,

0 commit comments

Comments
 (0)