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}