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#[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#[derive(Debug)]
50#[pyclass(name = "Images")]
51pub struct Pyo3Images(pub Images);
52
53#[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 fn get(&self, name: &str) -> Pyo3Image {
73 Pyo3Image(self.0.get(name))
74 }
75
76 #[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 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 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 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 #[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 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 #[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 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 #[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
417fn __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 let mut images = Vec::new();
442 for line in stdout.lines() {
443 if line.trim().is_empty() {
444 continue;
445 }
446 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#[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 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 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(), repo_tags: repo_tag,
513 repo_digests: repo_digest,
514 created: 0, 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, 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 fn name(&self) -> Py<PyAny> {
633 let rv = self.0.name();
634 pythonize_this!(rv)
635 }
636
637 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 fn remove(&self) -> PyResult<()> {
651 Err(exceptions::PyNotImplementedError::new_err(
652 "This method is not available yet.",
653 ))
654 }
655
656 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 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 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 #[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 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}