docker_pyo3/
lib.rs

1#[macro_use]
2mod macros;
3pub mod compose;
4pub mod config;
5pub mod container;
6pub mod exec;
7pub mod image;
8pub mod network;
9pub mod node;
10pub mod plugin;
11pub mod secret;
12pub mod service;
13pub mod task;
14pub mod volume;
15
16use pyo3::prelude::*;
17use pyo3::types::PyDict;
18use pyo3::wrap_pymodule;
19
20use docker_api::models::{EventMessage, PingInfo, SystemInfo, SystemVersion};
21use docker_api::opts::EventsOpts;
22use docker_api::Docker;
23
24use futures_util::stream::StreamExt;
25
26use pythonize::pythonize;
27
28use config::Pyo3Configs;
29use container::Pyo3Containers;
30use image::Pyo3Images;
31use network::Pyo3Networks;
32use node::Pyo3Nodes;
33use plugin::Pyo3Plugins;
34use secret::Pyo3Secrets;
35use service::Pyo3Services;
36use task::Pyo3Tasks;
37use volume::Pyo3Volumes;
38
39#[cfg(unix)]
40static SYSTEM_DEFAULT_URI: &str = "unix:///var/run/docker.sock";
41
42#[cfg(not(unix))]
43static SYSTEM_DEFAULT_URI: &str = "tcp://localhost:2375";
44
45/// Docker client for interacting with the Docker daemon.
46///
47/// Examples:
48///     >>> docker = Docker()  # Connect to default socket
49///     >>> docker = Docker("unix:///var/run/docker.sock")
50///     >>> docker = Docker("tcp://localhost:2375")
51#[pyclass(name = "Docker")]
52#[derive(Clone, Debug)]
53pub struct Pyo3Docker(pub Docker);
54
55#[pymethods]
56impl Pyo3Docker {
57    #[new]
58    #[pyo3(signature = ( uri = SYSTEM_DEFAULT_URI))]
59    /// Create a new Docker client.
60    ///
61    /// Args:
62    ///     uri: URI to connect to the Docker daemon. Defaults to the system default
63    ///          (unix:///var/run/docker.sock on Unix, tcp://localhost:2375 on Windows).
64    ///
65    /// Returns:
66    ///     Docker client instance
67    fn py_new(uri: &str) -> Self {
68        Pyo3Docker(Docker::new(uri).unwrap())
69    }
70
71    /// Get Docker version information.
72    ///
73    /// Returns:
74    ///     dict: Version information including API version, OS, architecture, etc.
75    fn version(&self) -> Py<PyAny> {
76        let sv = __version(self.clone());
77        pythonize_this!(sv)
78    }
79
80    /// Get Docker system information.
81    ///
82    /// Returns:
83    ///     dict: System information including containers count, images count, storage driver, etc.
84    fn info(&self) -> Py<PyAny> {
85        let si = __info(self.clone());
86        pythonize_this!(si)
87    }
88
89    /// Ping the Docker daemon to verify connectivity.
90    ///
91    /// Returns:
92    ///     dict: Ping response from the daemon
93    fn ping(&self) -> Py<PyAny> {
94        let pi = __ping(self.clone());
95        pythonize_this!(pi)
96    }
97
98    /// Get data usage information for Docker objects.
99    ///
100    /// Returns:
101    ///     dict: Data usage statistics for containers, images, volumes, and build cache
102    ///
103    /// Note: Uses docker CLI to avoid Docker API v1.44+ VirtualSize compatibility issues.
104    fn data_usage(&self) -> PyResult<Py<PyAny>> {
105        let du = __data_usage_via_cli();
106        match du {
107            Ok(du) => Ok(pythonize_this!(du)),
108            Err(e) => Err(pyo3::exceptions::PySystemError::new_err(e)),
109        }
110    }
111
112    /// Get a stream of Docker events.
113    ///
114    /// Retrieves real-time events from the Docker daemon such as container starts,
115    /// stops, image pulls, etc.
116    ///
117    /// Args:
118    ///     limit: Maximum number of events to return (default: 100)
119    ///
120    /// Returns:
121    ///     list[dict]: List of Docker events, each containing type, action, actor, time, etc.
122    #[pyo3(signature = (limit=None))]
123    fn events(&self, limit: Option<usize>) -> Py<PyAny> {
124        let limit = limit.unwrap_or(100);
125        let events = __events(self.clone(), limit);
126        pythonize_this!(events)
127    }
128
129    /// Get a Containers interface for managing containers.
130    ///
131    /// Returns:
132    ///     Containers: Interface for container operations
133    fn containers(&'_ self) -> Pyo3Containers {
134        Pyo3Containers::new(self.clone())
135    }
136
137    /// Get an Images interface for managing images.
138    ///
139    /// Returns:
140    ///     Images: Interface for image operations
141    fn images(&'_ self) -> Pyo3Images {
142        Pyo3Images::new(self.clone())
143    }
144
145    /// Get a Networks interface for managing networks.
146    ///
147    /// Returns:
148    ///     Networks: Interface for network operations
149    fn networks(&'_ self) -> Pyo3Networks {
150        Pyo3Networks::new(self.clone())
151    }
152
153    /// Get a Volumes interface for managing volumes.
154    ///
155    /// Returns:
156    ///     Volumes: Interface for volume operations
157    fn volumes(&'_ self) -> Pyo3Volumes {
158        Pyo3Volumes::new(self.clone())
159    }
160
161    /// Get a Nodes interface for managing Swarm nodes.
162    ///
163    /// Swarm mode must be enabled for these operations to work.
164    ///
165    /// Returns:
166    ///     Nodes: Interface for node operations
167    fn nodes(&'_ self) -> Pyo3Nodes {
168        Pyo3Nodes::new(self.clone())
169    }
170
171    /// Get a Services interface for managing Swarm services.
172    ///
173    /// Swarm mode must be enabled for these operations to work.
174    ///
175    /// Returns:
176    ///     Services: Interface for service operations
177    fn services(&'_ self) -> Pyo3Services {
178        Pyo3Services::new(self.clone())
179    }
180
181    /// Get a Tasks interface for managing Swarm tasks.
182    ///
183    /// Tasks are individual units of work running on swarm nodes as part of a service.
184    /// Swarm mode must be enabled for these operations to work.
185    ///
186    /// Returns:
187    ///     Tasks: Interface for task operations
188    fn tasks(&'_ self) -> Pyo3Tasks {
189        Pyo3Tasks::new(self.clone())
190    }
191
192    /// Get a Secrets interface for managing Swarm secrets.
193    ///
194    /// Secrets are sensitive data that can be mounted into containers.
195    /// Swarm mode must be enabled for these operations to work.
196    ///
197    /// Returns:
198    ///     Secrets: Interface for secret operations
199    fn secrets(&'_ self) -> Pyo3Secrets {
200        Pyo3Secrets::new(self.clone())
201    }
202
203    /// Get a Configs interface for managing Swarm configs.
204    ///
205    /// Configs are non-sensitive configuration data that can be mounted into containers.
206    /// Swarm mode must be enabled for these operations to work.
207    ///
208    /// Returns:
209    ///     Configs: Interface for config operations
210    fn configs(&'_ self) -> Pyo3Configs {
211        Pyo3Configs::new(self.clone())
212    }
213
214    /// Get a Plugins interface for managing Docker plugins.
215    ///
216    /// Docker plugins extend the capabilities of the Docker daemon, providing
217    /// additional volume drivers, network drivers, and other extensions.
218    ///
219    /// Returns:
220    ///     Plugins: Interface for plugin operations
221    fn plugins(&'_ self) -> Pyo3Plugins {
222        Pyo3Plugins::new(self.clone())
223    }
224}
225
226#[tokio::main]
227async fn __version(docker: Pyo3Docker) -> SystemVersion {
228    let version = docker.0.version().await;
229    version.unwrap()
230}
231
232#[tokio::main]
233async fn __info(docker: Pyo3Docker) -> SystemInfo {
234    let info = docker.0.info().await;
235    info.unwrap()
236}
237
238#[tokio::main]
239async fn __ping(docker: Pyo3Docker) -> PingInfo {
240    let ping = docker.0.ping().await;
241    ping.unwrap()
242}
243
244/// Data usage item from docker CLI
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246#[serde(rename_all = "PascalCase")]
247struct DataUsageItem {
248    #[serde(rename = "Type")]
249    pub type_: String,
250    pub total_count: String,
251    pub active: String,
252    pub size: String,
253    pub reclaimable: String,
254}
255
256/// Data usage response compatible with Docker API v1.44+
257#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
258#[serde(rename_all = "PascalCase")]
259struct DataUsageCompat {
260    pub images: Option<DataUsageItem>,
261    pub containers: Option<DataUsageItem>,
262    pub volumes: Option<DataUsageItem>,
263    pub build_cache: Option<DataUsageItem>,
264}
265
266/// Get data usage via docker CLI to avoid VirtualSize issues.
267fn __data_usage_via_cli() -> Result<DataUsageCompat, String> {
268    use std::process::Command;
269
270    let output = Command::new("docker")
271        .args(["system", "df", "--format", "json"])
272        .output()
273        .map_err(|e| format!("Failed to execute docker: {}", e))?;
274
275    if !output.status.success() {
276        return Err(format!(
277            "docker system df failed: {}",
278            String::from_utf8_lossy(&output.stderr)
279        ));
280    }
281
282    let stdout = String::from_utf8_lossy(&output.stdout);
283
284    // Docker outputs one JSON object per line (NDJSON format)
285    let mut result = DataUsageCompat {
286        images: None,
287        containers: None,
288        volumes: None,
289        build_cache: None,
290    };
291
292    for line in stdout.lines() {
293        if line.trim().is_empty() {
294            continue;
295        }
296        let item: DataUsageItem =
297            serde_json::from_str(line).map_err(|e| format!("Failed to parse JSON: {}", e))?;
298
299        match item.type_.as_str() {
300            "Images" => result.images = Some(item),
301            "Containers" => result.containers = Some(item),
302            "Local Volumes" => result.volumes = Some(item),
303            "Build Cache" => result.build_cache = Some(item),
304            _ => {}
305        }
306    }
307
308    Ok(result)
309}
310
311#[tokio::main]
312async fn __events(docker: Pyo3Docker, limit: usize) -> Vec<EventMessage> {
313    let opts = EventsOpts::builder().build();
314    let mut events_stream = docker.0.events(&opts);
315    let mut events = Vec::new();
316
317    // Use tokio timeout to avoid blocking indefinitely when no events
318    let timeout_duration = std::time::Duration::from_secs(1);
319
320    loop {
321        let event_result = tokio::time::timeout(timeout_duration, events_stream.next()).await;
322        match event_result {
323            Ok(Some(Ok(event))) => {
324                events.push(event);
325                if events.len() >= limit {
326                    break;
327                }
328            }
329            Ok(Some(Err(_))) => break,
330            Ok(None) => break,
331            Err(_) => break, // Timeout - no more events available
332        }
333    }
334
335    events
336}
337
338/// A Python module implemented in Rust.
339#[pymodule]
340pub fn docker_pyo3(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
341    m.add_class::<Pyo3Docker>()?;
342
343    m.add_wrapped(wrap_pymodule!(compose::compose))?;
344    m.add_wrapped(wrap_pymodule!(config::config))?;
345    m.add_wrapped(wrap_pymodule!(image::image))?;
346    m.add_wrapped(wrap_pymodule!(container::container))?;
347    m.add_wrapped(wrap_pymodule!(exec::exec))?;
348    m.add_wrapped(wrap_pymodule!(network::network))?;
349    m.add_wrapped(wrap_pymodule!(node::node))?;
350    m.add_wrapped(wrap_pymodule!(plugin::plugin))?;
351    m.add_wrapped(wrap_pymodule!(secret::secret))?;
352    m.add_wrapped(wrap_pymodule!(service::service))?;
353    m.add_wrapped(wrap_pymodule!(task::task))?;
354    m.add_wrapped(wrap_pymodule!(volume::volume))?;
355
356    let sys = PyModule::import(_py, "sys")?;
357    let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.cast_into()?;
358    sys_modules.set_item("docker_pyo3.compose", m.getattr("compose")?)?;
359    sys_modules.set_item("docker_pyo3.config", m.getattr("config")?)?;
360    sys_modules.set_item("docker_pyo3.image", m.getattr("image")?)?;
361    sys_modules.set_item("docker_pyo3.container", m.getattr("container")?)?;
362    sys_modules.set_item("docker_pyo3.exec", m.getattr("exec")?)?;
363    sys_modules.set_item("docker_pyo3.network", m.getattr("network")?)?;
364    sys_modules.set_item("docker_pyo3.node", m.getattr("node")?)?;
365    sys_modules.set_item("docker_pyo3.plugin", m.getattr("plugin")?)?;
366    sys_modules.set_item("docker_pyo3.secret", m.getattr("secret")?)?;
367    sys_modules.set_item("docker_pyo3.service", m.getattr("service")?)?;
368    sys_modules.set_item("docker_pyo3.task", m.getattr("task")?)?;
369    sys_modules.set_item("docker_pyo3.volume", m.getattr("volume")?)?;
370
371    Ok(())
372}