docker_pyo3/
image.rs

1use std::collections::HashMap;
2use std::fs::OpenOptions;
3
4use crate::Pyo3Docker;
5use docker_api::models::{
6    ImageDeleteResponseItem, ImageHistory200Response, ImageInspect, ImagePrune200Response,
7    ImageSummary,
8};
9use docker_api::opts::{
10    ImageBuildOpts, ImageFilter, ImageListOpts, ImageName, ImagePushOpts, PullOpts, RegistryAuth,
11    TagOpts,
12};
13
14use docker_api::{Image, Images};
15use futures_util::StreamExt;
16use pyo3::exceptions;
17use pyo3::prelude::*;
18use pyo3::types::PyDict;
19use pythonize::pythonize;
20use std::io::Write;
21
22#[pymodule]
23pub fn image(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
24    m.add_class::<Pyo3Images>()?;
25    m.add_class::<Pyo3Image>()?;
26    Ok(())
27}
28
29#[derive(Debug)]
30#[pyclass(name = "Images")]
31pub struct Pyo3Images(pub Images);
32
33#[derive(Debug)]
34#[pyclass(name = "Image")]
35pub struct Pyo3Image(pub Image);
36
37#[pymethods]
38impl Pyo3Images {
39    #[new]
40    pub fn new(docker: Pyo3Docker) -> Self {
41        Pyo3Images(Images::new(docker.0))
42    }
43
44    fn get(&self, name: &str) -> Pyo3Image {
45        Pyo3Image(self.0.get(name))
46    }
47
48    #[pyo3(signature = (all=None, digests=None, filter=None))]
49    fn list(
50        &self,
51        all: Option<bool>,
52        digests: Option<bool>,
53        filter: Option<&Bound<'_, PyDict>>,
54    ) -> PyResult<Py<PyAny>> {
55        let mut opts = ImageListOpts::builder();
56        bo_setter!(all, opts);
57        bo_setter!(digests, opts);
58
59        // Handle filter parameter - expects dict like {"type": "dangling", "value": true}
60        // or {"type": "label", "key": "foo", "value": "bar"}
61        if let Some(filter_dict) = filter {
62            if let Some(filter_type) = filter_dict.get_item("type")? {
63                let filter_type_str: String = filter_type.extract()?;
64
65                let image_filter = match filter_type_str.as_str() {
66                    "dangling" => ImageFilter::Dangling,
67                    "label" => {
68                        if let Some(value) = filter_dict.get_item("value")? {
69                            if let Some(key) = filter_dict.get_item("key")? {
70                                ImageFilter::Label(key.extract()?, value.extract()?)
71                            } else {
72                                ImageFilter::LabelKey(value.extract()?)
73                            }
74                        } else {
75                            return Err(exceptions::PyValueError::new_err(
76                                "label filter requires 'value' (and optionally 'key')",
77                            ));
78                        }
79                    }
80                    "before" => {
81                        if let Some(value) = filter_dict.get_item("value")? {
82                            let image_str: String = value.extract()?;
83                            ImageFilter::Before(ImageName::tag(image_str, None::<String>))
84                        } else {
85                            return Err(exceptions::PyValueError::new_err(
86                                "before filter requires 'value'",
87                            ));
88                        }
89                    }
90                    "since" => {
91                        if let Some(value) = filter_dict.get_item("value")? {
92                            let image_str: String = value.extract()?;
93                            ImageFilter::Since(ImageName::tag(image_str, None::<String>))
94                        } else {
95                            return Err(exceptions::PyValueError::new_err(
96                                "since filter requires 'value'",
97                            ));
98                        }
99                    }
100                    _ => {
101                        return Err(exceptions::PyValueError::new_err(format!(
102                            "unknown filter type: {}",
103                            filter_type_str
104                        )))
105                    }
106                };
107
108                opts = opts.filter([image_filter]);
109            }
110        }
111
112        let rv = __images_list(&self.0, &opts.build());
113
114        match rv {
115            Ok(rv) => Ok(pythonize_this!(rv)),
116            Err(rv) => Err(exceptions::PySystemError::new_err(format!("{rv:?}"))),
117        }
118    }
119
120    fn prune(&self) -> PyResult<Py<PyAny>> {
121        match __images_prune(&self.0) {
122            Ok(info) => Ok(pythonize_this!(info)),
123            Err(e) => Err(exceptions::PySystemError::new_err(format!("{e:?}"))),
124        }
125    }
126
127    #[pyo3(signature = (path, *, dockerfile=None, tag=None, extra_hosts=None, remote=None, quiet=None, nocahe=None, pull=None, rm=None, forcerm=None, memory=None, memswap=None, cpu_shares=None, cpu_set_cpus=None, cpu_period=None, cpu_quota=None, shm_size=None, squash=None, network_mode=None, platform=None, target=None, outputs=None, labels=None))]
128    fn build(
129        &self,
130        path: &str,
131        dockerfile: Option<&str>,
132        tag: Option<&str>,
133        extra_hosts: Option<&str>,
134        remote: Option<&str>,
135        quiet: Option<bool>,
136        nocahe: Option<bool>,
137        pull: Option<&str>,
138        rm: Option<bool>,
139        forcerm: Option<bool>,
140        memory: Option<usize>,
141        memswap: Option<usize>,
142        cpu_shares: Option<usize>,
143        cpu_set_cpus: Option<&str>,
144        cpu_period: Option<usize>,
145        cpu_quota: Option<usize>,
146        shm_size: Option<usize>,
147        squash: Option<bool>,
148        network_mode: Option<&str>,
149        platform: Option<&str>,
150        target: Option<&str>,
151        outputs: Option<&str>,
152        labels: Option<&Bound<'_, PyDict>>,
153    ) -> PyResult<Py<PyAny>> {
154        let mut bo = ImageBuildOpts::builder(path);
155
156        bo_setter!(dockerfile, bo);
157        bo_setter!(tag, bo);
158        bo_setter!(extra_hosts, bo);
159        bo_setter!(remote, bo);
160        bo_setter!(quiet, bo);
161        bo_setter!(nocahe, bo);
162        bo_setter!(pull, bo);
163        bo_setter!(rm, bo);
164        bo_setter!(forcerm, bo);
165        bo_setter!(memory, bo);
166        bo_setter!(memswap, bo);
167        bo_setter!(cpu_shares, bo);
168        bo_setter!(cpu_set_cpus, bo);
169        bo_setter!(cpu_period, bo);
170        bo_setter!(cpu_quota, bo);
171        bo_setter!(shm_size, bo);
172        bo_setter!(squash, bo);
173        bo_setter!(network_mode, bo);
174        bo_setter!(platform, bo);
175        bo_setter!(target, bo);
176        bo_setter!(outputs, bo);
177
178        let labels_map: Option<HashMap<String, String>> = if labels.is_some() {
179            Some(labels.unwrap().extract().unwrap())
180        } else {
181            None
182        };
183        let labels: Option<HashMap<&str, &str>> = labels_map
184            .as_ref()
185            .map(|m| m.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect());
186
187        bo_setter!(labels, bo);
188
189        let rv = __images_build(&self.0, &bo.build());
190
191        match rv {
192            Ok(rv) => Ok(pythonize_this!(rv)),
193            Err(rv) => Err(py_sys_exception!(rv)),
194        }
195    }
196
197    // fn search(&self) -> PyResult<()> {
198    //     Err(exceptions::PyNotImplementedError::new_err(
199    //         "This method is not available yet.",
200    //     ))
201    // }
202
203    #[pyo3(signature = (image=None, src=None, repo=None, tag=None, auth_password=None, auth_token=None))]
204    fn pull(
205        &self,
206        image: Option<&str>,
207        src: Option<&str>,
208        repo: Option<&str>,
209        tag: Option<&str>,
210        auth_password: Option<&Bound<'_, PyDict>>,
211        auth_token: Option<&Bound<'_, PyDict>>,
212    ) -> PyResult<Py<PyAny>> {
213        let mut pull_opts = PullOpts::builder();
214
215        if auth_password.is_some() && auth_token.is_some() {
216            let msg = "Got both auth_password and auth_token for image.push(). Only one of these options is allowed";
217            return Err(py_sys_exception!(msg));
218        }
219
220        let auth = if auth_password.is_some() && auth_token.is_none() {
221            let auth_dict = auth_password.unwrap();
222            let username = auth_dict.get_item("username").unwrap_or(None);
223            let password = auth_dict.get_item("password").unwrap_or(None);
224            let email = auth_dict.get_item("email").unwrap_or(None);
225            let server_address = auth_dict.get_item("server_address").unwrap_or(None);
226
227            let username = username.map(|v| v.extract::<String>().unwrap());
228            let password = password.map(|v| v.extract::<String>().unwrap());
229            let email = email.map(|v| v.extract::<String>().unwrap());
230            let server_address = server_address.map(|v| v.extract::<String>().unwrap());
231
232            let mut ra = RegistryAuth::builder();
233
234            bo_setter!(username, ra);
235            bo_setter!(password, ra);
236            bo_setter!(email, ra);
237            bo_setter!(server_address, ra);
238
239            Some(ra.build())
240        } else if auth_token.is_some() && auth_password.is_none() {
241            let token = RegistryAuth::token(
242                auth_token
243                    .unwrap()
244                    .get_item("identity_token")
245                    .unwrap_or(None)
246                    .expect("identity_token is required")
247                    .extract::<String>()
248                    .unwrap(),
249            );
250            Some(token)
251        } else {
252            Some(RegistryAuth::builder().build())
253        };
254
255        bo_setter!(src, pull_opts);
256        bo_setter!(repo, pull_opts);
257        bo_setter!(tag, pull_opts);
258        bo_setter!(image, pull_opts);
259        bo_setter!(auth, pull_opts);
260
261        let rv = __images_pull(&self.0, &pull_opts.build());
262
263        match rv {
264            Ok(rv) => Ok(pythonize_this!(rv)),
265            Err(rv) => Err(exceptions::PySystemError::new_err(format!("{rv}"))),
266        }
267    }
268
269    // fn export(&self) -> PyResult<()> {
270    //     Err(exceptions::PyNotImplementedError::new_err(
271    //         "This method is not available yet.",
272    //     ))
273    // }
274
275    // fn import(&self) -> PyResult<()> {
276    //     Err(exceptions::PyNotImplementedError::new_err(
277    //         "This method is not available yet.",
278    //     ))
279    // }
280
281    fn push(&self) -> PyResult<()> {
282        Err(exceptions::PyNotImplementedError::new_err(
283            "This method is not available yet.",
284        ))
285    }
286
287    fn clear_cache(&self) -> PyResult<()> {
288        Err(exceptions::PyNotImplementedError::new_err(
289            "This method is not available yet.",
290        ))
291    }
292}
293
294#[tokio::main]
295async fn __images_list(
296    images: &Images,
297    opts: &ImageListOpts,
298) -> Result<Vec<ImageSummary>, docker_api::Error> {
299    images.list(opts).await
300}
301
302#[tokio::main]
303async fn __images_prune(images: &Images) -> Result<ImagePrune200Response, docker_api::Error> {
304    images.prune(&Default::default()).await
305}
306
307#[tokio::main]
308async fn __images_build(
309    images: &Images,
310    opts: &ImageBuildOpts,
311) -> Result<Vec<String>, docker_api::Error> {
312    use futures_util::StreamExt;
313    let mut stream = images.build(opts);
314    let mut ok_stream_vec = Vec::new();
315    let mut err_message = None;
316    while let Some(build_result) = stream.next().await {
317        match build_result {
318            Ok(output) => ok_stream_vec.push(format!("{output:?}")),
319            Err(e) => err_message = Some(e),
320        }
321    }
322
323    match err_message {
324        Some(err_message) => Err(err_message),
325        _ => Ok(ok_stream_vec),
326    }
327}
328
329#[tokio::main]
330async fn __images_pull(
331    images: &Images,
332    pull_opts: &PullOpts,
333) -> Result<Vec<String>, docker_api::Error> {
334    let mut stream = images.pull(pull_opts);
335    let mut ok_stream_vec = Vec::new();
336    let mut err_message = None;
337    while let Some(pull_result) = stream.next().await {
338        match pull_result {
339            Ok(output) => ok_stream_vec.push(format!("{output:?}")),
340            Err(e) => err_message = Some(e),
341        }
342    }
343
344    match err_message {
345        Some(err_message) => Err(err_message),
346        _ => Ok(ok_stream_vec),
347    }
348}
349
350#[pymethods]
351impl Pyo3Image {
352    #[new]
353    fn new(docker: Pyo3Docker, name: &str) -> Pyo3Image {
354        Pyo3Image(Image::new(docker.0, name))
355    }
356
357    fn __repr__(&self) -> String {
358        let inspect = __image_inspect(&self.0).unwrap();
359        format!(
360            "Image(id: {:?}, name: {})",
361            inspect.id.unwrap(),
362            self.0.name()
363        )
364    }
365
366    fn __string__(&self) -> String {
367        self.__repr__()
368    }
369
370    fn name(&self) -> Py<PyAny> {
371        let rv = self.0.name();
372        pythonize_this!(rv)
373    }
374
375    fn inspect(&self) -> PyResult<Py<PyAny>> {
376        let rv = __image_inspect(&self.0);
377        match rv {
378            Ok(rv) => Ok(pythonize_this!(rv)),
379            Err(rv) => Err(py_sys_exception!(rv)),
380        }
381    }
382
383    fn remove(&self) -> PyResult<()> {
384        Err(exceptions::PyNotImplementedError::new_err(
385            "This method is not available yet.",
386        ))
387    }
388
389    fn delete(&self) -> PyResult<String> {
390        let rv = __image_delete(&self.0);
391        match rv {
392            Ok(rv) => {
393                let mut r_value = "".to_owned();
394                for r in rv {
395                    let r_str = format!("{r:?}");
396                    r_value.push_str(&r_str);
397                }
398                Ok(r_value)
399            }
400            Err(rv) => Err(py_sys_exception!(rv)),
401        }
402    }
403
404    fn history(&self) -> PyResult<String> {
405        let rv = __image_history(&self.0);
406
407        match rv {
408            Ok(rv) => {
409                let mut r_value = "".to_owned();
410                for r in rv {
411                    let r_str = format!("{r:?}");
412                    r_value.push_str(&r_str);
413                }
414                Ok(r_value)
415            }
416            Err(rv) => Err(py_sys_exception!(rv)),
417        }
418    }
419
420    fn export(&self, path: Option<&str>) -> PyResult<String> {
421        let path = if path.is_none() {
422            format!("{:?}", &self.0)
423        } else {
424            path.unwrap().to_string()
425        };
426
427        let rv = __image_export(&self.0, path);
428
429        if rv.is_some() {
430            match rv.unwrap() {
431                Ok(n) => Ok(n),
432                Err(e) => Err(py_sys_exception!(e)),
433            }
434        } else {
435            Err(exceptions::PySystemError::new_err("Unknown error occurred in export. (Seriously I don't know how you get here, open a ticket and tell me what happens)"))
436        }
437    }
438
439    #[pyo3(signature = (repo=None, tag=None))]
440    fn tag(&self, repo: Option<&str>, tag: Option<&str>) -> PyResult<()> {
441        let mut opts = TagOpts::builder();
442
443        bo_setter!(repo, opts);
444        bo_setter!(tag, opts);
445
446        let rv = __image_tag(&self.0, &opts.build());
447
448        match rv {
449            Ok(_rv) => Ok(()),
450            Err(rv) => Err(py_sys_exception!(rv)),
451        }
452    }
453
454    fn push(
455        &self,
456        auth_password: Option<&Bound<'_, PyDict>>,
457        auth_token: Option<&Bound<'_, PyDict>>,
458        tag: Option<&str>,
459    ) -> PyResult<()> {
460        if auth_password.is_some() && auth_token.is_some() {
461            let msg = "Got both auth_password and auth_token for image.push(). Only one of these options is allowed";
462            return Err(py_sys_exception!(msg));
463        }
464
465        let auth = if auth_password.is_some() && auth_token.is_none() {
466            let auth_dict = auth_password.unwrap();
467            let username = auth_dict.get_item("username").unwrap_or(None);
468            let password = auth_dict.get_item("password").unwrap_or(None);
469            let email = auth_dict.get_item("email").unwrap_or(None);
470            let server_address = auth_dict.get_item("server_address").unwrap_or(None);
471
472            let username = username.map(|v| v.extract::<String>().unwrap());
473            let password = password.map(|v| v.extract::<String>().unwrap());
474            let email = email.map(|v| v.extract::<String>().unwrap());
475            let server_address = server_address.map(|v| v.extract::<String>().unwrap());
476
477            let mut ra = RegistryAuth::builder();
478
479            bo_setter!(username, ra);
480            bo_setter!(password, ra);
481            bo_setter!(email, ra);
482            bo_setter!(server_address, ra);
483
484            Some(ra.build())
485        } else if auth_token.is_some() && auth_password.is_none() {
486            let token = RegistryAuth::token(
487                auth_token
488                    .unwrap()
489                    .get_item("identity_token")
490                    .unwrap_or(None)
491                    .expect("identity_token is required")
492                    .extract::<String>()
493                    .unwrap(),
494            );
495            Some(token)
496        } else {
497            Some(RegistryAuth::builder().build())
498        };
499
500        let mut opts = ImagePushOpts::builder();
501        bo_setter!(tag, opts);
502        bo_setter!(auth, opts);
503
504        let rv = __image_push(&self.0, &opts.build());
505        match rv {
506            Ok(_rv) => Ok(()),
507            Err(rv) => Err(py_sys_exception!(rv)),
508        }
509    }
510
511    fn distribution_inspect(&self) -> PyResult<()> {
512        Err(exceptions::PyNotImplementedError::new_err(
513            "This method is not available yet.",
514        ))
515    }
516}
517
518#[tokio::main]
519async fn __image_inspect(image: &Image) -> Result<ImageInspect, docker_api::Error> {
520    image.inspect().await
521}
522
523#[tokio::main]
524async fn __image_delete(image: &Image) -> Result<Vec<ImageDeleteResponseItem>, docker_api::Error> {
525    image.delete().await
526}
527
528#[tokio::main]
529async fn __image_history(image: &Image) -> Result<ImageHistory200Response, docker_api::Error> {
530    image.history().await
531}
532
533#[tokio::main]
534async fn __image_export(image: &Image, path: String) -> Option<Result<String, docker_api::Error>> {
535    let mut export_file = OpenOptions::new()
536        .write(true)
537        .create(true)
538        .open(path)
539        .unwrap();
540
541    let rv = image.export().next().await;
542
543    match rv {
544        None => None,
545        Some(_rv) => match _rv {
546            Ok(bytes) => {
547                let w_rv = export_file.write(&bytes).unwrap();
548                Some(Ok(format!("{w_rv:?}")))
549            }
550            Err(_rv) => Some(Err(_rv)),
551        },
552    }
553}
554
555#[tokio::main]
556async fn __image_tag(image: &Image, opts: &TagOpts) -> Result<(), docker_api::Error> {
557    image.tag(opts).await
558}
559
560#[tokio::main]
561async fn __image_push(image: &Image, opts: &ImagePushOpts) -> Result<(), docker_api::Error> {
562    image.push(opts).await
563}