torrust_tracker_deployer_lib/shared/docker_image.rs
1//! Docker image reference value object
2//!
3//! This module provides a strongly-typed Docker image reference that combines
4//! a repository name with a tag. It is used to represent Docker images in
5//! service configurations and templates.
6//!
7//! # Design Decision
8//!
9//! Docker image versions are **not user-configurable** — they are pinned as
10//! constants in the code to ensure compatibility between the deployer and the
11//! images it uses. Exposing them through domain configs (rather than hardcoding
12//! in templates) gives us:
13//!
14//! - A **single source of truth** for each image version
15//! - The ability to **inspect images via the `show` command**
16//! - Automatic propagation of version changes to both templates and CI scanning
17//!
18//! # Examples
19//!
20//! ```rust
21//! use torrust_tracker_deployer_lib::shared::docker_image::DockerImage;
22//!
23//! let image = DockerImage::new("torrust/tracker", "develop");
24//! assert_eq!(image.full_reference(), "torrust/tracker:develop");
25//! assert_eq!(image.repository(), "torrust/tracker");
26//! assert_eq!(image.tag(), "develop");
27//! ```
28
29use std::fmt;
30
31use serde::{Deserialize, Serialize};
32
33/// Docker image reference with repository and tag
34///
35/// Represents an image reference of the form `repository:tag`,
36/// e.g. `torrust/tracker:develop` or `mysql:8.4`.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct DockerImage {
39 repository: String,
40 tag: String,
41}
42
43impl DockerImage {
44 /// Creates a new Docker image reference
45 ///
46 /// # Examples
47 ///
48 /// ```rust
49 /// use torrust_tracker_deployer_lib::shared::docker_image::DockerImage;
50 ///
51 /// let image = DockerImage::new("torrust/tracker", "develop");
52 /// assert_eq!(image.repository(), "torrust/tracker");
53 /// assert_eq!(image.tag(), "develop");
54 /// ```
55 #[must_use]
56 pub fn new(repository: impl Into<String>, tag: impl Into<String>) -> Self {
57 Self {
58 repository: repository.into(),
59 tag: tag.into(),
60 }
61 }
62
63 /// Returns the repository name (e.g. `"torrust/tracker"`)
64 #[must_use]
65 pub fn repository(&self) -> &str {
66 &self.repository
67 }
68
69 /// Returns the image tag (e.g. `"develop"` or `"8.4"`)
70 #[must_use]
71 pub fn tag(&self) -> &str {
72 &self.tag
73 }
74
75 /// Returns the full image reference as `repository:tag`
76 ///
77 /// # Examples
78 ///
79 /// ```rust
80 /// use torrust_tracker_deployer_lib::shared::docker_image::DockerImage;
81 ///
82 /// let image = DockerImage::new("mysql", "8.4");
83 /// assert_eq!(image.full_reference(), "mysql:8.4");
84 /// ```
85 #[must_use]
86 pub fn full_reference(&self) -> String {
87 format!("{}:{}", self.repository, self.tag)
88 }
89}
90
91impl fmt::Display for DockerImage {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "{}:{}", self.repository, self.tag)
94 }
95}
96
97impl From<(&str, &str)> for DockerImage {
98 fn from((repository, tag): (&str, &str)) -> Self {
99 Self::new(repository, tag)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn it_should_create_docker_image_with_repository_and_tag() {
109 let image = DockerImage::new("torrust/tracker", "develop");
110
111 assert_eq!(image.repository(), "torrust/tracker");
112 assert_eq!(image.tag(), "develop");
113 }
114
115 #[test]
116 fn it_should_return_full_reference_as_repository_colon_tag() {
117 let image = DockerImage::new("torrust/tracker", "develop");
118
119 assert_eq!(image.full_reference(), "torrust/tracker:develop");
120 }
121
122 #[test]
123 fn it_should_display_as_full_reference() {
124 let image = DockerImage::new("mysql", "8.4");
125
126 assert_eq!(format!("{image}"), "mysql:8.4");
127 }
128
129 #[test]
130 fn it_should_create_from_str_tuple() {
131 let image = DockerImage::from(("prom/prometheus", "v3.5.1"));
132
133 assert_eq!(image.full_reference(), "prom/prometheus:v3.5.1");
134 }
135
136 #[test]
137 fn it_should_implement_equality() {
138 let a = DockerImage::new("grafana/grafana", "12.4.2");
139 let b = DockerImage::new("grafana/grafana", "12.4.2");
140 let c = DockerImage::new("grafana/grafana", "11.4.0");
141
142 assert_eq!(a, b);
143 assert_ne!(a, c);
144 }
145}