Skip to main content

redis_enterprise/
bootstrap.rs

1//! Cluster bootstrap and node joining operations
2//!
3//! ## Overview
4//! - Bootstrap new clusters
5//! - Join nodes to existing clusters
6//! - Configure initial settings
7
8use crate::client::RestClient;
9use crate::error::Result;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use typed_builder::TypedBuilder;
13
14/// Bootstrap configuration for cluster initialization
15///
16/// # Examples
17///
18/// ```rust,no_run
19/// use redis_enterprise::{BootstrapConfig, ClusterBootstrap, CredentialsBootstrap};
20///
21/// let config = BootstrapConfig::builder()
22///     .action("create_cluster")
23///     .cluster(ClusterBootstrap::builder()
24///         .name("my-cluster.local")
25///         .rack_aware(true)
26///         .build())
27///     .credentials(CredentialsBootstrap::builder()
28///         .username("admin@example.com")
29///         .password("secure-password")
30///         .build())
31///     .build();
32/// ```
33#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
34pub struct BootstrapConfig {
35    /// Action to perform (e.g., 'create', 'join', 'recover_cluster')
36    #[builder(setter(into))]
37    pub action: String,
38    /// Cluster configuration for initialization
39    #[serde(skip_serializing_if = "Option::is_none")]
40    #[builder(default, setter(strip_option))]
41    pub cluster: Option<ClusterBootstrap>,
42    /// Node configuration for bootstrap
43    #[serde(skip_serializing_if = "Option::is_none")]
44    #[builder(default, setter(strip_option))]
45    pub node: Option<NodeBootstrap>,
46    /// Admin credentials for cluster access
47    #[serde(skip_serializing_if = "Option::is_none")]
48    #[builder(default, setter(strip_option))]
49    pub credentials: Option<CredentialsBootstrap>,
50}
51
52/// Cluster bootstrap configuration
53#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
54pub struct ClusterBootstrap {
55    /// Cluster name for identification
56    #[builder(setter(into))]
57    pub name: String,
58    /// DNS suffixes for cluster FQDN resolution
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[builder(default, setter(strip_option))]
61    pub dns_suffixes: Option<Vec<String>>,
62    /// Enable rack-aware placement for high availability
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[builder(default, setter(strip_option))]
65    pub rack_aware: Option<bool>,
66}
67
68/// Node bootstrap configuration
69#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
70pub struct NodeBootstrap {
71    /// Storage paths configuration for the node
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[builder(default, setter(strip_option))]
74    pub paths: Option<NodePaths>,
75}
76
77/// Node paths configuration
78#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
79pub struct NodePaths {
80    /// Path for persistent storage (databases, configuration, logs)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    #[builder(default, setter(into, strip_option))]
83    pub persistent_path: Option<String>,
84    /// Path for ephemeral storage (temporary files, caches)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    #[builder(default, setter(into, strip_option))]
87    pub ephemeral_path: Option<String>,
88}
89
90/// Credentials bootstrap configuration
91#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
92pub struct CredentialsBootstrap {
93    /// Admin username for cluster management
94    #[builder(setter(into))]
95    pub username: String,
96    /// Admin password for authentication
97    #[builder(setter(into))]
98    pub password: String,
99}
100
101/// Bootstrap status response
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct BootstrapStatus {
104    /// Current status of the bootstrap operation
105    pub status: String,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    /// Progress percentage (0.0-100.0) of the bootstrap operation
108    pub progress: Option<f32>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    /// Status message or error description
111    pub message: Option<String>,
112}
113
114/// Bootstrap handler for cluster initialization
115pub struct BootstrapHandler {
116    client: RestClient,
117}
118
119impl BootstrapHandler {
120    pub fn new(client: RestClient) -> Self {
121        BootstrapHandler { client }
122    }
123
124    /// Initialize cluster bootstrap
125    pub async fn create(&self, config: BootstrapConfig) -> Result<BootstrapStatus> {
126        self.client.post("/v1/bootstrap", &config).await
127    }
128
129    /// Get bootstrap status
130    pub async fn status(&self) -> Result<BootstrapStatus> {
131        self.client.get("/v1/bootstrap").await
132    }
133
134    /// Join node to existing cluster
135    pub async fn join(&self, config: BootstrapConfig) -> Result<BootstrapStatus> {
136        self.client.post("/v1/bootstrap/join", &config).await
137    }
138
139    /// Reset bootstrap (dangerous operation)
140    pub async fn reset(&self) -> Result<()> {
141        self.client.delete("/v1/bootstrap").await
142    }
143
144    /// Validate bootstrap for a specific UID - POST /v1/bootstrap/validate/{uid}
145    pub async fn validate_for(&self, uid: u32, body: Value) -> Result<Value> {
146        self.client
147            .post(&format!("/v1/bootstrap/validate/{}", uid), &body)
148            .await
149    }
150
151    /// Post a specific bootstrap action - POST /v1/bootstrap/{action}
152    pub async fn post_action(&self, action: &str, body: Value) -> Result<Value> {
153        self.client
154            .post(&format!("/v1/bootstrap/{}", action), &body)
155            .await
156    }
157}