1use std::{collections::HashMap, io::Read, iter};
6
7use futures_util::{stream::Stream, TryFutureExt, TryStreamExt};
8use hyper::Body;
9use serde::{Deserialize, Serialize};
10use url::form_urlencoded;
11
12use crate::{docker::Docker, errors::Result, tarball, transport::tar};
13
14#[cfg(feature = "chrono")]
15use crate::datetime::datetime_from_unix_timestamp;
16#[cfg(feature = "chrono")]
17use chrono::{DateTime, Utc};
18
19pub struct Image<'docker> {
23 docker: &'docker Docker,
24 name: String,
25}
26
27impl<'docker> Image<'docker> {
28 pub fn new<S>(
30 docker: &'docker Docker,
31 name: S,
32 ) -> Self
33 where
34 S: Into<String>,
35 {
36 Image {
37 docker,
38 name: name.into(),
39 }
40 }
41
42 pub async fn inspect(&self) -> Result<ImageDetails> {
46 self.docker
47 .get_json(&format!("/images/{}/json", self.name)[..])
48 .await
49 }
50
51 pub async fn history(&self) -> Result<Vec<History>> {
55 self.docker
56 .get_json(&format!("/images/{}/history", self.name)[..])
57 .await
58 }
59
60 pub async fn delete(&self) -> Result<Vec<Status>> {
64 self.docker
65 .delete_json::<Vec<Status>>(&format!("/images/{}", self.name)[..])
66 .await
67 }
68
69 pub fn export(&self) -> impl Stream<Item = Result<Vec<u8>>> + Unpin + 'docker {
73 Box::pin(
74 self.docker
75 .stream_get(format!("/images/{}/get", self.name))
76 .map_ok(|c| c.to_vec()),
77 )
78 }
79
80 pub async fn tag(
84 &self,
85 opts: &TagOptions,
86 ) -> Result<()> {
87 let mut path = vec![format!("/images/{}/tag", self.name)];
88 if let Some(query) = opts.serialize() {
89 path.push(query)
90 }
91 let _ = self.docker.post(&path.join("?"), None).await?;
92 Ok(())
93 }
94}
95
96pub struct Images<'docker> {
98 docker: &'docker Docker,
99}
100
101impl<'docker> Images<'docker> {
102 pub fn new(docker: &'docker Docker) -> Self {
104 Images { docker }
105 }
106
107 pub fn build(
111 &self,
112 opts: &BuildOptions,
113 ) -> impl Stream<Item = Result<ImageBuildChunk>> + Unpin + 'docker {
114 let mut endpoint = vec!["/build".to_owned()];
115 if let Some(query) = opts.serialize() {
116 endpoint.push(query)
117 }
118
119 let mut bytes = Vec::default();
123 let tar_result = tarball::dir(&mut bytes, opts.path.as_str());
124
125 let docker = self.docker;
128 Box::pin(
129 async move {
130 tar_result?;
132
133 let value_stream = docker.stream_post_into(
134 endpoint.join("?"),
135 Some((Body::from(bytes), tar())),
136 None::<iter::Empty<_>>,
137 );
138
139 Ok(value_stream)
140 }
141 .try_flatten_stream(),
142 )
143 }
144
145 pub async fn list(
149 &self,
150 opts: &ImageListOptions,
151 ) -> Result<Vec<ImageInfo>> {
152 let mut path = vec!["/images/json".to_owned()];
153 if let Some(query) = opts.serialize() {
154 path.push(query);
155 }
156 self.docker
157 .get_json::<Vec<ImageInfo>>(&path.join("?"))
158 .await
159 }
160
161 pub fn get<S>(
163 &self,
164 name: S,
165 ) -> Image<'docker>
166 where
167 S: Into<String>,
168 {
169 Image::new(self.docker, name)
170 }
171
172 pub async fn search(
176 &self,
177 term: &str,
178 ) -> Result<Vec<SearchResult>> {
179 let query = form_urlencoded::Serializer::new(String::new())
180 .append_pair("term", term)
181 .finish();
182 self.docker
183 .get_json::<Vec<SearchResult>>(&format!("/images/search?{}", query)[..])
184 .await
185 }
186
187 pub fn pull(
191 &self,
192 opts: &PullOptions,
193 ) -> impl Stream<Item = Result<ImageBuildChunk>> + Unpin + 'docker {
194 let mut path = vec!["/images/{name}/push".to_owned()];
195 if let Some(query) = opts.serialize() {
196 path.push(query);
197 }
198 let headers = opts
199 .auth_header()
200 .map(|a| iter::once(("X-Registry-Auth", a)));
201
202 Box::pin(self.docker.stream_post_into(path.join("?"), None, headers))
203 }
204
205 pub fn push(
209 &self,
210 opts: &PushOptions,
211 name: String,
212 ) -> impl Stream<Item = Result<ImageBuildChunk>> + Unpin + 'docker {
213 let uri = &format!("/images/{}/push", name);
214 let mut path = vec![uri.to_owned()];
215 if let Some(query) = opts.serialize() {
216 path.push(query);
217 }
218 let headers = opts
219 .auth_header()
220 .map(|a| iter::once(("X-Registry-Auth", a)));
221
222 Box::pin(self.docker.stream_post_into(path.join("?"), None, headers))
223 }
224
225 pub fn export(
230 &self,
231 names: Vec<&str>,
232 ) -> impl Stream<Item = Result<Vec<u8>>> + 'docker {
233 let params = names.iter().map(|n| ("names", *n));
234 let query = form_urlencoded::Serializer::new(String::new())
235 .extend_pairs(params)
236 .finish();
237 self.docker
238 .stream_get(format!("/images/get?{}", query))
239 .map_ok(|c| c.to_vec())
240 }
241
242 pub fn import<R>(
247 self,
248 mut tarball: R,
249 ) -> impl Stream<Item = Result<ImageBuildChunk>> + Unpin + 'docker
250 where
251 R: Read + Send + 'docker,
252 {
253 Box::pin(
254 async move {
255 let mut bytes = Vec::default();
256
257 tarball.read_to_end(&mut bytes)?;
258
259 let value_stream = self.docker.stream_post_into(
260 "/images/load",
261 Some((Body::from(bytes), tar())),
262 None::<iter::Empty<_>>,
263 );
264 Ok(value_stream)
265 }
266 .try_flatten_stream(),
267 )
268 }
269}
270
271#[derive(Clone, Serialize, Debug)]
272#[serde(untagged)]
273pub enum RegistryAuth {
274 Password {
275 username: String,
276 password: String,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
279 email: Option<String>,
280
281 #[serde(rename = "serveraddress")]
282 #[serde(skip_serializing_if = "Option::is_none")]
283 server_address: Option<String>,
284 },
285 Token {
286 #[serde(rename = "identitytoken")]
287 identity_token: String,
288 },
289}
290
291impl RegistryAuth {
292 pub fn token<S>(token: S) -> RegistryAuth
294 where
295 S: Into<String>,
296 {
297 RegistryAuth::Token {
298 identity_token: token.into(),
299 }
300 }
301
302 pub fn builder() -> RegistryAuthBuilder {
304 RegistryAuthBuilder::default()
305 }
306
307 pub fn serialize(&self) -> String {
309 serde_json::to_string(self)
310 .map(|c| base64::encode_config(&c, base64::URL_SAFE))
311 .unwrap()
312 }
313}
314
315#[derive(Default)]
316pub struct RegistryAuthBuilder {
317 username: Option<String>,
318 password: Option<String>,
319 email: Option<String>,
320 server_address: Option<String>,
321}
322
323impl RegistryAuthBuilder {
324 pub fn username<I>(
325 &mut self,
326 username: I,
327 ) -> &mut Self
328 where
329 I: Into<String>,
330 {
331 self.username = Some(username.into());
332 self
333 }
334
335 pub fn password<I>(
336 &mut self,
337 password: I,
338 ) -> &mut Self
339 where
340 I: Into<String>,
341 {
342 self.password = Some(password.into());
343 self
344 }
345
346 pub fn email<I>(
347 &mut self,
348 email: I,
349 ) -> &mut Self
350 where
351 I: Into<String>,
352 {
353 self.email = Some(email.into());
354 self
355 }
356
357 pub fn server_address<I>(
358 &mut self,
359 server_address: I,
360 ) -> &mut Self
361 where
362 I: Into<String>,
363 {
364 self.server_address = Some(server_address.into());
365 self
366 }
367
368 pub fn build(&self) -> RegistryAuth {
369 RegistryAuth::Password {
370 username: self.username.clone().unwrap_or_else(String::new),
371 password: self.password.clone().unwrap_or_else(String::new),
372 email: self.email.clone(),
373 server_address: self.server_address.clone(),
374 }
375 }
376}
377
378#[derive(Default, Debug)]
379pub struct TagOptions {
380 pub params: HashMap<&'static str, String>,
381}
382
383impl TagOptions {
384 pub fn builder() -> TagOptionsBuilder {
386 TagOptionsBuilder::default()
387 }
388
389 pub fn serialize(&self) -> Option<String> {
391 if self.params.is_empty() {
392 None
393 } else {
394 Some(
395 form_urlencoded::Serializer::new(String::new())
396 .extend_pairs(&self.params)
397 .finish(),
398 )
399 }
400 }
401}
402
403#[derive(Default)]
404pub struct TagOptionsBuilder {
405 params: HashMap<&'static str, String>,
406}
407
408impl TagOptionsBuilder {
409 pub fn repo<R>(
410 &mut self,
411 r: R,
412 ) -> &mut Self
413 where
414 R: Into<String>,
415 {
416 self.params.insert("repo", r.into());
417 self
418 }
419
420 pub fn tag<T>(
421 &mut self,
422 t: T,
423 ) -> &mut Self
424 where
425 T: Into<String>,
426 {
427 self.params.insert("tag", t.into());
428 self
429 }
430
431 pub fn build(&self) -> TagOptions {
432 TagOptions {
433 params: self.params.clone(),
434 }
435 }
436}
437
438#[derive(Default, Debug)]
439pub struct PullOptions {
440 auth: Option<RegistryAuth>,
441 params: HashMap<&'static str, String>,
442}
443
444impl PullOptions {
445 pub fn builder() -> PullOptionsBuilder {
447 PullOptionsBuilder::default()
448 }
449
450 pub fn serialize(&self) -> Option<String> {
452 if self.params.is_empty() {
453 None
454 } else {
455 Some(
456 form_urlencoded::Serializer::new(String::new())
457 .extend_pairs(&self.params)
458 .finish(),
459 )
460 }
461 }
462
463 pub(crate) fn auth_header(&self) -> Option<String> {
464 self.auth.clone().map(|a| a.serialize())
465 }
466}
467
468pub struct PullOptionsBuilder {
469 auth: Option<RegistryAuth>,
470 params: HashMap<&'static str, String>,
471}
472
473impl Default for PullOptionsBuilder {
474 fn default() -> Self {
475 let mut params = HashMap::new();
476 params.insert("tag", "latest".to_string());
477
478 PullOptionsBuilder { auth: None, params }
479 }
480}
481
482impl PullOptionsBuilder {
483 pub fn image<I>(
489 &mut self,
490 img: I,
491 ) -> &mut Self
492 where
493 I: Into<String>,
494 {
495 self.params.insert("fromImage", img.into());
496 self
497 }
498
499 pub fn src<S>(
500 &mut self,
501 s: S,
502 ) -> &mut Self
503 where
504 S: Into<String>,
505 {
506 self.params.insert("fromSrc", s.into());
507 self
508 }
509
510 pub fn repo<R>(
516 &mut self,
517 r: R,
518 ) -> &mut Self
519 where
520 R: Into<String>,
521 {
522 self.params.insert("repo", r.into());
523 self
524 }
525
526 pub fn tag<T>(
529 &mut self,
530 t: T,
531 ) -> &mut Self
532 where
533 T: Into<String>,
534 {
535 self.params.insert("tag", t.into());
536 self
537 }
538
539 pub fn auth(
540 &mut self,
541 auth: RegistryAuth,
542 ) -> &mut Self {
543 self.auth = Some(auth);
544 self
545 }
546
547 pub fn build(&mut self) -> PullOptions {
548 PullOptions {
549 auth: self.auth.take(),
550 params: self.params.clone(),
551 }
552 }
553}
554
555
556
557#[derive(Default, Debug)]
558pub struct PushOptions {
559 auth: Option<RegistryAuth>,
560 params: HashMap<&'static str, String>,
561}
562
563impl PushOptions {
564 pub fn builder() -> PushOptionsBuilder {
565 PushOptionsBuilder::default()
566 }
567
568 pub fn serialize(&self) -> Option<String> {
569 if self.params.is_empty() {
570 None
571 } else {
572 Some(
573 form_urlencoded::Serializer::new(String::new())
574 .extend_pairs(&self.params)
575 .finish(),
576 )
577 }
578 }
579
580 pub(crate) fn auth_header(&self) -> Option<String> {
581 self.auth.clone().map(|a| a.serialize())
582 }
583}
584
585pub struct PushOptionsBuilder {
586 auth: Option<RegistryAuth>,
587 params: HashMap<&'static str, String>,
588}
589
590impl Default for PushOptionsBuilder {
591 fn default() -> Self {
592 let params = HashMap::new();
593 PushOptionsBuilder { auth: None, params }
594 }
595}
596
597impl PushOptionsBuilder {
598
599 pub fn tag<T>(
600 &mut self,
601 t: T,
602 ) -> &mut Self
603 where
604 T: Into<String>,
605 {
606 self.params.insert("tag", t.into());
607 self
608 }
609
610 pub fn auth(
611 &mut self,
612 auth: RegistryAuth,
613 ) -> &mut Self {
614 self.auth = Some(auth);
615 self
616 }
617
618 pub fn build(&mut self) -> PushOptions {
619 PushOptions {
620 auth: self.auth.take(),
621 params: self.params.clone(),
622 }
623 }
624}
625
626#[derive(Default, Debug)]
627pub struct BuildOptions {
628 pub path: String,
629 params: HashMap<&'static str, String>,
630}
631
632impl BuildOptions {
633 pub fn builder<S>(path: S) -> BuildOptionsBuilder
637 where
638 S: Into<String>,
639 {
640 BuildOptionsBuilder::new(path)
641 }
642
643 pub fn serialize(&self) -> Option<String> {
645 if self.params.is_empty() {
646 None
647 } else {
648 Some(
649 form_urlencoded::Serializer::new(String::new())
650 .extend_pairs(&self.params)
651 .finish(),
652 )
653 }
654 }
655}
656
657#[derive(Default)]
658pub struct BuildOptionsBuilder {
659 path: String,
660 params: HashMap<&'static str, String>,
661}
662
663impl BuildOptionsBuilder {
664 pub(crate) fn new<S>(path: S) -> Self
667 where
668 S: Into<String>,
669 {
670 BuildOptionsBuilder {
671 path: path.into(),
672 ..Default::default()
673 }
674 }
675
676 pub fn dockerfile<P>(
678 &mut self,
679 path: P,
680 ) -> &mut Self
681 where
682 P: Into<String>,
683 {
684 self.params.insert("dockerfile", path.into());
685 self
686 }
687
688 pub fn tag<T>(
690 &mut self,
691 t: T,
692 ) -> &mut Self
693 where
694 T: Into<String>,
695 {
696 self.params.insert("t", t.into());
697 self
698 }
699
700 pub fn remote<R>(
701 &mut self,
702 r: R,
703 ) -> &mut Self
704 where
705 R: Into<String>,
706 {
707 self.params.insert("remote", r.into());
708 self
709 }
710
711 pub fn nocache(
713 &mut self,
714 nc: bool,
715 ) -> &mut Self {
716 self.params.insert("nocache", nc.to_string());
717 self
718 }
719
720 pub fn rm(
721 &mut self,
722 r: bool,
723 ) -> &mut Self {
724 self.params.insert("rm", r.to_string());
725 self
726 }
727
728 pub fn forcerm(
729 &mut self,
730 fr: bool,
731 ) -> &mut Self {
732 self.params.insert("forcerm", fr.to_string());
733 self
734 }
735
736 pub fn network_mode<T>(
738 &mut self,
739 t: T,
740 ) -> &mut Self
741 where
742 T: Into<String>,
743 {
744 self.params.insert("networkmode", t.into());
745 self
746 }
747
748 pub fn memory(
749 &mut self,
750 memory: u64,
751 ) -> &mut Self {
752 self.params.insert("memory", memory.to_string());
753 self
754 }
755
756 pub fn cpu_shares(
757 &mut self,
758 cpu_shares: u32,
759 ) -> &mut Self {
760 self.params.insert("cpushares", cpu_shares.to_string());
761 self
762 }
763
764 pub fn platform<T>(
771 &mut self,
772 t: T,
773 ) -> &mut Self
774 where
775 T: Into<String>,
776 {
777 self.params.insert("platform", t.into());
778 self
779 }
780
781
782 pub fn build(&self) -> BuildOptions {
783 BuildOptions {
784 path: self.path.clone(),
785 params: self.params.clone(),
786 }
787 }
788}
789
790pub enum ImageFilter {
792 Dangling,
793 LabelName(String),
794 Label(String, String),
795}
796
797#[derive(Default, Debug)]
799pub struct ImageListOptions {
800 params: HashMap<&'static str, String>,
801}
802
803impl ImageListOptions {
804 pub fn builder() -> ImageListOptionsBuilder {
805 ImageListOptionsBuilder::default()
806 }
807 pub fn serialize(&self) -> Option<String> {
808 if self.params.is_empty() {
809 None
810 } else {
811 Some(
812 form_urlencoded::Serializer::new(String::new())
813 .extend_pairs(&self.params)
814 .finish(),
815 )
816 }
817 }
818}
819
820#[derive(Default)]
822pub struct ImageListOptionsBuilder {
823 params: HashMap<&'static str, String>,
824}
825
826impl ImageListOptionsBuilder {
827 pub fn digests(
828 &mut self,
829 d: bool,
830 ) -> &mut Self {
831 self.params.insert("digests", d.to_string());
832 self
833 }
834
835 pub fn all(&mut self) -> &mut Self {
836 self.params.insert("all", "true".to_owned());
837 self
838 }
839
840 pub fn filter_name(
841 &mut self,
842 name: &str,
843 ) -> &mut Self {
844 self.params.insert("filter", name.to_owned());
845 self
846 }
847
848 pub fn filter(
849 &mut self,
850 filters: Vec<ImageFilter>,
851 ) -> &mut Self {
852 let mut param = HashMap::new();
853 for f in filters {
854 match f {
855 ImageFilter::Dangling => param.insert("dangling", vec![true.to_string()]),
856 ImageFilter::LabelName(n) => param.insert("label", vec![n]),
857 ImageFilter::Label(n, v) => param.insert("label", vec![format!("{}={}", n, v)]),
858 };
859 }
860 self.params
863 .insert("filters", serde_json::to_string(¶m).unwrap());
864 self
865 }
866
867 pub fn build(&self) -> ImageListOptions {
868 ImageListOptions {
869 params: self.params.clone(),
870 }
871 }
872}
873
874#[derive(Clone, Debug, Serialize, Deserialize)]
875pub struct SearchResult {
876 pub description: String,
877 pub is_official: bool,
878 pub is_automated: bool,
879 pub name: String,
880 pub star_count: u64,
881}
882
883#[derive(Clone, Debug, Serialize, Deserialize)]
884#[serde(rename_all = "PascalCase")]
885pub struct ImageInfo {
886 #[cfg(feature = "chrono")]
887 #[serde(deserialize_with = "datetime_from_unix_timestamp")]
888 pub created: DateTime<Utc>,
889 #[cfg(not(feature = "chrono"))]
890 pub created: u64,
891 pub id: String,
892 pub parent_id: String,
893 pub labels: Option<HashMap<String, String>>,
894 pub repo_tags: Option<Vec<String>>,
895 pub repo_digests: Option<Vec<String>>,
896 pub virtual_size: u64,
897}
898
899#[derive(Clone, Debug, Serialize, Deserialize)]
900#[serde(rename_all = "PascalCase")]
901pub struct ImageDetails {
902 pub architecture: String,
903 pub author: String,
904 pub comment: String,
905 pub config: ContainerConfig,
906 #[cfg(feature = "chrono")]
907 pub created: DateTime<Utc>,
908 #[cfg(not(feature = "chrono"))]
909 pub created: String,
910 pub docker_version: String,
911 pub id: String,
912 pub os: String,
913 pub parent: String,
914 pub repo_tags: Option<Vec<String>>,
915 pub repo_digests: Option<Vec<String>>,
916 pub size: u64,
917 pub virtual_size: u64,
918}
919
920#[derive(Clone, Debug, Serialize, Deserialize)]
921#[serde(rename_all = "PascalCase")]
922pub struct ContainerConfig {
923 pub attach_stderr: bool,
924 pub attach_stdin: bool,
925 pub attach_stdout: bool,
926 pub cmd: Option<Vec<String>>,
927 pub domainname: String,
928 pub entrypoint: Option<Vec<String>>,
929 pub env: Option<Vec<String>>,
930 pub exposed_ports: Option<HashMap<String, HashMap<String, String>>>,
931 pub hostname: String,
932 pub image: String,
933 pub labels: Option<HashMap<String, String>>,
934 pub on_build: Option<Vec<String>>,
936 pub open_stdin: bool,
938 pub stdin_once: bool,
939 pub tty: bool,
940 pub user: String,
941 pub working_dir: String,
942}
943
944impl ContainerConfig {
945 pub fn env(&self) -> HashMap<String, String> {
946 let mut map = HashMap::new();
947 if let Some(ref vars) = self.env {
948 for e in vars {
949 let pair: Vec<&str> = e.split('=').collect();
950 map.insert(pair[0].to_owned(), pair[1].to_owned());
951 }
952 }
953 map
954 }
955}
956
957#[derive(Clone, Debug, Serialize, Deserialize)]
958#[serde(rename_all = "PascalCase")]
959pub struct History {
960 pub id: String,
961 #[cfg(feature = "chrono")]
962 #[serde(deserialize_with = "datetime_from_unix_timestamp")]
963 pub created: DateTime<Utc>,
964 #[cfg(not(feature = "chrono"))]
965 pub created: u64,
966 pub created_by: String,
967}
968
969#[derive(Clone, Debug, Serialize, Deserialize)]
970pub enum Status {
971 Untagged(String),
972 Deleted(String),
973}
974
975#[derive(Serialize, Deserialize, Debug)]
976#[serde(untagged)]
977pub enum ImageBuildChunk {
979 Update {
980 stream: String,
981 },
982 Error {
983 error: String,
984 #[serde(rename = "errorDetail")]
985 error_detail: ErrorDetail,
986 },
987 Digest {
988 aux: Aux,
989 },
990 PullStatus {
991 status: String,
992 id: Option<String>,
993 progress: Option<String>,
994 #[serde(rename = "progressDetail")]
995 progress_detail: Option<ProgressDetail>,
996 },
997 PushedResponse {
998 status: String,
999 digest: String,
1000 size: usize,
1001 },
1002 PushedStatus {
1003 status: String,
1004 id: Option<String>,
1005 },
1006 Null {
1007
1008 }
1009
1010}
1011
1012
1013
1014#[derive(Serialize, Deserialize, Debug)]
1015pub struct Aux {
1016 #[serde(rename = "ID")]
1017 id: String,
1018}
1019
1020#[derive(Serialize, Deserialize, Debug)]
1021pub struct ErrorDetail {
1022 message: String,
1023}
1024
1025#[derive(Serialize, Deserialize, Debug)]
1026pub struct ProgressDetail {
1027 current: Option<u64>,
1028 total: Option<u64>,
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1037 fn registry_auth_token() {
1038 let options = RegistryAuth::token("abc");
1039 assert_eq!(
1040 base64::encode(r#"{"identitytoken":"abc"}"#),
1041 options.serialize()
1042 );
1043 }
1044
1045 #[test]
1047 fn registry_auth_password_simple() {
1048 let options = RegistryAuth::builder()
1049 .username("user_abc")
1050 .password("password_abc")
1051 .build();
1052 assert_eq!(
1053 base64::encode(r#"{"username":"user_abc","password":"password_abc"}"#),
1054 options.serialize()
1055 );
1056 }
1057
1058 #[test]
1060 fn registry_auth_password_all() {
1061 let options = RegistryAuth::builder()
1062 .username("user_abc")
1063 .password("password_abc")
1064 .email("email_abc")
1065 .server_address("https://example.org")
1066 .build();
1067 assert_eq!(
1068 base64::encode(
1069 r#"{"username":"user_abc","password":"password_abc","email":"email_abc","serveraddress":"https://example.org"}"#
1070 ),
1071 options.serialize()
1072 );
1073 }
1074}