docker_wrapper/template/redis/
basic.rs1#![allow(clippy::doc_markdown)]
4#![allow(clippy::must_use_candidate)]
5#![allow(clippy::return_self_not_must_use)]
6#![allow(clippy::needless_borrows_for_generic_args)]
7#![allow(clippy::unnecessary_get_then_check)]
8
9use super::common::{
10 default_redis_health_check, redis_config_volume, redis_connection_string, redis_data_volume,
11 DEFAULT_REDIS_IMAGE, DEFAULT_REDIS_TAG, REDIS_STACK_IMAGE, REDIS_STACK_TAG,
12};
13use crate::template::{HasConnectionString, Template, TemplateConfig};
14use async_trait::async_trait;
15use std::collections::HashMap;
16
17pub struct RedisTemplate {
19 config: TemplateConfig,
20 use_redis_stack: bool,
21}
22
23impl RedisTemplate {
24 pub fn new(name: impl Into<String>) -> Self {
26 let name = name.into();
27 let env = HashMap::new();
28
29 let config = TemplateConfig {
31 name: name.clone(),
32 image: DEFAULT_REDIS_IMAGE.to_string(),
33 tag: DEFAULT_REDIS_TAG.to_string(),
34 ports: vec![(6379, 6379)],
35 env,
36 volumes: Vec::new(),
37 network: None,
38 health_check: Some(default_redis_health_check()),
39 auto_remove: false,
40 memory_limit: None,
41 cpu_limit: None,
42 platform: None,
43 };
44
45 Self {
46 config,
47 use_redis_stack: false,
48 }
49 }
50
51 pub fn port(mut self, port: u16) -> Self {
53 self.config.ports = vec![(port, 6379)];
54 self
55 }
56
57 pub fn password(mut self, password: impl Into<String>) -> Self {
59 self.config
61 .env
62 .insert("REDIS_PASSWORD".to_string(), password.into());
63 self
64 }
65
66 pub fn with_persistence(mut self, volume_name: impl Into<String>) -> Self {
68 self.config.volumes.push(redis_data_volume(volume_name));
69 self
70 }
71
72 pub fn config_file(mut self, config_path: impl Into<String>) -> Self {
74 self.config.volumes.push(redis_config_volume(config_path));
75 self
76 }
77
78 pub fn memory_limit(mut self, limit: impl Into<String>) -> Self {
80 self.config.memory_limit = Some(limit.into());
81 self
82 }
83
84 pub fn cluster_mode(mut self) -> Self {
86 self.config
87 .env
88 .insert("REDIS_CLUSTER".to_string(), "yes".to_string());
89 self
90 }
91
92 pub fn maxmemory_policy(mut self, policy: impl Into<String>) -> Self {
94 self.config
95 .env
96 .insert("REDIS_MAXMEMORY_POLICY".to_string(), policy.into());
97 self
98 }
99
100 pub fn version(mut self, version: impl Into<String>) -> Self {
102 self.config.tag = format!("{}-alpine", version.into());
103 self
104 }
105
106 pub fn network(mut self, network: impl Into<String>) -> Self {
108 self.config.network = Some(network.into());
109 self
110 }
111
112 pub fn auto_remove(mut self) -> Self {
114 self.config.auto_remove = true;
115 self
116 }
117
118 pub fn with_redis_stack(mut self) -> Self {
120 self.use_redis_stack = true;
121 self
122 }
123
124 pub fn custom_image(mut self, image: impl Into<String>, tag: impl Into<String>) -> Self {
126 self.config.image = image.into();
127 self.config.tag = tag.into();
128 self
129 }
130
131 pub fn platform(mut self, platform: impl Into<String>) -> Self {
133 self.config.platform = Some(platform.into());
134 self
135 }
136}
137
138#[async_trait]
139impl Template for RedisTemplate {
140 fn name(&self) -> &str {
141 &self.config.name
142 }
143
144 fn config(&self) -> &TemplateConfig {
145 &self.config
146 }
147
148 fn config_mut(&mut self) -> &mut TemplateConfig {
149 &mut self.config
150 }
151
152 async fn wait_for_ready(&self) -> crate::template::Result<()> {
153 use std::time::Duration;
154 use tokio::time::{sleep, timeout};
155
156 let wait_timeout = Duration::from_secs(60);
159 let check_interval = Duration::from_millis(500);
160
161 timeout(wait_timeout, async {
162 loop {
163 if !self.is_running().await.unwrap_or(false) {
166 sleep(check_interval).await;
167 continue;
168 }
169
170 let password = self.config.env.get("REDIS_PASSWORD");
172 let mut ping_cmd = vec!["redis-cli", "-h", "localhost"];
173
174 let auth_args;
176 if let Some(pass) = password {
177 auth_args = vec!["-a", pass.as_str()];
178 ping_cmd.extend(&auth_args);
179 }
180
181 ping_cmd.push("ping");
182
183 if let Ok(result) = self.exec(ping_cmd).await {
185 if result.stdout.trim() == "PONG" {
186 return Ok(());
187 }
188 }
189
190 sleep(check_interval).await;
191 }
192 })
193 .await
194 .map_err(|_| {
195 crate::template::TemplateError::InvalidConfig(format!(
196 "Redis container {} failed to become ready within timeout",
197 self.config().name
198 ))
199 })?
200 }
201
202 fn build_command(&self) -> crate::RunCommand {
203 let config = self.config();
204
205 let image_tag = if self.use_redis_stack {
207 format!("{REDIS_STACK_IMAGE}:{REDIS_STACK_TAG}")
208 } else {
209 format!("{}:{}", config.image, config.tag)
210 };
211
212 let mut cmd = crate::RunCommand::new(image_tag)
213 .name(&config.name)
214 .detach();
215
216 for (host, container) in &config.ports {
218 cmd = cmd.port(*host, *container);
219 }
220
221 for mount in &config.volumes {
223 if mount.read_only {
224 cmd = cmd.volume_ro(&mount.source, &mount.target);
225 } else {
226 cmd = cmd.volume(&mount.source, &mount.target);
227 }
228 }
229
230 if let Some(network) = &config.network {
232 cmd = cmd.network(network);
233 }
234
235 if let Some(health) = &config.health_check {
237 cmd = cmd
238 .health_cmd(&health.test.join(" "))
239 .health_interval(&health.interval)
240 .health_timeout(&health.timeout)
241 .health_retries(health.retries)
242 .health_start_period(&health.start_period);
243 }
244
245 if let Some(memory) = &config.memory_limit {
247 cmd = cmd.memory(memory);
248 }
249
250 if let Some(cpu) = &config.cpu_limit {
251 cmd = cmd.cpus(cpu);
252 }
253
254 if config.auto_remove {
256 cmd = cmd.remove();
257 }
258
259 if let Some(password) = config.env.get("REDIS_PASSWORD") {
261 if self.use_redis_stack {
262 cmd = cmd.env("REDIS_ARGS", format!("--requirepass {password}"));
264 } else {
265 cmd = cmd.entrypoint("redis-server").cmd(vec![
267 "--requirepass".to_string(),
268 password.clone(),
269 "--protected-mode".to_string(),
270 "yes".to_string(),
271 ]);
272 }
273 }
274
275 let has_config = config
277 .volumes
278 .iter()
279 .any(|v| v.target == "/usr/local/etc/redis/redis.conf");
280 if has_config && config.env.get("REDIS_PASSWORD").is_none() {
281 cmd = cmd.cmd(vec![
282 "redis-server".to_string(),
283 "/usr/local/etc/redis/redis.conf".to_string(),
284 ]);
285 }
286
287 cmd
288 }
289}
290
291impl HasConnectionString for RedisTemplate {
292 fn connection_string(&self) -> String {
310 let port = self.config.ports.first().map_or(6379, |(h, _)| *h);
311 let password = self.config.env.get("REDIS_PASSWORD").map(String::as_str);
312 redis_connection_string("localhost", port, password)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::DockerCommand;
320
321 #[test]
322 fn test_redis_template_basic() {
323 let template = RedisTemplate::new("test-redis");
324 assert_eq!(template.name(), "test-redis");
325 assert_eq!(template.config().image, "redis");
326 assert_eq!(template.config().tag, "7-alpine");
327 assert_eq!(template.config().ports, vec![(6379, 6379)]);
328 }
329
330 #[test]
331 fn test_redis_template_with_password() {
332 let template = RedisTemplate::new("test-redis").password("secret123");
333
334 assert_eq!(
335 template.config().env.get("REDIS_PASSWORD"),
336 Some(&"secret123".to_string())
337 );
338 }
339
340 #[test]
341 fn test_redis_template_with_persistence() {
342 let template = RedisTemplate::new("test-redis").with_persistence("redis-data");
343
344 assert_eq!(template.config().volumes.len(), 1);
345 assert_eq!(template.config().volumes[0].source, "redis-data");
346 assert_eq!(template.config().volumes[0].target, "/data");
347 }
348
349 #[test]
350 fn test_redis_template_custom_port() {
351 let template = RedisTemplate::new("test-redis").port(16379);
352
353 assert_eq!(template.config().ports, vec![(16379, 6379)]);
354 }
355
356 #[test]
357 fn test_redis_build_command() {
358 let template = RedisTemplate::new("test-redis")
359 .password("mypass")
360 .port(16379);
361
362 let cmd = template.build_command();
363 let args = cmd.build_command_args();
364
365 assert!(args.contains(&"run".to_string()));
367 assert!(args.contains(&"--name".to_string()));
368 assert!(args.contains(&"test-redis".to_string()));
369 assert!(args.contains(&"--publish".to_string()));
370 assert!(args.contains(&"16379:6379".to_string()));
371 }
372
373 #[test]
374 fn test_redis_connection_string() {
375 use crate::template::HasConnectionString;
376
377 let template = RedisTemplate::new("test-redis").port(6380);
378 assert_eq!(template.connection_string(), "redis://localhost:6380");
379 }
380
381 #[test]
382 fn test_redis_connection_string_with_password() {
383 use crate::template::HasConnectionString;
384
385 let template = RedisTemplate::new("test-redis")
386 .port(6380)
387 .password("secret");
388 assert_eq!(
389 template.connection_string(),
390 "redis://:secret@localhost:6380"
391 );
392 }
393
394 #[test]
395 fn test_redis_connection_string_default_port() {
396 use crate::template::HasConnectionString;
397
398 let template = RedisTemplate::new("test-redis");
399 assert_eq!(template.connection_string(), "redis://localhost:6379");
400 }
401}