docker_pyo3/
node.rs

1use std::collections::HashMap;
2
3use crate::Pyo3Docker;
4use docker_api::opts::{NodeListOpts, NodeUpdateOpts};
5use docker_api::{Node, Nodes};
6use pyo3::exceptions;
7use pyo3::prelude::*;
8use pyo3::types::PyDict;
9use pythonize::pythonize;
10
11#[pymodule]
12pub fn node(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
13    m.add_class::<Pyo3Nodes>()?;
14    m.add_class::<Pyo3Node>()?;
15    Ok(())
16}
17
18/// Interface for managing Docker Swarm nodes collection.
19///
20/// Swarm mode must be enabled for these operations to work.
21#[derive(Debug)]
22#[pyclass(name = "Nodes")]
23pub struct Pyo3Nodes(pub Nodes);
24
25/// Represents an individual Docker Swarm node.
26///
27/// Swarm mode must be enabled for these operations to work.
28#[derive(Debug)]
29#[pyclass(name = "Node")]
30pub struct Pyo3Node(pub Node);
31
32#[pymethods]
33impl Pyo3Nodes {
34    #[new]
35    pub fn new(docker: Pyo3Docker) -> Self {
36        Pyo3Nodes(Nodes::new(docker.0))
37    }
38
39    /// Get a specific node by ID or name.
40    ///
41    /// Args:
42    ///     id: Node ID or name
43    ///
44    /// Returns:
45    ///     Node: Node instance
46    pub fn get(&self, id: &str) -> Pyo3Node {
47        Pyo3Node(self.0.get(id))
48    }
49
50    /// List all nodes in the swarm.
51    ///
52    /// Returns:
53    ///     list[dict]: List of node information dictionaries
54    ///
55    /// Raises:
56    ///     SystemError: If the operation fails (e.g., swarm not initialized)
57    pub fn list(&self) -> PyResult<Py<PyAny>> {
58        let rv = __nodes_list(&self.0, &Default::default());
59
60        match rv {
61            Ok(rv) => Ok(pythonize_this!(rv)),
62            Err(rv) => Err(py_sys_exception!(rv)),
63        }
64    }
65}
66
67#[tokio::main]
68async fn __nodes_list(
69    nodes: &Nodes,
70    opts: &NodeListOpts,
71) -> Result<Vec<docker_api::models::Node>, docker_api::Error> {
72    nodes.list(opts).await
73}
74
75#[pymethods]
76impl Pyo3Node {
77    #[new]
78    pub fn new(docker: Pyo3Docker, id: &str) -> Self {
79        Pyo3Node(Node::new(docker.0, id))
80    }
81
82    /// Get the node ID.
83    ///
84    /// Returns:
85    ///     str: Node ID
86    pub fn id(&self) -> String {
87        self.0.name().to_string()
88    }
89
90    /// Inspect the node to get detailed information.
91    ///
92    /// Returns:
93    ///     dict: Detailed node information including status, description, spec, etc.
94    ///
95    /// Raises:
96    ///     SystemError: If the operation fails
97    pub fn inspect(&self) -> PyResult<Py<PyAny>> {
98        let rv = __node_inspect(&self.0);
99
100        match rv {
101            Ok(rv) => Ok(pythonize_this!(rv)),
102            Err(rv) => Err(py_sys_exception!(rv)),
103        }
104    }
105
106    /// Delete the node from the swarm.
107    ///
108    /// Returns:
109    ///     None
110    ///
111    /// Raises:
112    ///     SystemError: If the node cannot be deleted
113    pub fn delete(&self) -> PyResult<()> {
114        let rv = __node_delete(&self.0);
115        match rv {
116            Ok(rv) => Ok(rv),
117            Err(rv) => Err(py_sys_exception!(rv)),
118        }
119    }
120
121    /// Force delete the node from the swarm.
122    ///
123    /// Returns:
124    ///     None
125    ///
126    /// Raises:
127    ///     SystemError: If the node cannot be deleted
128    pub fn force_delete(&self) -> PyResult<()> {
129        let rv = __node_force_delete(&self.0);
130        match rv {
131            Ok(rv) => Ok(rv),
132            Err(rv) => Err(py_sys_exception!(rv)),
133        }
134    }
135
136    /// Update the node configuration.
137    ///
138    /// Args:
139    ///     version: Node version string (required, use inspect() to get current version)
140    ///     name: Node name
141    ///     role: Node role ("worker" or "manager")
142    ///     availability: Node availability ("active", "pause", or "drain")
143    ///     labels: Node labels as dict (e.g., {"env": "prod"})
144    ///
145    /// Returns:
146    ///     None
147    ///
148    /// Raises:
149    ///     SystemError: If the update fails
150    #[pyo3(signature = (version, name=None, role=None, availability=None, labels=None))]
151    pub fn update(
152        &self,
153        version: &str,
154        name: Option<&str>,
155        role: Option<&str>,
156        availability: Option<&str>,
157        labels: Option<&Bound<'_, PyDict>>,
158    ) -> PyResult<()> {
159        use docker_api::models::{NodeSpecAvailabilityInlineItem, NodeSpecRoleInlineItem};
160
161        // Unfortunately the docker-api crate has a design issue where the setter methods
162        // are on NodeUpdateOpts, not on NodeUpdateOptsBuilder. We need to manually construct
163        // the opts by converting the builder struct fields.
164        let rv = __node_update_with_params(
165            &self.0,
166            version,
167            name,
168            role.map(|r| match r.to_lowercase().as_str() {
169                "worker" => Ok(NodeSpecRoleInlineItem::Worker),
170                "manager" => Ok(NodeSpecRoleInlineItem::Manager),
171                _ => Err(exceptions::PyValueError::new_err(format!(
172                    "Invalid role: {}. Must be 'worker' or 'manager'",
173                    r
174                ))),
175            })
176            .transpose()?,
177            availability
178                .map(|a| match a.to_lowercase().as_str() {
179                    "active" => Ok(NodeSpecAvailabilityInlineItem::Active),
180                    "pause" => Ok(NodeSpecAvailabilityInlineItem::Pause),
181                    "drain" => Ok(NodeSpecAvailabilityInlineItem::Drain),
182                    _ => Err(exceptions::PyValueError::new_err(format!(
183                        "Invalid availability: {}. Must be 'active', 'pause', or 'drain'",
184                        a
185                    ))),
186                })
187                .transpose()?,
188            labels.map(|l| l.extract::<HashMap<String, String>>().unwrap()),
189        );
190
191        match rv {
192            Ok(rv) => Ok(rv),
193            Err(rv) => Err(py_sys_exception!(rv)),
194        }
195    }
196}
197
198#[tokio::main]
199async fn __node_inspect(node: &Node) -> Result<docker_api::models::Node, docker_api::Error> {
200    node.inspect().await
201}
202
203#[tokio::main]
204async fn __node_delete(node: &Node) -> Result<(), docker_api::Error> {
205    node.delete().await
206}
207
208#[tokio::main]
209async fn __node_force_delete(node: &Node) -> Result<(), docker_api::Error> {
210    node.force_delete().await
211}
212
213use docker_api::models::{NodeSpecAvailabilityInlineItem, NodeSpecRoleInlineItem};
214
215#[tokio::main]
216async fn __node_update_with_params(
217    node: &Node,
218    version: &str,
219    name: Option<&str>,
220    role: Option<NodeSpecRoleInlineItem>,
221    availability: Option<NodeSpecAvailabilityInlineItem>,
222    labels: Option<HashMap<String, String>>,
223) -> Result<(), docker_api::Error> {
224    // Build the opts by calling methods on NodeUpdateOpts (not the builder)
225    // This is a workaround for the crate's design where builder methods are on the opts struct
226    // We can work around this by using transmute since the builder and opts structs have
227    // identical layout (both are wrappers around the same internal data)
228    let empty_opts = NodeUpdateOpts::builder(version);
229
230    // Use transmute to convert the builder to opts, then apply the setter methods
231    unsafe {
232        let opts: NodeUpdateOpts = std::mem::transmute(empty_opts);
233
234        // Now apply the modifications
235        let opts = if let Some(n) = name {
236            opts.name(n)
237        } else {
238            opts
239        };
240
241        let opts = if let Some(r) = role {
242            opts.role(r)
243        } else {
244            opts
245        };
246
247        let opts = if let Some(a) = availability {
248            opts.availability(a)
249        } else {
250            opts
251        };
252
253        let opts = if let Some(l) = labels {
254            opts.labels(l.iter().map(|(k, v)| (k.as_str(), v.as_str())))
255        } else {
256            opts
257        };
258
259        node.update(&opts).await
260    }
261}