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};
pub struct Image<'docker> {
docker: &'docker Docker,
name: String,
}
impl<'docker> Image<'docker> {
pub fn new<S>(
docker: &'docker Docker,
name: S,
) -> Self
where
S: Into<String>,
{
Image {
docker,
name: name.into(),
}
}
pub async fn inspect(&self) -> Result<ImageDetails> {
self.docker
.get_json(&format!("/images/{}/json", self.name)[..])
.await
}
pub async fn history(&self) -> Result<Vec<History>> {
self.docker
.get_json(&format!("/images/{}/history", self.name)[..])
.await
}
pub async fn delete(&self) -> Result<Vec<Status>> {
self.docker
.delete_json::<Vec<Status>>(&format!("/images/{}", self.name)[..])
.await
}
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()),
)
}
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(())
}
}
pub struct Images<'docker> {
docker: &'docker Docker,
}
impl<'docker> Images<'docker> {
pub fn new(docker: &'docker Docker) -> Self {
Images { docker }
}
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)
}
let mut bytes = Vec::default();
let tar_result = tarball::dir(&mut bytes, opts.path.as_str());
let docker = self.docker;
Box::pin(
async move {
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(),
)
}
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
}
pub fn get<S>(
&self,
name: S,
) -> Image<'docker>
where
S: Into<String>,
{
Image::new(self.docker, name)
}
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
}
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))
}
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())
}
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 {
pub fn token<S>(token: S) -> RegistryAuth
where
S: Into<String>,
{
RegistryAuth::Token {
identity_token: token.into(),
}
}
pub fn builder() -> RegistryAuthBuilder {
RegistryAuthBuilder::default()
}
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 {
pub fn builder() -> TagOptionsBuilder {
TagOptionsBuilder::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(),
)
}
}
}
#[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 {
pub fn builder() -> PullOptionsBuilder {
PullOptionsBuilder::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(),
)
}
}
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 {
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
}
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 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 {
pub fn builder<S>(path: S) -> BuildOptionsBuilder
where
S: Into<String>,
{
BuildOptionsBuilder::new(path)
}
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 {
pub(crate) fn new<S>(path: S) -> Self
where
S: Into<String>,
{
BuildOptionsBuilder {
path: path.into(),
..Default::default()
}
}
pub fn dockerfile<P>(
&mut self,
path: P,
) -> &mut Self
where
P: Into<String>,
{
self.params.insert("dockerfile", path.into());
self
}
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
}
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
}
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
}
pub fn build(&self) -> BuildOptions {
BuildOptions {
path: self.path.clone(),
params: self.params.clone(),
}
}
}
pub enum ImageFilter {
Dangling,
LabelName(String),
Label(String, String),
}
#[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(),
)
}
}
}
#[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)]),
};
}
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<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 on_build: Option<Vec<String>>,
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)]
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]
fn registry_auth_token() {
let options = RegistryAuth::token("abc");
assert_eq!(
base64::encode(r#"{"identitytoken":"abc"}"#),
options.serialize()
);
}
#[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]
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()
);
}
}