Skip to main content

ironflow_ops_docker/images/
tag.rs

1//! Image tag, history, and search operations.
2
3use async_trait::async_trait;
4use bollard::Docker;
5use bollard::query_parameters::{SearchImagesOptions, TagImageOptions};
6use ironflow_core::error::OperationError;
7use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::containers::DockerRef;
12use crate::helpers::{docker_error, to_value};
13
14// ---------------------------------------------------------------------------
15// ImageTag
16// ---------------------------------------------------------------------------
17
18/// Output of an image tag.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ImageTagOutput {
21    /// The source image.
22    pub source: String,
23    /// The new repository name.
24    pub repo: String,
25    /// The new tag.
26    pub tag: String,
27}
28
29/// Add a tag to an image.
30///
31/// # Examples
32///
33/// ```no_run
34/// use ironflow_ops_docker::images::ImageTag;
35/// use ironflow_ops_docker::DockerClient;
36/// use ironflow_core::operation::Operation;
37///
38/// let client = DockerClient::connect_local().unwrap();
39/// let op = ImageTag::new(&client, "alpine:latest", "my-repo", "v1");
40/// assert_eq!(op.kind(), "docker");
41/// ```
42pub struct ImageTag {
43    docker: Docker,
44    source: String,
45    repo: String,
46    tag: String,
47}
48
49impl ImageTag {
50    /// Create a new image-tag operation.
51    pub fn new(
52        client: impl Into<DockerRef>,
53        source: impl Into<String>,
54        repo: impl Into<String>,
55        tag: impl Into<String>,
56    ) -> Self {
57        Self {
58            docker: client.into().0,
59            source: source.into(),
60            repo: repo.into(),
61            tag: tag.into(),
62        }
63    }
64
65    /// Execute and return a typed result.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`OperationError::External`] if the source image does not exist.
70    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageTagOutput, OperationError> {
71        let options = TagImageOptions {
72            repo: Some(self.repo.clone()),
73            tag: Some(self.tag.clone()),
74        };
75        self.docker
76            .tag_image(&self.source, Some(options))
77            .await
78            .map_err(docker_error)?;
79        Ok(ImageTagOutput {
80            source: self.source.clone(),
81            repo: self.repo.clone(),
82            tag: self.tag.clone(),
83        })
84    }
85}
86
87#[async_trait]
88impl Operation for ImageTag {
89    fn kind(&self) -> &str {
90        "docker"
91    }
92
93    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
94        to_value(&self.run(ctx).await?)
95    }
96
97    fn input(&self) -> Option<Value> {
98        Some(serde_json::json!({
99            "operation": "image_tag",
100            "source": self.source,
101            "repo": self.repo,
102            "tag": self.tag,
103        }))
104    }
105}
106
107impl TypedOperation for ImageTag {
108    type Output = ImageTagOutput;
109}
110
111// ---------------------------------------------------------------------------
112// ImageHistory
113// ---------------------------------------------------------------------------
114
115/// A single layer in the image history.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ImageHistoryEntry {
118    /// Layer ID.
119    pub id: String,
120    /// Created timestamp.
121    pub created: i64,
122    /// Created-by command.
123    pub created_by: String,
124    /// Layer size in bytes.
125    pub size: i64,
126}
127
128/// Output of an image history.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ImageHistoryOutput {
131    /// The layers.
132    pub layers: Vec<ImageHistoryEntry>,
133}
134
135/// Get the history of an image (layers).
136///
137/// # Examples
138///
139/// ```no_run
140/// use ironflow_ops_docker::images::ImageHistory;
141/// use ironflow_ops_docker::DockerClient;
142/// use ironflow_core::operation::Operation;
143///
144/// let client = DockerClient::connect_local().unwrap();
145/// let op = ImageHistory::new(&client, "alpine:latest");
146/// assert_eq!(op.kind(), "docker");
147/// ```
148pub struct ImageHistory {
149    docker: Docker,
150    image: String,
151}
152
153impl ImageHistory {
154    /// Create a new image-history operation.
155    pub fn new(client: impl Into<DockerRef>, image: impl Into<String>) -> Self {
156        Self {
157            docker: client.into().0,
158            image: image.into(),
159        }
160    }
161
162    /// Execute and return a typed result.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`OperationError::External`] if the image does not exist.
167    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageHistoryOutput, OperationError> {
168        let history = self
169            .docker
170            .image_history(&self.image)
171            .await
172            .map_err(docker_error)?;
173        let layers = history
174            .into_iter()
175            .map(|h| ImageHistoryEntry {
176                id: h.id,
177                created: h.created,
178                created_by: h.created_by,
179                size: h.size,
180            })
181            .collect();
182        Ok(ImageHistoryOutput { layers })
183    }
184}
185
186#[async_trait]
187impl Operation for ImageHistory {
188    fn kind(&self) -> &str {
189        "docker"
190    }
191
192    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
193        to_value(&self.run(ctx).await?)
194    }
195
196    fn input(&self) -> Option<Value> {
197        Some(serde_json::json!({
198            "operation": "image_history",
199            "image": self.image,
200        }))
201    }
202}
203
204impl TypedOperation for ImageHistory {
205    type Output = ImageHistoryOutput;
206}
207
208// ---------------------------------------------------------------------------
209// ImageSearch
210// ---------------------------------------------------------------------------
211
212/// A single search result.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ImageSearchEntry {
215    /// Image name.
216    pub name: String,
217    /// Description.
218    pub description: String,
219    /// Star count.
220    pub star_count: i64,
221    /// Whether it is an official image.
222    pub is_official: bool,
223}
224
225/// Output of an image search.
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ImageSearchOutput {
228    /// The search results.
229    pub results: Vec<ImageSearchEntry>,
230}
231
232/// Search for images on the registry.
233///
234/// # Examples
235///
236/// ```no_run
237/// use ironflow_ops_docker::images::ImageSearch;
238/// use ironflow_ops_docker::DockerClient;
239/// use ironflow_core::operation::Operation;
240///
241/// let client = DockerClient::connect_local().unwrap();
242/// let op = ImageSearch::new(&client, "alpine");
243/// assert_eq!(op.kind(), "docker");
244/// ```
245pub struct ImageSearch {
246    docker: Docker,
247    term: String,
248    limit: Option<i64>,
249}
250
251impl ImageSearch {
252    /// Create a new image-search operation.
253    pub fn new(client: impl Into<DockerRef>, term: impl Into<String>) -> Self {
254        Self {
255            docker: client.into().0,
256            term: term.into(),
257            limit: None,
258        }
259    }
260
261    /// Set the maximum number of results.
262    pub fn limit(mut self, limit: i64) -> Self {
263        self.limit = Some(limit);
264        self
265    }
266
267    /// Execute and return a typed result.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`OperationError::External`] if the search fails.
272    pub async fn run(&self, _ctx: &OperationContext) -> Result<ImageSearchOutput, OperationError> {
273        let options = SearchImagesOptions {
274            term: self.term.clone(),
275            limit: self.limit.map(|l| l as i32),
276            ..Default::default()
277        };
278        let results = self
279            .docker
280            .search_images(options)
281            .await
282            .map_err(docker_error)?;
283        let entries = results
284            .into_iter()
285            .map(|r| ImageSearchEntry {
286                name: r.name.unwrap_or_default(),
287                description: r.description.unwrap_or_default(),
288                star_count: r.star_count.unwrap_or(0),
289                is_official: r.is_official.unwrap_or(false),
290            })
291            .collect();
292        Ok(ImageSearchOutput { results: entries })
293    }
294}
295
296#[async_trait]
297impl Operation for ImageSearch {
298    fn kind(&self) -> &str {
299        "docker"
300    }
301
302    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
303        to_value(&self.run(ctx).await?)
304    }
305
306    fn input(&self) -> Option<Value> {
307        Some(serde_json::json!({
308            "operation": "image_search",
309            "term": self.term,
310        }))
311    }
312}
313
314impl TypedOperation for ImageSearch {
315    type Output = ImageSearchOutput;
316}