Skip to main content

redis_enterprise/
modules.rs

1//! Redis module management
2//!
3//! ## Overview
4//! - List available modules
5//! - Query module versions
6//! - Configure module settings
7
8use crate::client::RestClient;
9use crate::error::Result;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14/// Platform-specific information for a module
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct PlatformInfo {
17    /// Platform dependencies (typically an empty object)
18    #[serde(default)]
19    pub dependencies: Value,
20
21    /// SHA256 checksum of the module binary for this platform
22    pub sha256: Option<String>,
23}
24
25/// Module information
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Module {
28    /// Unique identifier (read-only).
29    pub uid: String,
30    /// Module name (e.g. `"search"`, `"timeseries"`).
31    pub module_name: Option<String>,
32    /// Version (read-only).
33    pub version: Option<u32>,
34    /// Semantic version string.
35    pub semantic_version: Option<String>,
36    /// Author name.
37    pub author: Option<String>,
38    /// Human-readable description.
39    pub description: Option<String>,
40    /// Homepage URL.
41    pub homepage: Option<String>,
42    /// License identifier.
43    pub license: Option<String>,
44    /// Default command-line arguments.
45    pub command_line_args: Option<String>,
46    /// List of module capabilities.
47    pub capabilities: Option<Vec<String>>,
48    /// Minimum compatible Redis version.
49    pub min_redis_version: Option<String>,
50    /// Compatible Redis version range.
51    pub compatible_redis_version: Option<String>,
52    /// Display name shown in the UI.
53    pub display_name: Option<String>,
54    /// Whether the module is bundled with Redis Enterprise.
55    pub is_bundled: Option<bool>,
56
57    // Additional fields from API audit
58    /// Whether the module supports BigStore (Auto Tiering) version 2
59    pub bigstore_version_2_support: Option<bool>,
60
61    /// Name of the capability this module provides
62    pub capability_name: Option<String>,
63
64    /// Redis command used to configure this module
65    pub config_command: Option<String>,
66
67    /// CRDB (Conflict-free Replicated Database) configuration
68    /// The API returns an empty object {} for modules without CRDB support
69    pub crdb: Option<Value>,
70
71    /// Module dependencies
72    /// The API returns an empty object {} for modules without dependencies
73    pub dependencies: Option<Value>,
74
75    /// Contact email address of the module author
76    pub email: Option<String>,
77
78    /// Minimum Redis Enterprise version required for this module
79    pub min_redis_pack_version: Option<String>,
80
81    /// Platform-specific information for this module
82    /// Maps platform names (e.g., 'rhel9/x86_64', 'rhel8/x86_64') to platform details
83    #[serde(default)]
84    pub platforms: Option<HashMap<String, PlatformInfo>>,
85
86    /// SHA256 checksum of the module binary for verification
87    pub sha256: Option<String>,
88}
89
90/// Module handler for managing Redis modules
91pub struct ModuleHandler {
92    client: RestClient,
93}
94
95/// Alias for backwards compatibility and intuitive plural naming
96pub type ModulesHandler = ModuleHandler;
97
98impl ModuleHandler {
99    /// Create a new handler bound to the given REST client.
100    pub fn new(client: RestClient) -> Self {
101        ModuleHandler { client }
102    }
103
104    /// List all modules
105    pub async fn list(&self) -> Result<Vec<Module>> {
106        self.client.get("/v1/modules").await
107    }
108
109    /// Get specific module
110    pub async fn get(&self, uid: &str) -> Result<Module> {
111        self.client.get(&format!("/v1/modules/{}", uid)).await
112    }
113
114    /// Upload new module (tries v2 first, falls back to v1)
115    ///
116    /// Note: Some Redis Enterprise versions (particularly RE 8.x) do not support
117    /// module upload via the REST API. In those cases, use the Admin UI or
118    /// node-level CLI tools (rladmin) to upload modules.
119    pub async fn upload(&self, module_data: Vec<u8>, file_name: &str) -> Result<Value> {
120        // Try v2 first (returns action_uid for async tracking)
121        match self
122            .client
123            .post_multipart("/v2/modules", module_data.clone(), "module", file_name)
124            .await
125        {
126            Ok(response) => Ok(response),
127            Err(crate::error::RestError::NotFound) => {
128                // v2 endpoint doesn't exist, try v1
129                match self
130                    .client
131                    .post_multipart("/v1/modules", module_data, "module", file_name)
132                    .await
133                {
134                    Ok(response) => Ok(response),
135                    Err(crate::error::RestError::ApiError { code: 405, .. }) => {
136                        Err(crate::error::RestError::ValidationError(
137                            "Module upload via REST API is not supported in this Redis Enterprise version. \
138                             Use the Admin UI or rladmin CLI to upload modules.".to_string()
139                        ))
140                    }
141                    Err(e) => Err(e),
142                }
143            }
144            Err(crate::error::RestError::ApiError { code: 405, .. }) => {
145                Err(crate::error::RestError::ValidationError(
146                    "Module upload via REST API is not supported in this Redis Enterprise version. \
147                     Use the Admin UI or rladmin CLI to upload modules.".to_string()
148                ))
149            }
150            Err(e) => Err(e),
151        }
152    }
153
154    /// Delete module
155    pub async fn delete(&self, uid: &str) -> Result<()> {
156        self.client.delete(&format!("/v1/modules/{}", uid)).await
157    }
158
159    /// Update module configuration
160    pub async fn update(&self, uid: &str, updates: Value) -> Result<Module> {
161        self.client
162            .put(&format!("/v1/modules/{}", uid), &updates)
163            .await
164    }
165
166    /// Configure modules for a specific database - POST /v1/modules/config/bdb/{uid}
167    pub async fn config_bdb(&self, bdb_uid: u32, config: Value) -> Result<Module> {
168        self.client
169            .post(&format!("/v1/modules/config/bdb/{}", bdb_uid), &config)
170            .await
171    }
172
173    /// List custom module artifacts on the local node.
174    ///
175    /// `GET /v2/local/modules/user-defined/artifacts`. Live verification on
176    /// Redis Enterprise Software 8.0.10-81 returned `200 OK` with `[]` on a
177    /// cluster with no custom modules uploaded.
178    pub async fn list_user_defined_artifacts(&self) -> Result<Value> {
179        self.client
180            .get("/v2/local/modules/user-defined/artifacts")
181            .await
182    }
183
184    /// Upload a custom module artifact to the local node.
185    ///
186    /// `POST /v2/local/modules/user-defined/artifacts`. The endpoint expects
187    /// a multipart upload; the file is sent under the `module` form field
188    /// to match the existing v1/v2 module upload behaviour. Live
189    /// verification on 8.0.10-81 returned `400 no_module` when an empty
190    /// body was sent.
191    pub async fn upload_user_defined_artifact(
192        &self,
193        module_data: Vec<u8>,
194        file_name: &str,
195    ) -> Result<Value> {
196        self.client
197            .post_multipart(
198                "/v2/local/modules/user-defined/artifacts",
199                module_data,
200                "module",
201                file_name,
202            )
203            .await
204    }
205
206    /// Register a previously-uploaded artifact as a cluster-wide
207    /// user-defined module configuration.
208    ///
209    /// `POST /v2/modules/user-defined`. The body shape is version-specific
210    /// (it must reference an artifact that has already been uploaded via
211    /// [`upload_user_defined_artifact`](Self::upload_user_defined_artifact)
212    /// and describe how the cluster should load it); pass a `Value`
213    /// matching the documented payload. Live verification on 8.0.10-81
214    /// returned `406 invalid_module` when an empty body was sent.
215    pub async fn register_user_defined(&self, body: Value) -> Result<Value> {
216        self.client.post_raw("/v2/modules/user-defined", body).await
217    }
218
219    /// Delete a custom module configuration cluster-wide.
220    ///
221    /// `DELETE /v2/modules/user-defined/{uid}`. Live verification on
222    /// 8.0.10-81 returned `404 module_delete_failed` against a uid that
223    /// does not exist on the cluster.
224    pub async fn delete_user_defined(&self, uid: &str) -> Result<()> {
225        self.client
226            .delete(&format!("/v2/modules/user-defined/{}", uid))
227            .await
228    }
229
230    /// Delete a custom module artifact from the local node.
231    ///
232    /// `DELETE /v2/local/modules/user-defined/artifacts/{module_name}/{version}`.
233    pub async fn delete_user_defined_artifact(
234        &self,
235        module_name: &str,
236        version: &str,
237    ) -> Result<()> {
238        self.client
239            .delete(&format!(
240                "/v2/local/modules/user-defined/artifacts/{}/{}",
241                module_name, version
242            ))
243            .await
244    }
245}