//! Create and manage images. //! //! API Reference: use std::{collections::HashMap, io::Read, iter}; use futures_util::{stream::Stream, TryFutureExt, TryStreamExt}; use hyper::Body; use serde::{Deserialize, Serialize}; use url::form_urlencoded; use crate::{docker::Docker, errors::Result, tarball, transport::tar}; #[cfg(feature = "chrono")] use crate::datetime::datetime_from_unix_timestamp; #[cfg(feature = "chrono")] use chrono::{DateTime, Utc}; /// Interface for accessing and manipulating a named docker image /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#tag/Image) pub struct Image<'docker> { docker: &'docker Docker, name: String, } impl<'docker> Image<'docker> { /// Exports an interface for operations that may be performed against a named image pub fn new( docker: &'docker Docker, name: S, ) -> Self where S: Into, { Image { docker, name: name.into(), } } /// Inspects a named image's details /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageInspect) pub async fn inspect(&self) -> Result { self.docker .get_json(&format!("/images/{}/json", self.name)[..]) .await } /// Lists the history of the images set of changes /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageHistory) pub async fn history(&self) -> Result> { self.docker .get_json(&format!("/images/{}/history", self.name)[..]) .await } /// Deletes an image /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImagePrune) pub async fn delete(&self) -> Result> { self.docker .delete_json::>(&format!("/images/{}", self.name)[..]) .await } /// Export this image to a tarball /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageGet) pub fn export(&self) -> impl Stream>> + Unpin + 'docker { Box::pin( self.docker .stream_get(format!("/images/{}/get", self.name)) .map_ok(|c| c.to_vec()), ) } /// Adds a tag to an image /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageTag) pub async fn tag( &self, opts: &TagOptions, ) -> Result<()> { let mut path = vec![format!("/images/{}/tag", self.name)]; if let Some(query) = opts.serialize() { path.push(query) } let _ = self.docker.post(&path.join("?"), None).await?; Ok(()) } } /// Interface for docker images pub struct Images<'docker> { docker: &'docker Docker, } impl<'docker> Images<'docker> { /// Exports an interface for interacting with docker images pub fn new(docker: &'docker Docker) -> Self { Images { docker } } /// Builds a new image build by reading a Dockerfile in a target directory /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageBuild) pub fn build( &self, opts: &BuildOptions, ) -> impl Stream> + Unpin + 'docker { let mut endpoint = vec!["/build".to_owned()]; if let Some(query) = opts.serialize() { endpoint.push(query) } // To not tie the lifetime of `opts` to the 'stream, we do the tarring work outside of the // stream. But for backwards compatability, we have to return the error inside of the // stream. let mut bytes = Vec::default(); let tar_result = tarball::dir(&mut bytes, opts.path.as_str()); // We must take ownership of the Docker reference. If we don't then the lifetime of 'stream // is incorrectly tied to `self`. let docker = self.docker; Box::pin( async move { // Bubble up error inside the stream for backwards compatability tar_result?; let value_stream = docker.stream_post_into( endpoint.join("?"), Some((Body::from(bytes), tar())), None::>, ); Ok(value_stream) } .try_flatten_stream(), ) } /// Lists the docker images on the current docker host /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageList) pub async fn list( &self, opts: &ImageListOptions, ) -> Result> { let mut path = vec!["/images/json".to_owned()]; if let Some(query) = opts.serialize() { path.push(query); } self.docker .get_json::>(&path.join("?")) .await } /// Returns a reference to a set of operations available for a named image pub fn get( &self, name: S, ) -> Image<'docker> where S: Into, { Image::new(self.docker, name) } /// Search for docker images by term /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageSearch) pub async fn search( &self, term: &str, ) -> Result> { let query = form_urlencoded::Serializer::new(String::new()) .append_pair("term", term) .finish(); self.docker .get_json::>(&format!("/images/search?{}", query)[..]) .await } /// Pull and create a new docker images from an existing image /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImagePull) pub fn pull( &self, opts: &PullOptions, ) -> impl Stream> + Unpin + 'docker { let mut path = vec!["/images/create".to_owned()]; if let Some(query) = opts.serialize() { path.push(query); } let headers = opts .auth_header() .map(|a| iter::once(("X-Registry-Auth", a))); Box::pin(self.docker.stream_post_into(path.join("?"), None, headers)) } /// exports a collection of named images, /// either by name, name:tag, or image id, into a tarball /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageGetAll) pub fn export( &self, names: Vec<&str>, ) -> impl Stream>> + 'docker { let params = names.iter().map(|n| ("names", *n)); let query = form_urlencoded::Serializer::new(String::new()) .extend_pairs(params) .finish(); self.docker .stream_get(format!("/images/get?{}", query)) .map_ok(|c| c.to_vec()) } /// imports an image or set of images from a given tarball source /// source can be uncompressed on compressed via gzip, bzip2 or xz /// /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ImageLoad) pub fn import( self, mut tarball: R, ) -> impl Stream> + Unpin + 'docker where R: Read + Send + 'docker, { Box::pin( async move { let mut bytes = Vec::default(); tarball.read_to_end(&mut bytes)?; let value_stream = self.docker.stream_post_into( "/images/load", Some((Body::from(bytes), tar())), None::>, ); Ok(value_stream) } .try_flatten_stream(), ) } } #[derive(Clone, Serialize, Debug)] #[serde(untagged)] pub enum RegistryAuth { Password { username: String, password: String, #[serde(skip_serializing_if = "Option::is_none")] email: Option, #[serde(rename = "serveraddress")] #[serde(skip_serializing_if = "Option::is_none")] server_address: Option, }, Token { #[serde(rename = "identitytoken")] identity_token: String, }, } impl RegistryAuth { /// return a new instance with token authentication pub fn token(token: S) -> RegistryAuth where S: Into, { RegistryAuth::Token { identity_token: token.into(), } } /// return a new instance of a builder for authentication pub fn builder() -> RegistryAuthBuilder { RegistryAuthBuilder::default() } /// serialize authentication as JSON in base64 pub fn serialize(&self) -> String { serde_json::to_string(self) .map(|c| base64::encode_config(&c, base64::URL_SAFE)) .unwrap() } } #[derive(Default)] pub struct RegistryAuthBuilder { username: Option, password: Option, email: Option, server_address: Option, } impl RegistryAuthBuilder { pub fn username( &mut self, username: I, ) -> &mut Self where I: Into, { self.username = Some(username.into()); self } pub fn password( &mut self, password: I, ) -> &mut Self where I: Into, { self.password = Some(password.into()); self } pub fn email( &mut self, email: I, ) -> &mut Self where I: Into, { self.email = Some(email.into()); self } pub fn server_address( &mut self, server_address: I, ) -> &mut Self where I: Into, { self.server_address = Some(server_address.into()); self } pub fn build(&self) -> RegistryAuth { RegistryAuth::Password { username: self.username.clone().unwrap_or_else(String::new), password: self.password.clone().unwrap_or_else(String::new), email: self.email.clone(), server_address: self.server_address.clone(), } } } #[derive(Default, Debug)] pub struct TagOptions { pub params: HashMap<&'static str, String>, } impl TagOptions { /// return a new instance of a builder for options pub fn builder() -> TagOptionsBuilder { TagOptionsBuilder::default() } /// serialize options as a string. returns None if no options are defined pub fn serialize(&self) -> Option { if self.params.is_empty() { None } else { Some( form_urlencoded::Serializer::new(String::new()) .extend_pairs(&self.params) .finish(), ) } } } #[derive(Default)] pub struct TagOptionsBuilder { params: HashMap<&'static str, String>, } impl TagOptionsBuilder { pub fn repo( &mut self, r: R, ) -> &mut Self where R: Into, { self.params.insert("repo", r.into()); self } pub fn tag( &mut self, t: T, ) -> &mut Self where T: Into, { self.params.insert("tag", t.into()); self } pub fn build(&self) -> TagOptions { TagOptions { params: self.params.clone(), } } } #[derive(Default, Debug)] pub struct PullOptions { auth: Option, params: HashMap<&'static str, String>, } impl PullOptions { /// return a new instance of a builder for options pub fn builder() -> PullOptionsBuilder { PullOptionsBuilder::default() } /// serialize options as a string. returns None if no options are defined pub fn serialize(&self) -> Option { if self.params.is_empty() { None } else { Some( form_urlencoded::Serializer::new(String::new()) .extend_pairs(&self.params) .finish(), ) } } pub(crate) fn auth_header(&self) -> Option { self.auth.clone().map(|a| a.serialize()) } } pub struct PullOptionsBuilder { auth: Option, params: HashMap<&'static str, String>, } impl Default for PullOptionsBuilder { fn default() -> Self { let mut params = HashMap::new(); params.insert("tag", "latest".to_string()); PullOptionsBuilder { auth: None, params } } } impl PullOptionsBuilder { /// Name of the image to pull. The name may include a tag or digest. /// This parameter may only be used when pulling an image. /// If an untagged value is provided and no `tag` is provided, _all_ /// tags will be pulled /// The pull is cancelled if the HTTP connection is closed. pub fn image( &mut self, img: I, ) -> &mut Self where I: Into, { self.params.insert("fromImage", img.into()); self } pub fn src( &mut self, s: S, ) -> &mut Self where S: Into, { self.params.insert("fromSrc", s.into()); self } /// Repository name given to an image when it is imported. The repo may include a tag. /// This parameter may only be used when importing an image. /// /// By default a `latest` tag is added when calling /// [PullOptionsBuilder::default](PullOptionsBuilder::default]. pub fn repo( &mut self, r: R, ) -> &mut Self where R: Into, { self.params.insert("repo", r.into()); self } /// Tag or digest. If empty when pulling an image, /// this causes all tags for the given image to be pulled. pub fn tag( &mut self, t: T, ) -> &mut Self where T: Into, { self.params.insert("tag", t.into()); self } pub fn auth( &mut self, auth: RegistryAuth, ) -> &mut Self { self.auth = Some(auth); self } pub fn build(&mut self) -> PullOptions { PullOptions { auth: self.auth.take(), params: self.params.clone(), } } } #[derive(Default, Debug)] pub struct BuildOptions { pub path: String, params: HashMap<&'static str, String>, } impl BuildOptions { /// return a new instance of a builder for options /// path is expected to be a file path to a directory containing a Dockerfile /// describing how to build a Docker image pub fn builder(path: S) -> BuildOptionsBuilder where S: Into, { BuildOptionsBuilder::new(path) } /// serialize options as a string. returns None if no options are defined pub fn serialize(&self) -> Option { if self.params.is_empty() { None } else { Some( form_urlencoded::Serializer::new(String::new()) .extend_pairs(&self.params) .finish(), ) } } } #[derive(Default)] pub struct BuildOptionsBuilder { path: String, params: HashMap<&'static str, String>, } impl BuildOptionsBuilder { /// path is expected to be a file path to a directory containing a Dockerfile /// describing how to build a Docker image pub(crate) fn new(path: S) -> Self where S: Into, { BuildOptionsBuilder { path: path.into(), ..Default::default() } } /// set the name of the docker file. defaults to "DockerFile" pub fn dockerfile

