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 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 #[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 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}