Skip to main content

ironflow_ops_docker/containers/
manage.rs

1//! Container management: kill, remove, inspect, list.
2
3use std::collections::HashMap;
4
5use async_trait::async_trait;
6use bollard::Docker;
7use bollard::query_parameters::{
8    InspectContainerOptions, KillContainerOptions, ListContainersOptions, RemoveContainerOptions,
9};
10use ironflow_core::error::OperationError;
11use ironflow_core::operation::{Operation, OperationContext, TypedOperation};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::containers::DockerRef;
16use crate::helpers::{docker_error, to_value};
17
18// ---------------------------------------------------------------------------
19// ContainerKill
20// ---------------------------------------------------------------------------
21
22/// Output of a container kill.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ContainerKillOutput {
25    /// The container ID or name.
26    pub container: String,
27}
28
29/// Send a signal to a container.
30///
31/// # Examples
32///
33/// ```no_run
34/// use ironflow_ops_docker::containers::ContainerKill;
35/// use ironflow_ops_docker::DockerClient;
36/// use ironflow_core::operation::Operation;
37///
38/// let client = DockerClient::connect_local().unwrap();
39/// let op = ContainerKill::new(&client, "my-container");
40/// assert_eq!(op.kind(), "docker");
41/// ```
42pub struct ContainerKill {
43    docker: Docker,
44    container: String,
45    signal: Option<String>,
46}
47
48impl ContainerKill {
49    /// Create a new container-kill operation.
50    pub fn new(client: impl Into<DockerRef>, container: impl Into<String>) -> Self {
51        Self {
52            docker: client.into().0,
53            container: container.into(),
54            signal: None,
55        }
56    }
57
58    /// Set the signal to send (defaults to SIGKILL).
59    pub fn signal(mut self, signal: impl Into<String>) -> Self {
60        self.signal = Some(signal.into());
61        self
62    }
63
64    /// Execute and return a typed result.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`OperationError::External`] if the container does not exist.
69    pub async fn run(
70        &self,
71        _ctx: &OperationContext,
72    ) -> Result<ContainerKillOutput, OperationError> {
73        let options = KillContainerOptions {
74            signal: self.signal.clone().unwrap_or_else(|| "SIGKILL".to_string()),
75        };
76        self.docker
77            .kill_container(&self.container, Some(options))
78            .await
79            .map_err(docker_error)?;
80        Ok(ContainerKillOutput {
81            container: self.container.clone(),
82        })
83    }
84}
85
86#[async_trait]
87impl Operation for ContainerKill {
88    fn kind(&self) -> &str {
89        "docker"
90    }
91
92    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
93        to_value(&self.run(ctx).await?)
94    }
95
96    fn input(&self) -> Option<Value> {
97        Some(serde_json::json!({
98            "operation": "container_kill",
99            "container": self.container,
100        }))
101    }
102}
103
104impl TypedOperation for ContainerKill {
105    type Output = ContainerKillOutput;
106}
107
108// ---------------------------------------------------------------------------
109// ContainerRemove
110// ---------------------------------------------------------------------------
111
112/// Output of a container removal.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ContainerRemoveOutput {
115    /// The container ID or name that was removed.
116    pub container: String,
117}
118
119/// Remove a container.
120///
121/// # Examples
122///
123/// ```no_run
124/// use ironflow_ops_docker::containers::ContainerRemove;
125/// use ironflow_ops_docker::DockerClient;
126/// use ironflow_core::operation::Operation;
127///
128/// let client = DockerClient::connect_local().unwrap();
129/// let op = ContainerRemove::new(&client, "my-container");
130/// assert_eq!(op.kind(), "docker");
131/// ```
132pub struct ContainerRemove {
133    docker: Docker,
134    container: String,
135    force: bool,
136    volumes: bool,
137}
138
139impl ContainerRemove {
140    /// Create a new container-remove operation.
141    pub fn new(client: impl Into<DockerRef>, container: impl Into<String>) -> Self {
142        Self {
143            docker: client.into().0,
144            container: container.into(),
145            force: false,
146            volumes: false,
147        }
148    }
149
150    /// Force-remove the container even if it is running.
151    pub fn force(mut self) -> Self {
152        self.force = true;
153        self
154    }
155
156    /// Also remove anonymous volumes associated with the container.
157    pub fn volumes(mut self) -> Self {
158        self.volumes = true;
159        self
160    }
161
162    /// Execute and return a typed result.
163    ///
164    /// # Errors
165    ///
166    /// Returns [`OperationError::External`] if the container does not exist.
167    pub async fn run(
168        &self,
169        _ctx: &OperationContext,
170    ) -> Result<ContainerRemoveOutput, OperationError> {
171        let options = RemoveContainerOptions {
172            force: self.force,
173            v: self.volumes,
174            ..Default::default()
175        };
176        self.docker
177            .remove_container(&self.container, Some(options))
178            .await
179            .map_err(docker_error)?;
180        Ok(ContainerRemoveOutput {
181            container: self.container.clone(),
182        })
183    }
184}
185
186#[async_trait]
187impl Operation for ContainerRemove {
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": "container_remove",
199            "container": self.container,
200            "force": self.force,
201        }))
202    }
203}
204
205impl TypedOperation for ContainerRemove {
206    type Output = ContainerRemoveOutput;
207}
208
209// ---------------------------------------------------------------------------
210// ContainerInspect
211// ---------------------------------------------------------------------------
212
213/// Output of a container inspection.
214#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ContainerInspectOutput {
216    /// The full inspection response as JSON.
217    pub data: Value,
218}
219
220/// Inspect a container.
221///
222/// # Examples
223///
224/// ```no_run
225/// use ironflow_ops_docker::containers::ContainerInspect;
226/// use ironflow_ops_docker::DockerClient;
227/// use ironflow_core::operation::Operation;
228///
229/// let client = DockerClient::connect_local().unwrap();
230/// let op = ContainerInspect::new(&client, "my-container");
231/// assert_eq!(op.kind(), "docker");
232/// ```
233pub struct ContainerInspect {
234    docker: Docker,
235    container: String,
236}
237
238impl ContainerInspect {
239    /// Create a new container-inspect operation.
240    pub fn new(client: impl Into<DockerRef>, container: impl Into<String>) -> Self {
241        Self {
242            docker: client.into().0,
243            container: container.into(),
244        }
245    }
246
247    /// Execute and return a typed result.
248    ///
249    /// # Errors
250    ///
251    /// Returns [`OperationError::External`] if the container does not exist.
252    pub async fn run(
253        &self,
254        _ctx: &OperationContext,
255    ) -> Result<ContainerInspectOutput, OperationError> {
256        let options = InspectContainerOptions { size: false };
257        let response = self
258            .docker
259            .inspect_container(&self.container, Some(options))
260            .await
261            .map_err(docker_error)?;
262        let data = serde_json::to_value(&response).map_err(|e| OperationError::External {
263            origin: "docker".to_string(),
264            message: e.to_string(),
265        })?;
266        Ok(ContainerInspectOutput { data })
267    }
268}
269
270#[async_trait]
271impl Operation for ContainerInspect {
272    fn kind(&self) -> &str {
273        "docker"
274    }
275
276    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
277        to_value(&self.run(ctx).await?)
278    }
279
280    fn input(&self) -> Option<Value> {
281        Some(serde_json::json!({
282            "operation": "container_inspect",
283            "container": self.container,
284        }))
285    }
286}
287
288impl TypedOperation for ContainerInspect {
289    type Output = ContainerInspectOutput;
290}
291
292// ---------------------------------------------------------------------------
293// ContainerList
294// ---------------------------------------------------------------------------
295
296/// A single entry in the container list.
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct ContainerListEntry {
299    /// Container ID.
300    pub id: String,
301    /// Container names.
302    pub names: Vec<String>,
303    /// Image name.
304    pub image: String,
305    /// Current state.
306    pub state: String,
307    /// Human-readable status string.
308    pub status: String,
309}
310
311/// Output of a container list.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct ContainerListOutput {
314    /// The containers.
315    pub containers: Vec<ContainerListEntry>,
316}
317
318/// List containers.
319///
320/// # Examples
321///
322/// ```no_run
323/// use ironflow_ops_docker::containers::ContainerList;
324/// use ironflow_ops_docker::DockerClient;
325/// use ironflow_core::operation::Operation;
326///
327/// let client = DockerClient::connect_local().unwrap();
328/// let op = ContainerList::new(&client);
329/// assert_eq!(op.kind(), "docker");
330/// ```
331pub struct ContainerList {
332    docker: Docker,
333    all: bool,
334    filters: HashMap<String, Vec<String>>,
335}
336
337impl ContainerList {
338    /// Create a new container-list operation.
339    pub fn new(client: impl Into<DockerRef>) -> Self {
340        Self {
341            docker: client.into().0,
342            all: false,
343            filters: HashMap::new(),
344        }
345    }
346
347    /// Include stopped containers.
348    pub fn all(mut self) -> Self {
349        self.all = true;
350        self
351    }
352
353    /// Add a filter.
354    pub fn filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
355        self.filters
356            .entry(key.into())
357            .or_default()
358            .push(value.into());
359        self
360    }
361
362    /// Execute and return a typed result.
363    ///
364    /// # Errors
365    ///
366    /// Returns [`OperationError::External`] if the Docker daemon is
367    /// unreachable.
368    pub async fn run(
369        &self,
370        _ctx: &OperationContext,
371    ) -> Result<ContainerListOutput, OperationError> {
372        let options = ListContainersOptions {
373            all: self.all,
374            filters: Some(self.filters.clone()),
375            ..Default::default()
376        };
377        let containers = self
378            .docker
379            .list_containers(Some(options))
380            .await
381            .map_err(docker_error)?;
382        let entries = containers
383            .into_iter()
384            .map(|c| ContainerListEntry {
385                id: c.id.unwrap_or_default(),
386                names: c.names.unwrap_or_default(),
387                image: c.image.unwrap_or_default(),
388                state: c.state.map(|s| s.to_string()).unwrap_or_default(),
389                status: c.status.unwrap_or_default(),
390            })
391            .collect();
392        Ok(ContainerListOutput {
393            containers: entries,
394        })
395    }
396}
397
398#[async_trait]
399impl Operation for ContainerList {
400    fn kind(&self) -> &str {
401        "docker"
402    }
403
404    async fn execute(&self, ctx: &OperationContext) -> Result<Value, OperationError> {
405        to_value(&self.run(ctx).await?)
406    }
407
408    fn input(&self) -> Option<Value> {
409        Some(serde_json::json!({
410            "operation": "container_list",
411            "all": self.all,
412        }))
413    }
414}
415
416impl TypedOperation for ContainerList {
417    type Output = ContainerListOutput;
418}