docker_pyo3/
image.rs

1use std::collections::HashMap;
2use std::fs::{File, OpenOptions};
3
4use crate::Pyo3Docker;
5use docker_api::models::{
6    BuildPrune200Response, ImageDeleteResponseItem, ImageHistory200Response, ImageInspect,
7    ImagePrune200Response, ImageSearch200Response,
8};
9use docker_api::opts::{
10    ClearCacheOpts, ImageBuildOpts, ImageFilter, ImageListOpts, ImageName, ImagePushOpts, PullOpts,
11    RegistryAuth, 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 serde::{Deserialize, Serialize};
21use std::io::Write;
22
23/// Compatible ImageSummary that handles Docker API v1.44+ which removed VirtualSize.
24/// This struct mirrors docker_api::models::ImageSummary but makes virtual_size optional.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "PascalCase")]
27pub struct ImageSummaryCompat {
28    pub id: String,
29    pub parent_id: String,
30    pub repo_tags: Option<Vec<String>>,
31    pub repo_digests: Option<Vec<String>>,
32    pub created: i64,
33    pub size: i64,
34    pub shared_size: i64,
35    #[serde(default)]
36    pub virtual_size: Option<i64>,
37    pub labels: Option<HashMap<String, String>>,
38    pub containers: i64,
39}
40
41#[pymodule]
42pub fn image(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
43    m.add_class::<Pyo3Images>()?;
44    m.add_class::<Pyo3Image>()?;
45    Ok(())
46}
47
48/// Interface for managing Docker images collection.
49#[derive(Debug)]
50#[pyclass(name = "Images")]
51pub struct Pyo3Images(pub Images);
52
53/// Represents an individual Docker image.
54#[derive(Debug)]
55#[pyclass(name = "Image")]
56pub struct Pyo3Image(pub Image);
57
58#[pymethods]
59impl Pyo3Images {
60    #[new]
61    pub fn new(docker: Pyo3Docker) -> Self {
62        Pyo3Images(Images::new(docker.0))
63    }
64
65    /// Get a specific image by name, ID, or tag.
66    ///
67    /// Args:
68    ///     name: Image name, ID, or tag (e.g., "busybox", "busybox:latest")
69    ///
70    /// Returns:
71    ///     Image: Image instance
72    fn get(&self, name: &str) -> Pyo3Image {
73        Pyo3Image(self.0.get(name))
74    }
75
76    /// List images.
77    ///
78    /// Args:
79    ///     all: Show all images (default hides intermediate images)
80    ///     digests: Show digests
81    ///     filter: Filter images by dict with type and value:
82    ///             - {"type": "dangling"}: dangling images
83    ///             - {"type": "label", "key": "foo", "value": "bar"}: by label
84    ///             - {"type": "before", "value": "image:tag"}: images before specified
85    ///             - {"type": "since", "value": "image:tag"}: images since specified
86    ///
87    /// Returns:
88    ///     list[dict]: List of image information dictionaries
89    #[pyo3(signature = (all=None, digests=None, filter=None))]
90    fn list(
91        &self,
92        all: Option<bool>,
93        digests: Option<bool>,
94        filter: Option<&Bound<'_, PyDict>>,
95    ) -> PyResult<Py<PyAny>> {
96        let mut opts = ImageListOpts::builder();
97        bo_setter!(all, opts);
98        bo_setter!(digests, opts);
99
100        // Handle filter parameter - expects dict like {"type": "dangling", "value": true}
101        // or {"type": "label", "key": "foo", "value": "bar"}
102        if let Some(filter_dict) = filter {
103            if let Some(filter_type) = filter_dict.get_item("type")? {
104                let filter_type_str: String = filter_type.extract()?;
105
106                let image_filter = match filter_type_str.as_str() {
107                    "dangling" => ImageFilter::Dangling,
108                    "label" => {
109                        if let Some(value) = filter_dict.get_item("value")? {
110                            if let Some(key) = filter_dict.get_item("key")? {
111                                ImageFilter::Label(key.extract()?, value.extract()?)
112                            } else {
113                                ImageFilter::LabelKey(value.extract()?)
114                            }
115                        } else {
116                            return Err(exceptions::PyValueError::new_err(
117                                "label filter requires 'value' (and optionally 'key')",
118                            ));
119                        }
120                    }
121                    "before" => {
122                        if let Some(value) = filter_dict.get_item("value")? {
123                            let image_str: String = value.extract()?;
124                            ImageFilter::Before(ImageName::tag(image_str, None::<String>))
125                        } else {
126                            return Err(exceptions::PyValueError::new_err(
127                                "before filter requires 'value'",
128                            ));
129                        }
130                    }
131                    "since" => {
132                        if let Some(value) = filter_dict.get_item("value")? {
133                            let image_str: String = value.extract()?;
134                            ImageFilter::Since(ImageName::tag(image_str, None::<String>))
135                        } else {
136                            return Err(exceptions::PyValueError::new_err(
137                                "since filter requires 'value'",
138                            ));
139                        }
140                    }
141                    _ => {
142                        return Err(exceptions::PyValueError::new_err(format!(
143                            "unknown filter type: {}",
144                            filter_type_str
145                        )))
146                    }
147                };
148
149                opts = opts.filter([image_filter]);
150            }
151        }
152
153        // Use docker CLI to list images, working around docker-api's VirtualSize issue
154        // Docker API v1.44+ removed VirtualSize field, breaking upstream ImageSummary struct
155        let rv = __images_list_via_cli(all.unwrap_or(false));
156
157        match rv {
158            Ok(rv) => Ok(pythonize_this!(rv)),
159            Err(rv) => Err(exceptions::PySystemError::new_err(rv)),
160        }
161    }
162
163    /// Remove unused images.
164    ///
165    /// Returns:
166    ///     dict: Prune results including images deleted and space reclaimed
167    fn prune(&self) -> PyResult<Py<PyAny>> {
168        match __images_prune(&self.0) {
169            Ok(info) => Ok(pythonize_this!(info)),
170            Err(e) => Err(exceptions::PySystemError::new_err(format!("{e:?}"))),
171        }
172    }
173
174    /// Build an image from a Dockerfile.
175    ///
176    /// Args:
177    ///     path: Path to build context directory
178    ///     dockerfile: Path to Dockerfile relative to build context
179    ///     tag: Tag for the built image (e.g., "myimage:latest")
180    ///     extra_hosts: Extra hosts to add to /etc/hosts
181    ///     remote: Remote repository URL
182    ///     quiet: Suppress build output
183    ///     nocahe: Do not use cache when building
184    ///     pull: Attempt to pull newer version of base image
185    ///     rm: Remove intermediate containers after build
186    ///     forcerm: Always remove intermediate containers
187    ///     memory: Memory limit in bytes
188    ///     memswap: Total memory limit (memory + swap)
189    ///     cpu_shares: CPU shares (relative weight)
190    ///     cpu_set_cpus: CPUs to allow execution (e.g., "0-3", "0,1")
191    ///     cpu_period: CPU CFS period in microseconds
192    ///     cpu_quota: CPU CFS quota in microseconds
193    ///     shm_size: Size of /dev/shm in bytes
194    ///     squash: Squash newly built layers into single layer
195    ///     network_mode: Network mode (e.g., "bridge", "host", "none")
196    ///     platform: Target platform (e.g., "linux/amd64")
197    ///     target: Build stage to target
198    ///     outputs: Output configuration
199    ///     labels: Labels as dict (e.g., {"version": "1.0"})
200    ///
201    /// Returns:
202    ///     dict: Build result information
203    #[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))]
204    fn build(
205        &self,
206        path: &str,
207        dockerfile: Option<&str>,
208        tag: Option<&str>,
209        extra_hosts: Option<&str>,
210        remote: Option<&str>,
211        quiet: Option<bool>,
212        nocahe: Option<bool>,
213        pull: Option<&str>,
214        rm: Option<bool>,
215        forcerm: Option<bool>,
216        memory: Option<usize>,
217        memswap: Option<usize>,
218        cpu_shares: Option<usize>,
219        cpu_set_cpus: Option<&str>,
220        cpu_period: Option<usize>,
221        cpu_quota: Option<usize>,
222        shm_size: Option<usize>,
223        squash: Option<bool>,
224        network_mode: Option<&str>,
225        platform: Option<&str>,
226        target: Option<&str>,
227        outputs: Option<&str>,
228        labels: Option<&Bound<'_, PyDict>>,
229    ) -> PyResult<Py<PyAny>> {
230        let mut bo = ImageBuildOpts::builder(path);
231
232        bo_setter!(dockerfile, bo);
233        bo_setter!(tag, bo);
234        bo_setter!(extra_hosts, bo);
235        bo_setter!(remote, bo);
236        bo_setter!(quiet, bo);
237        bo_setter!(nocahe, bo);
238        bo_setter!(pull, bo);
239        bo_setter!(rm, bo);
240        bo_setter!(forcerm, bo);
241        bo_setter!(memory, bo);
242        bo_setter!(memswap, bo);
243        bo_setter!(cpu_shares, bo);
244        bo_setter!(cpu_set_cpus, bo);
245        bo_setter!(cpu_period, bo);
246        bo_setter!(cpu_quota, bo);
247        bo_setter!(shm_size, bo);
248        bo_setter!(squash, bo);
249        bo_setter!(network_mode, bo);
250        bo_setter!(platform, bo);
251        bo_setter!(target, bo);
252        bo_setter!(outputs, bo);
253
254        let labels_map: Option<HashMap<String, String>> = if labels.is_some() {
255            Some(labels.unwrap().extract().unwrap())
256        } else {
257            None
258        };
259        let labels: Option<HashMap<&str, &str>> = labels_map
260            .as_ref()
261            .map(|m| m.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect());
262
263        bo_setter!(labels, bo);
264
265        let rv = __images_build(&self.0, &bo.build());
266
267        match rv {
268            Ok(rv) => Ok(pythonize_this!(rv)),
269            Err(rv) => Err(py_sys_exception!(rv)),
270        }
271    }
272
273    /// Search for images on Docker Hub.
274    ///
275    /// Args:
276    ///     term: Search term (e.g., "nginx", "python")
277    ///
278    /// Returns:
279    ///     dict: Search results including image names, descriptions, and star counts
280    ///
281    /// Raises:
282    ///     SystemError: If search fails
283    fn search(&self, term: &str) -> PyResult<Py<PyAny>> {
284        let rv = __images_search(&self.0, term);
285        match rv {
286            Ok(rv) => Ok(pythonize_this!(rv)),
287            Err(rv) => Err(py_sys_exception!(rv)),
288        }
289    }
290
291    /// Pull an image from a registry.
292    ///
293    /// Args:
294    ///     image: Image name to pull (e.g., "busybox", "ubuntu:latest")
295    ///     src: Source repository
296    ///     repo: Repository to pull from
297    ///     tag: Tag to pull
298    ///     auth_password: Password authentication dict with username, password, email, server_address
299    ///     auth_token: Token authentication dict with identity_token
300    ///
301    /// Returns:
302    ///     dict: Pull result information
303    ///
304    /// Raises:
305    ///     SystemError: If both auth_password and auth_token are provided
306    #[pyo3(signature = (image=None, src=None, repo=None, tag=None, auth_password=None, auth_token=None))]
307    fn pull(
308        &self,
309        image: Option<&str>,
310        src: Option<&str>,
311        repo: Option<&str>,
312        tag: Option<&str>,
313        auth_password: Option<&Bound<'_, PyDict>>,
314        auth_token: Option<&Bound<'_, PyDict>>,
315    ) -> PyResult<Py<PyAny>> {
316        let mut pull_opts = PullOpts::builder();
317
318        if auth_password.is_some() && auth_token.is_some() {
319            let msg = "Got both auth_password and auth_token for image.push(). Only one of these options is allowed";
320            return Err(py_sys_exception!(msg));
321        }
322
323        let auth = if auth_password.is_some() && auth_token.is_none() {
324            let auth_dict = auth_password.unwrap();
325            let username = auth_dict.get_item("username").unwrap_or(None);
326            let password = auth_dict.get_item("password").unwrap_or(None);
327            let email = auth_dict.get_item("email").unwrap_or(None);
328            let server_address = auth_dict.get_item("server_address").unwrap_or(None);
329
330            let username = username.map(|v| v.extract::<String>().unwrap());
331            let password = password.map(|v| v.extract::<String>().unwrap());
332            let email = email.map(|v| v.extract::<String>().unwrap());
333            let server_address = server_address.map(|v| v.extract::<String>().unwrap());
334
335            let mut ra = RegistryAuth::builder();
336
337            bo_setter!(username, ra);
338            bo_setter!(password, ra);
339            bo_setter!(email, ra);
340            bo_setter!(server_address, ra);
341
342            Some(ra.build())
343        } else if auth_token.is_some() && auth_password.is_none() {
344            let token = RegistryAuth::token(
345                auth_token
346                    .unwrap()
347                    .get_item("identity_token")
348                    .unwrap_or(None)
349                    .expect("identity_token is required")
350                    .extract::<String>()
351                    .unwrap(),
352            );
353            Some(token)
354        } else {
355            Some(RegistryAuth::builder().build())
356        };
357
358        bo_setter!(src, pull_opts);
359        bo_setter!(repo, pull_opts);
360        bo_setter!(tag, pull_opts);
361        bo_setter!(image, pull_opts);
362        bo_setter!(auth, pull_opts);
363
364        let rv = __images_pull(&self.0, &pull_opts.build());
365
366        match rv {
367            Ok(rv) => Ok(pythonize_this!(rv)),
368            Err(rv) => Err(exceptions::PySystemError::new_err(format!("{rv}"))),
369        }
370    }
371
372    /// Import images from a tarball file.
373    ///
374    /// This loads images that were previously exported using `docker save` or `image.export()`.
375    ///
376    /// Args:
377    ///     path: Path to the tarball file to import
378    ///
379    /// Returns:
380    ///     list[str]: Import result messages
381    ///
382    /// Raises:
383    ///     SystemError: If import fails
384    fn import_image(&self, path: &str) -> PyResult<Py<PyAny>> {
385        let rv = __images_import(&self.0, path);
386        match rv {
387            Ok(rv) => Ok(pythonize_this!(rv)),
388            Err(rv) => Err(py_sys_exception!(rv)),
389        }
390    }
391
392    /// Clear the build cache.
393    ///
394    /// Args:
395    ///     all: Remove all unused build cache, not just dangling ones
396    ///     keep_storage: Amount of disk space to keep for cache (in bytes)
397    ///
398    /// Returns:
399    ///     dict: Cache prune results including space reclaimed
400    ///
401    /// Raises:
402    ///     SystemError: If cache clear fails
403    #[pyo3(signature = (all=None, keep_storage=None))]
404    fn clear_cache(&self, all: Option<bool>, keep_storage: Option<i64>) -> PyResult<Py<PyAny>> {
405        let mut opts = ClearCacheOpts::builder();
406        bo_setter!(all, opts);
407        bo_setter!(keep_storage, opts);
408
409        let rv = __images_clear_cache(&self.0, &opts.build());
410        match rv {
411            Ok(rv) => Ok(pythonize_this!(rv)),
412            Err(rv) => Err(py_sys_exception!(rv)),
413        }
414    }
415}
416
417/// List images using docker CLI to handle Docker API v1.44+ compatibility.
418/// Docker API v1.44+ removed the VirtualSize field, breaking the upstream ImageSummary struct.
419/// This function calls `docker images` and parses JSON output with optional virtual_size.
420fn __images_list_via_cli(all: bool) -> Result<Vec<ImageSummaryCompat>, String> {
421    use std::process::Command;
422
423    let mut cmd = Command::new("docker");
424    cmd.args(["images", "--format", "json", "--no-trunc"]);
425    if all {
426        cmd.arg("--all");
427    }
428
429    let output = cmd.output().map_err(|e| format!("Failed to execute docker: {}", e))?;
430
431    if !output.status.success() {
432        return Err(format!(
433            "docker images failed: {}",
434            String::from_utf8_lossy(&output.stderr)
435        ));
436    }
437
438    let stdout = String::from_utf8_lossy(&output.stdout);
439
440    // Docker outputs one JSON object per line (NDJSON format)
441    let mut images = Vec::new();
442    for line in stdout.lines() {
443        if line.trim().is_empty() {
444            continue;
445        }
446        // Parse the docker CLI JSON format and convert to our struct
447        let cli_image: DockerCliImage =
448            serde_json::from_str(line).map_err(|e| format!("Failed to parse JSON: {}", e))?;
449        images.push(cli_image.into());
450    }
451
452    Ok(images)
453}
454
455/// Docker CLI image format (from `docker images --format json`)
456#[derive(Debug, Clone, Deserialize)]
457#[serde(rename_all = "PascalCase")]
458struct DockerCliImage {
459    #[serde(rename = "ID")]
460    pub id: String,
461    pub repository: String,
462    pub tag: String,
463    pub digest: String,
464    pub created_since: String,
465    pub created_at: String,
466    pub size: String,
467    #[serde(default)]
468    pub virtual_size: Option<String>,
469    pub shared_size: String,
470    pub unique_size: String,
471    pub containers: String,
472}
473
474impl From<DockerCliImage> for ImageSummaryCompat {
475    fn from(cli: DockerCliImage) -> Self {
476        // Parse size strings to bytes (e.g., "1.23GB" -> bytes)
477        fn parse_size(s: &str) -> i64 {
478            let s = s.trim();
479            if s == "N/A" || s.is_empty() {
480                return 0;
481            }
482            let (num, unit) = s.split_at(s.len().saturating_sub(2));
483            let num: f64 = num.parse().unwrap_or(0.0);
484            match unit.to_uppercase().as_str() {
485                "KB" => (num * 1024.0) as i64,
486                "MB" => (num * 1024.0 * 1024.0) as i64,
487                "GB" => (num * 1024.0 * 1024.0 * 1024.0) as i64,
488                "TB" => (num * 1024.0 * 1024.0 * 1024.0 * 1024.0) as i64,
489                _ => {
490                    // Try parsing as plain bytes
491                    let clean: String = s.chars().filter(|c| c.is_numeric()).collect();
492                    clean.parse().unwrap_or(0)
493                }
494            }
495        }
496
497        let repo_tag = if cli.repository != "<none>" && cli.tag != "<none>" {
498            Some(vec![format!("{}:{}", cli.repository, cli.tag)])
499        } else {
500            None
501        };
502
503        let repo_digest = if cli.digest != "<none>" {
504            Some(vec![format!("{}@{}", cli.repository, cli.digest)])
505        } else {
506            None
507        };
508
509        ImageSummaryCompat {
510            id: cli.id,
511            parent_id: String::new(), // CLI doesn't provide parent ID
512            repo_tags: repo_tag,
513            repo_digests: repo_digest,
514            created: 0, // CLI provides human-readable, not timestamp
515            size: parse_size(&cli.size),
516            shared_size: parse_size(&cli.shared_size),
517            virtual_size: cli.virtual_size.map(|s| parse_size(&s)),
518            labels: None, // CLI doesn't provide labels in default format
519            containers: cli.containers.parse().unwrap_or(0),
520        }
521    }
522}
523
524#[tokio::main]
525async fn __images_prune(images: &Images) -> Result<ImagePrune200Response, docker_api::Error> {
526    images.prune(&Default::default()).await
527}
528
529#[tokio::main]
530async fn __images_build(
531    images: &Images,
532    opts: &ImageBuildOpts,
533) -> Result<Vec<String>, docker_api::Error> {
534    use futures_util::StreamExt;
535    let mut stream = images.build(opts);
536    let mut ok_stream_vec = Vec::new();
537    let mut err_message = None;
538    while let Some(build_result) = stream.next().await {
539        match build_result {
540            Ok(output) => ok_stream_vec.push(format!("{output:?}")),
541            Err(e) => err_message = Some(e),
542        }
543    }
544
545    match err_message {
546        Some(err_message) => Err(err_message),
547        _ => Ok(ok_stream_vec),
548    }
549}
550
551#[tokio::main]
552async fn __images_pull(
553    images: &Images,
554    pull_opts: &PullOpts,
555) -> Result<Vec<String>, docker_api::Error> {
556    let mut stream = images.pull(pull_opts);
557    let mut ok_stream_vec = Vec::new();
558    let mut err_message = None;
559    while let Some(pull_result) = stream.next().await {
560        match pull_result {
561            Ok(output) => ok_stream_vec.push(format!("{output:?}")),
562            Err(e) => err_message = Some(e),
563        }
564    }
565
566    match err_message {
567        Some(err_message) => Err(err_message),
568        _ => Ok(ok_stream_vec),
569    }
570}
571
572#[tokio::main]
573async fn __images_search(
574    images: &Images,
575    term: &str,
576) -> Result<ImageSearch200Response, docker_api::Error> {
577    images.search(term).await
578}
579
580#[tokio::main]
581async fn __images_import(images: &Images, path: &str) -> Result<Vec<String>, docker_api::Error> {
582    let file = File::open(path).map_err(|e| docker_api::Error::Any(Box::new(e)))?;
583
584    let mut stream = images.import(file);
585    let mut ok_stream_vec = Vec::new();
586    let mut err_message = None;
587    while let Some(import_result) = stream.next().await {
588        match import_result {
589            Ok(output) => ok_stream_vec.push(format!("{output:?}")),
590            Err(e) => err_message = Some(e),
591        }
592    }
593
594    match err_message {
595        Some(err_message) => Err(err_message),
596        _ => Ok(ok_stream_vec),
597    }
598}
599
600#[tokio::main]
601async fn __images_clear_cache(
602    images: &Images,
603    opts: &ClearCacheOpts,
604) -> Result<BuildPrune200Response, docker_api::Error> {
605    images.clear_cache(opts).await
606}
607
608#[pymethods]
609impl Pyo3Image {
610    #[new]
611    fn new(docker: Pyo3Docker, name: &str) -> Pyo3Image {
612        Pyo3Image(Image::new(docker.0, name))
613    }
614
615    fn __repr__(&self) -> String {
616        let inspect = __image_inspect(&self.0).unwrap();
617        format!(
618            "Image(id: {:?}, name: {})",
619            inspect.id.unwrap(),
620            self.0.name()
621        )
622    }
623
624    fn __string__(&self) -> String {
625        self.__repr__()
626    }
627
628    /// Get the image name.
629    ///
630    /// Returns:
631    ///     str: Image name
632    fn name(&self) -> Py<PyAny> {
633        let rv = self.0.name();
634        pythonize_this!(rv)
635    }
636
637    /// Inspect the image to get detailed information.
638    ///
639    /// Returns:
640    ///     dict: Detailed image information including config, layers, etc.
641    fn inspect(&self) -> PyResult<Py<PyAny>> {
642        let rv = __image_inspect(&self.0);
643        match rv {
644            Ok(rv) => Ok(pythonize_this!(rv)),
645            Err(rv) => Err(py_sys_exception!(rv)),
646        }
647    }
648
649    /// Remove the image (not implemented yet).
650    fn remove(&self) -> PyResult<()> {
651        Err(exceptions::PyNotImplementedError::new_err(
652            "This method is not available yet.",
653        ))
654    }
655
656    /// Delete the image.
657    ///
658    /// Returns:
659    ///     str: Deletion result information
660    fn delete(&self) -> PyResult<String> {
661        let rv = __image_delete(&self.0);
662        match rv {
663            Ok(rv) => {
664                let mut r_value = "".to_owned();
665                for r in rv {
666                    let r_str = format!("{r:?}");
667                    r_value.push_str(&r_str);
668                }
669                Ok(r_value)
670            }
671            Err(rv) => Err(py_sys_exception!(rv)),
672        }
673    }
674
675    /// Get the image history.
676    ///
677    /// Returns:
678    ///     str: Image history information
679    fn history(&self) -> PyResult<String> {
680        let rv = __image_history(&self.0);
681
682        match rv {
683            Ok(rv) => {
684                let mut r_value = "".to_owned();
685                for r in rv {
686                    let r_str = format!("{r:?}");
687                    r_value.push_str(&r_str);
688                }
689                Ok(r_value)
690            }
691            Err(rv) => Err(py_sys_exception!(rv)),
692        }
693    }
694
695    /// Export the image to a tar file.
696    ///
697    /// Args:
698    ///     path: Path to save the exported tar file
699    ///
700    /// Returns:
701    ///     str: Path to the exported file
702    fn export(&self, path: Option<&str>) -> PyResult<String> {
703        let path = if path.is_none() {
704            format!("{:?}", &self.0)
705        } else {
706            path.unwrap().to_string()
707        };
708
709        let rv = __image_export(&self.0, path);
710
711        if rv.is_some() {
712            match rv.unwrap() {
713                Ok(n) => Ok(n),
714                Err(e) => Err(py_sys_exception!(e)),
715            }
716        } else {
717            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)"))
718        }
719    }
720
721    /// Tag the image with a new name and/or tag.
722    ///
723    /// Args:
724    ///     repo: Repository name (e.g., "myrepo/myimage")
725    ///     tag: Tag name (e.g., "v1.0", "latest")
726    ///
727    /// Returns:
728    ///     None
729    #[pyo3(signature = (repo=None, tag=None))]
730    fn tag(&self, repo: Option<&str>, tag: Option<&str>) -> PyResult<()> {
731        let mut opts = TagOpts::builder();
732
733        bo_setter!(repo, opts);
734        bo_setter!(tag, opts);
735
736        let rv = __image_tag(&self.0, &opts.build());
737
738        match rv {
739            Ok(_rv) => Ok(()),
740            Err(rv) => Err(py_sys_exception!(rv)),
741        }
742    }
743
744    /// Push the image to a registry.
745    ///
746    /// Args:
747    ///     auth_password: Password authentication dict with username, password, email, server_address
748    ///     auth_token: Token authentication dict with identity_token
749    ///     tag: Tag to push
750    ///
751    /// Returns:
752    ///     None
753    ///
754    /// Raises:
755    ///     SystemError: If both auth_password and auth_token are provided
756    fn push(
757        &self,
758        auth_password: Option<&Bound<'_, PyDict>>,
759        auth_token: Option<&Bound<'_, PyDict>>,
760        tag: Option<&str>,
761    ) -> PyResult<()> {
762        if auth_password.is_some() && auth_token.is_some() {
763            let msg = "Got both auth_password and auth_token for image.push(). Only one of these options is allowed";
764            return Err(py_sys_exception!(msg));
765        }
766
767        let auth = if auth_password.is_some() && auth_token.is_none() {
768            let auth_dict = auth_password.unwrap();
769            let username = auth_dict.get_item("username").unwrap_or(None);
770            let password = auth_dict.get_item("password").unwrap_or(None);
771            let email = auth_dict.get_item("email").unwrap_or(None);
772            let server_address = auth_dict.get_item("server_address").unwrap_or(None);
773
774            let username = username.map(|v| v.extract::<String>().unwrap());
775            let password = password.map(|v| v.extract::<String>().unwrap());
776            let email = email.map(|v| v.extract::<String>().unwrap());
777            let server_address = server_address.map(|v| v.extract::<String>().unwrap());
778
779            let mut ra = RegistryAuth::builder();
780
781            bo_setter!(username, ra);
782            bo_setter!(password, ra);
783            bo_setter!(email, ra);
784            bo_setter!(server_address, ra);
785
786            Some(ra.build())
787        } else if auth_token.is_some() && auth_password.is_none() {
788            let token = RegistryAuth::token(
789                auth_token
790                    .unwrap()
791                    .get_item("identity_token")
792                    .unwrap_or(None)
793                    .expect("identity_token is required")
794                    .extract::<String>()
795                    .unwrap(),
796            );
797            Some(token)
798        } else {
799            Some(RegistryAuth::builder().build())
800        };
801
802        let mut opts = ImagePushOpts::builder();
803        bo_setter!(tag, opts);
804        bo_setter!(auth, opts);
805
806        let rv = __image_push(&self.0, &opts.build());
807        match rv {
808            Ok(_rv) => Ok(()),
809            Err(rv) => Err(py_sys_exception!(rv)),
810        }
811    }
812
813    fn distribution_inspect(&self) -> PyResult<()> {
814        Err(exceptions::PyNotImplementedError::new_err(
815            "This method is not available yet.",
816        ))
817    }
818}
819
820#[tokio::main]
821async fn __image_inspect(image: &Image) -> Result<ImageInspect, docker_api::Error> {
822    image.inspect().await
823}
824
825#[tokio::main]
826async fn __image_delete(image: &Image) -> Result<Vec<ImageDeleteResponseItem>, docker_api::Error> {
827    image.delete().await
828}
829
830#[tokio::main]
831async fn __image_history(image: &Image) -> Result<ImageHistory200Response, docker_api::Error> {
832    image.history().await
833}
834
835#[tokio::main]
836async fn __image_export(image: &Image, path: String) -> Option<Result<String, docker_api::Error>> {
837    let mut export_file = OpenOptions::new()
838        .write(true)
839        .create(true)
840        .open(path)
841        .unwrap();
842
843    let rv = image.export().next().await;
844
845    match rv {
846        None => None,
847        Some(_rv) => match _rv {
848            Ok(bytes) => {
849                let w_rv = export_file.write(&bytes).unwrap();
850                Some(Ok(format!("{w_rv:?}")))
851            }
852            Err(_rv) => Some(Err(_rv)),
853        },
854    }
855}
856
857#[tokio::main]
858async fn __image_tag(image: &Image, opts: &TagOpts) -> Result<(), docker_api::Error> {
859    image.tag(opts).await
860}
861
862#[tokio::main]
863async fn __image_push(image: &Image, opts: &ImagePushOpts) -> Result<(), docker_api::Error> {
864    image.push(opts).await
865}