summaryrefslogtreecommitdiffstats
path: root/src/image.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/image.rs')
-rw-r--r--src/image.rs956
1 files changed, 956 insertions, 0 deletions
diff --git a/src/image.rs b/src/image.rs
new file mode 100644
index 0000000..6df8cb7
--- /dev/null
+++ b/src/image.rs
@@ -0,0 +1,956 @@
+//! Create and manage images.
+//!
+//! API Reference: <https://docs.docker.com/engine/api/v1.41/#tag/Image>
+
+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<S>(
+ docker: &'docker Docker,
+ name: S,
+ ) -> Self
+ where
+ S: Into<String>,
+ {
+ 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<ImageDetails> {
+ 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<Vec<History>> {
+ 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<Vec<Status>> {
+ self.docker
+ .delete_json::<Vec<Status>>(&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<Item = Result<Vec<u8>>> + 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<Item = Result<ImageBuildChunk>> + 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::<iter::Empty<_>>,
+ );
+
+ 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<Vec<ImageInfo>> {
+ let mut path = vec!["/images/json".to_owned()];
+ if let Some(query) = opts.serialize() {
+ path.push(query);
+ }
+ self.docker
+ .get_json::<Vec<ImageInfo>>(&path.join("?"))
+ .await
+ }
+
+ /// Returns a reference to a set of operations available for a named image
+ pub fn get<S>(
+ &self,
+ name: S,
+ ) -> Image<'docker>
+ where
+ S: Into<String>,
+ {
+ 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<Vec<SearchResult>> {
+ let query = form_urlencoded::Serializer::new(String::new())
+ .append_pair("term", term)
+ .finish();
+ self.docker
+ .get_json::<Vec<SearchResult>>(&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<Item = Result<ImageBuildChunk>> + 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<Item = Result<Vec<u8>>> + '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<R>(
+ self,
+ mut tarball: R,
+ ) -> impl Stream<Item = Result<ImageBuildChunk>> + 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::<iter::Empty<_>>,
+ );
+ 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<String>,
+
+ #[serde(rename = "serveraddress")]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ server_address: Option<String>,
+ },
+ Token {
+ #[serde(rename = "identitytoken")]
+ identity_token: String,
+ },
+}
+
+impl RegistryAuth {
+ /// return a new instance with token authentication
+ pub fn token<S>(token: S) -> RegistryAuth
+ where
+ S: Into<String>,
+ {
+ 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<String>,
+ password: Option<String>,
+ email: Option<String>,
+ server_address: Option<String>,
+}
+
+impl RegistryAuthBuilder {
+ pub fn username<I>(
+ &mut self,
+ username: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.username = Some(username.into());
+ self
+ }
+
+ pub fn password<I>(
+ &mut self,
+ password: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.password = Some(password.into());
+ self
+ }
+
+ pub fn email<I>(
+ &mut self,
+ email: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.email = Some(email.into());
+ self
+ }
+
+ pub fn server_address<I>(
+ &mut self,
+ server_address: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ 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<String> {
+ 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<R>(
+ &mut self,
+ r: R,
+ ) -> &mut Self
+ where
+ R: Into<String>,
+ {
+ self.params.insert("repo", r.into());
+ self
+ }
+
+ pub fn tag<T>(
+ &mut self,
+ t: T,
+ ) -> &mut Self
+ where
+ T: Into<String>,
+ {
+ 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<RegistryAuth>,
+ 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<String> {
+ 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<String> {
+ self.auth.clone().map(|a| a.serialize())
+ }
+}
+
+pub struct PullOptionsBuilder {
+ auth: Option<RegistryAuth>,
+ 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<I>(
+ &mut self,
+ img: I,
+ ) -> &mut Self
+ where
+ I: Into<String>,
+ {
+ self.params.insert("fromImage", img.into());
+ self
+ }
+
+ pub fn src<S>(
+ &mut self,
+ s: S,
+ ) -> &mut Self
+ where
+ S: Into<String>,
+ {
+ 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<R>(
+ &mut self,
+ r: R,
+ ) -> &mut Self
+ where
+ R: Into<String>,
+ {
+ 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<T>(
+ &mut self,
+ t: T,
+ ) -> &mut Self
+ where
+ T: Into<String>,
+ {
+ 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<S>(path: S) -> BuildOptionsBuilder
+ where
+ S: Into<String>,
+ {
+ BuildOptionsBuilder::new(path)
+ }
+
+ /// serialize options as a string. returns None if no options are defined
+ pub fn serialize(&self) -> Option<String> {
+ 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<S>(path: S) -> Self
+ where
+ S: Into<String>,
+ {
+ BuildOptionsBuilder {
+ path: path.into(),
+ ..Default::default()
+ }
+ }
+
+ /// set the name of the docker file. defaults to "DockerFile"
+ pub fn dockerfile<P>(
+ &mut self,
+ path: P,
+ ) -> &mut Self
+ where
+ P: Into<String>,
+ {
+ self.params.insert("dockerfile", path.into());
+ self
+ }
+
+ /// tag this image with a name after building it
+ pub fn tag<T>(
+ &mut self,
+ t: T,
+ ) -> &mut Self
+ where
+ T: Into<String>,
+ {
+ self.params.insert("t", t.into());
+ self
+ }
+
+ pub fn remote<R>(
+ &mut self,
+ r: R,
+ ) -> &mut Self
+ where
+ R: Into<String>,
+ {
+ 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:<name|id>`, or a custom network name.
+ pub fn network_mode<T>(
+ &mut self,
+ t: T,
+ ) -> &mut Self
+ where
+ T: Into<String>,
+ {
+ 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<String> {
+ 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<ImageFilter>,
+ ) -> &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(&param).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<Utc>,
+ #[cfg(not(feature = "chrono"))]
+ pub created: u64,
+ pub id: String,
+ pub parent_id: String,
+ pub labels: Option<HashMap<String, String>>,
+ pub repo_tags: Option<Vec<String>>,
+ pub repo_digests: Option<Vec<String>>,
+ 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<Utc>,
+ #[cfg(not(feature = "chrono"))]
+ pub created: String,
+ pub docker_version: String,
+ pub id: String,
+ pub os: String,
+ pub parent: String,
+ pub repo_tags: Option<Vec<String>>,
+ pub repo_digests: Option<Vec<String>>,
+ 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<Vec<String>>,
+ pub domainname: String,
+ pub entrypoint: Option<Vec<String>>,
+ pub env: Option<Vec<String>>,
+ pub exposed_ports: Option<HashMap<String, HashMap<String, String>>>,
+ pub hostname: String,
+ pub image: String,
+ pub labels: Option<HashMap<String, String>>,
+ // pub MacAddress: String,
+ pub on_build: Option<Vec<String>>,
+ // 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<String, String> {
+ 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<Utc>,
+ #[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<String>,
+ progress: Option<String>,
+ #[serde(rename = "progressDetail")]
+ progress_detail: Option<ProgressDetail>,
+ },
+}
+
+#[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<u64>,
+ total: Option<u64>,
+}
+
+#[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()
+ );
+ }
+}