( &mut self, path: P, ) -> &mut Self where P: Into, { self.params.insert("dockerfile", path.into()); self } /// tag this image with a name after building it pub fn tag( &mut self, t: T, ) -> &mut Self where T: Into, { self.params.insert("t", t.into()); self } pub fn remote( &mut self, r: R, ) -> &mut Self where R: Into, { self.params.insert("remote", r.into()); self } /// don't use the image cache when building image pub fn nocache( &mut self, nc: bool, ) -> &mut Self { self.params.insert("nocache", nc.to_string()); self } pub fn rm( &mut self, r: bool, ) -> &mut Self { self.params.insert("rm", r.to_string()); self } pub fn forcerm( &mut self, fr: bool, ) -> &mut Self { self.params.insert("forcerm", fr.to_string()); self } /// `bridge`, `host`, `none`, `container:`, or a custom network name. pub fn network_mode( &mut self, t: T, ) -> &mut Self where T: Into, { self.params.insert("networkmode", t.into()); self } pub fn memory( &mut self, memory: u64, ) -> &mut Self { self.params.insert("memory", memory.to_string()); self } pub fn cpu_shares( &mut self, cpu_shares: u32, ) -> &mut Self { self.params.insert("cpushares", cpu_shares.to_string()); self } // todo: memswap // todo: cpusetcpus // todo: cpuperiod // todo: cpuquota // todo: buildargs pub fn build(&self) -> BuildOptions { BuildOptions { path: self.path.clone(), params: self.params.clone(), } } } /// Filter options for image listings pub enum ImageFilter { Dangling, LabelName(String), Label(String, String), } /// Options for filtering image list results #[derive(Default, Debug)] pub struct ImageListOptions { params: HashMap<&'static str, String>, } impl ImageListOptions { pub fn builder() -> ImageListOptionsBuilder { ImageListOptionsBuilder::default() } pub fn serialize(&self) -> Option { if self.params.is_empty() { None } else { Some( form_urlencoded::Serializer::new(String::new()) .extend_pairs(&self.params) .finish(), ) } } } /// Builder interface for `ImageListOptions` #[derive(Default)] pub struct ImageListOptionsBuilder { params: HashMap<&'static str, String>, } impl ImageListOptionsBuilder { pub fn digests( &mut self, d: bool, ) -> &mut Self { self.params.insert("digests", d.to_string()); self } pub fn all(&mut self) -> &mut Self { self.params.insert("all", "true".to_owned()); self } pub fn filter_name( &mut self, name: &str, ) -> &mut Self { self.params.insert("filter", name.to_owned()); self } pub fn filter( &mut self, filters: Vec, ) -> &mut Self { let mut param = HashMap::new(); for f in filters { match f { ImageFilter::Dangling => param.insert("dangling", vec![true.to_string()]), ImageFilter::LabelName(n) => param.insert("label", vec![n]), ImageFilter::Label(n, v) => param.insert("label", vec![format!("{}={}", n, v)]), }; } // structure is a a json encoded object mapping string keys to a list // of string values self.params .insert("filters", serde_json::to_string(¶m).unwrap()); self } pub fn build(&self) -> ImageListOptions { ImageListOptions { params: self.params.clone(), } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SearchResult { pub description: String, pub is_official: bool, pub is_automated: bool, pub name: String, pub star_count: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ImageInfo { #[cfg(feature = "chrono")] #[serde(deserialize_with = "datetime_from_unix_timestamp")] pub created: DateTime, #[cfg(not(feature = "chrono"))] pub created: u64, pub id: String, pub parent_id: String, pub labels: Option>, pub repo_tags: Option>, pub repo_digests: Option>, pub virtual_size: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ImageDetails { pub architecture: String, pub author: String, pub comment: String, pub config: ContainerConfig, #[cfg(feature = "chrono")] pub created: DateTime, #[cfg(not(feature = "chrono"))] pub created: String, pub docker_version: String, pub id: String, pub os: String, pub parent: String, pub repo_tags: Option>, pub repo_digests: Option>, pub size: u64, pub virtual_size: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ContainerConfig { pub attach_stderr: bool, pub attach_stdin: bool, pub attach_stdout: bool, pub cmd: Option>, pub domainname: String, pub entrypoint: Option>, pub env: Option>, pub exposed_ports: Option>>, pub hostname: String, pub image: String, pub labels: Option>, // pub MacAddress: String, pub on_build: Option>, // pub NetworkDisabled: bool, pub open_stdin: bool, pub stdin_once: bool, pub tty: bool, pub user: String, pub working_dir: String, } impl ContainerConfig { pub fn env(&self) -> HashMap { let mut map = HashMap::new(); if let Some(ref vars) = self.env { for e in vars { let pair: Vec<&str> = e.split('=').collect(); map.insert(pair[0].to_owned(), pair[1].to_owned()); } } map } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct History { pub id: String, #[cfg(feature = "chrono")] #[serde(deserialize_with = "datetime_from_unix_timestamp")] pub created: DateTime, #[cfg(not(feature = "chrono"))] pub created: u64, pub created_by: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Status { Untagged(String), Deleted(String), } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] /// Represents a response chunk from Docker api when building, pulling or importing an image. pub enum ImageBuildChunk { Update { stream: String, }, Error { error: String, #[serde(rename = "errorDetail")] error_detail: ErrorDetail, }, Digest { aux: Aux, }, PullStatus { status: String, id: Option, progress: Option, #[serde(rename = "progressDetail")] progress_detail: Option, }, } #[derive(Serialize, Deserialize, Debug)] pub struct Aux { #[serde(rename = "ID")] id: String, } #[derive(Serialize, Deserialize, Debug)] pub struct ErrorDetail { message: String, } #[derive(Serialize, Deserialize, Debug)] pub struct ProgressDetail { current: Option, total: Option, } #[cfg(test)] mod tests { use super::*; /// Test registry auth with token #[test] fn registry_auth_token() { let options = RegistryAuth::token("abc"); assert_eq!( base64::encode(r#"{"identitytoken":"abc"}"#), options.serialize() ); } /// Test registry auth with username and password #[test] fn registry_auth_password_simple() { let options = RegistryAuth::builder() .username("user_abc") .password("password_abc") .build(); assert_eq!( base64::encode(r#"{"username":"user_abc","password":"password_abc"}"#), options.serialize() ); } /// Test registry auth with all fields #[test] fn registry_auth_password_all() { let options = RegistryAuth::builder() .username("user_abc") .password("password_abc") .email("email_abc") .server_address("https://example.org") .build(); assert_eq!( base64::encode( r#"{"username":"user_abc","password":"password_abc","email":"email_abc","serveraddress":"https://example.org"}"# ), options.serialize() ); } }