diff options
33 files changed, 5418 insertions, 3713 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80c2290..e84d448 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,24 @@ name: Main -on: push +on: + workflow_dispatch: + push: + paths-ignore: + - "*.md" + branches: + - main + - master + tags: + - "**" + pull_request: + paths-ignore: + - "*.md" + branches: + - main + - master + +env: + CARGO_TERM_COLOR: always jobs: codestyle: @@ -40,26 +58,26 @@ jobs: runs-on: ubuntu-latest steps: - - name: Setup Rust - uses: hecrj/setup-rust-action@v1 - with: - rust-version: ${{ matrix.rust }} - - name: Checkout - uses: actions/checkout@v1 - - name: Test - run: cargo test - - name: Coverage - if: matrix.rust == 'stable' - run: | - # tarpaulin knows how to extract data from ci - # ci services and GitHub actions is not one of them - # work around that by masquerading as travis - # https://github.com/xd009642/coveralls-api/blob/6da4ccd7c6eaf1df04cfd1e560362de70fa80605/src/lib.rs#L247-L262 - export TRAVIS_JOB_ID=${GITHUB_SHA} - export TRAVIS_PULL_REQUEST=false - export TRAVIS_BRANCH=${GITHUB_REF##*/} - cargo install cargo-tarpaulin - cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID + - name: Setup Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust }} + - name: Checkout + uses: actions/checkout@v1 + - name: Test + run: cargo test + - name: Coverage + if: matrix.rust == 'stable' + run: | + # tarpaulin knows how to extract data from ci + # ci services and GitHub actions is not one of them + # work around that by masquerading as travis + # https://github.com/xd009642/coveralls-api/blob/6da4ccd7c6eaf1df04cfd1e560362de70fa80605/src/lib.rs#L247-L262 + export TRAVIS_JOB_ID=${GITHUB_SHA} + export TRAVIS_PULL_REQUEST=false + export TRAVIS_BRANCH=${GITHUB_REF##*/} + cargo install cargo-tarpaulin + cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID publish-docs: if: github.ref == 'refs/heads/master' @@ -75,13 +93,10 @@ jobs: cargo doc --no-deps echo "<meta http-equiv=refresh content=0;url=`echo ${{ github.repository }} | cut -d / -f 2 | tr '-' '_'`/index.html>" > target/doc/index.html - name: Publish - uses: docker://peaceiris/gh-pages:v2.3.1 - env: - PUBLISH_BRANCH: gh-pages - PUBLISH_DIR: ./target/doc - PERSONAL_TOKEN: ${{ secrets.GH_PAGES_TOKEN }} + uses: peaceiris/actions-gh-pages@v3 with: - emptyCommits: true + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc publish-crate: if: startsWith(github.ref, 'refs/tags/') @@ -93,4 +108,4 @@ jobs: - uses: actions/checkout@v1 - name: Publish shell: bash - run: cargo publish --token ${{ secrets.CRATES_TOKEN }}
\ No newline at end of file + run: cargo publish --token ${{ secrets.CRATES_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e72e8c..2898200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ +# 0.8.0 + +* `ContainerOptionsBuilder::entrypoint` now correctly takes an `IntoIterator<Item = AsRef<str>>` instead of `&str` [#269](https://github.com/softprops/shiplift/pull/269) +* make `config` field of `ImageDetails` optional [#264](https://github.com/softprops/shiplift/pull/264) +* add `container`, `container_config`, `os_version`, `graph_driver`, `root_fs`, `metadata` fields to `ImageDetails` [#264](https://github.com/softprops/shiplift/pull/264) +* rename `shiplift::rep::Config` to `shiplift::rep::ContainerConfig` [#264](https://github.com/softprops/shiplift/pull/264) +* add missing fields ([API version 1.41](https://docs.docker.com/engine/api/v1.41/#operation/ImageInspect)) to `ContainerConfig` [#264](https://github.com/softprops/shiplift/pull/264) +* add missing fields ([API version 1.41](https://docs.docker.com/engine/api/v1.41/#operation/ImageHistory)) to `History` [#264](https://github.com/softprops/shiplift/pull/264) +* add missing fields to `NetworkEntry` [#254](https://github.com/softprops/shiplift/pull/254) +* PullOptionsBuilder now adds a `latest` tag by default [#261](https://github.com/softprops/shiplift/pull/261) +* `Image::pull`, `Image::build` and `Image::import` now return a stream of `ImageBuildChunk` instead of `json::Value`[#262](https://github.com/softprops/shiplift/262) +* rename `Config` to `ContainerConfig` [#266](https://github.com/softprops/shiplift/pull/266) +* `HostConfig.port_bindings` inner elements now have a clear type `PortBinding` instead of `HashMap<String, String>` [#266](https://github.com/softprops/shiplift/pull/266) +* `ContainerDetails` contains new fields [#266](https://github.com/softprops/shiplift/pull/266) +* Units of `ContainerInfo` `size_rw` and `size_root_fs` units changed to match API [#266](https://github.com/softprops/shiplift/pull/266) + # 0.7.0 * async-await support [#229](https://github.com/softprops/shiplift/pull/229) +* add multiple fields to `shiplift::rep::Version` [#212](https://github.com/softprops/shiplift/pull/212) +* add `image_id` and `state` fields to `shiplift::rep::Container` [#213](https://github.com/softprops/shiplift/pull/213) +* add `ContainerOptionsBuilder::publish_all_ports()` [#215](https://github.com/softprops/shiplift/pull/215) +* re-export `hyper::Uri` as `shiplift::Uri` [#209](https://github.com/softprops/shiplift/pull/209) +* `shiplift::builder::ImageListOptionsBuilder::all()` no longer accepts an argument and always sets the option to true [#211](https://github.com/softprops/shiplift/pull/211) +* add `repo_tags`, `repo_digests` fields to `ImageDetails` [#222](https://github.com/softprops/shiplift/pull/222) +* add `status` field to container State [#221](https://github.com/softprops/shiplift/pull/221) +* support for specifying user when creating container [#220](https://github.com/softprops/shiplift/pull/220) +* add `nano_cpus` and `memory_swap` to `ContainerOptions` [#230](https://github.com/softprops/shiplift/pull/230) +* `ContainerOptionsBuilder::env()` signature changed [#237](https://github.com/softprops/shiplift/pull/237) +* allow attaching to containers when connecting to Docker daemon via UNIX socket [#238](https://github.com/softprops/shiplift/pull/238) +* support for uploading tar to container [#239](https://github.com/softprops/shiplift/pull/239) +* fix registry authentication to use URL-safe base64 encoding [#245](https://github.com/softprops/shiplift/pull/245) +* add StopSignal and StopTimeout to ContainerOptionsBuilder [#248](https://github.com/softprops/shiplift/pull/248) +* update lifetimes of various methods to avoid `temporary value dropped while borrowed` errors [#272](https://github.com/softprops/shiplift/pull/272) # 0.6.0 @@ -1,7 +1,7 @@ [package] name = "shiplift" -version = "0.6.0" +version = "0.7.0" authors = ["softprops <d.tangren@gmail.com>"] description = "A Rust interface for maneuvering Docker containers" documentation = "https://docs.rs/shiplift" @@ -18,31 +18,35 @@ coveralls = { repository = "softprops/shipflit" } maintenance = { status = "actively-developed" } [dependencies] -base64 = "0.11" -byteorder = "1.3" -bytes = "0.4" +base64 = "0.13" +byteorder = "1.4" +bytes = "1.0" chrono = { version = "0.4", optional = true, features = ["serde"] } flate2 = "1.0" futures-util = "0.3" -futures_codec = "0.3" -hyper = "0.13" -hyper-openssl = { version = "0.8", optional = true } -hyperlocal = { version = "0.7", optional = true } +futures_codec = "0.4" +hyper = { version = "0.14", features = ["client", "http1", "tcp", "stream"] } +hyper-openssl = { version = "0.9", optional = true } +hyperlocal = { version = "0.8", optional = true } log = "0.4" mime = "0.3" openssl = { version = "0.10", optional = true } -pin-project = "0.4" +pin-project = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tar = "0.4" -tokio = "0.2" +tokio = "1.0" url = "2.1" +# XXX: This is a temporary dependency for the reexport! macro in lib.rs. Remove +# me before 0.9.0 is released. +paste = "1.0" + [dev-dependencies] -env_logger = "0.7" +env_logger = "0.8" # Required for examples to run futures = "0.3.1" -tokio = { version = "0.2.6", features = ["macros"] } +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } [features] default = ["chrono", "unix-socket", "tls"] @@ -10,15 +10,11 @@ Add the following to your `Cargo.toml` file ```toml [dependencies] -shiplift = "0.6" +shiplift = "0.7" ``` ## usage Many small runnable example programs can be found in this repository's [examples directory](https://github.com/softprops/shiplift/tree/master/examples). -## planned changes - -* give image pull chunked json a proper type - Doug Tangren (softprops) 2015-2018 diff --git a/examples/logs.rs b/examples/containerlogs.rs index 73c8045..73c8045 100644 --- a/examples/logs.rs +++ b/examples/containerlogs.rs diff --git a/examples/execinspect.rs b/examples/execinspect.rs new file mode 100644 index 0000000..de92ec7 --- /dev/null +++ b/examples/execinspect.rs @@ -0,0 +1,32 @@ +use futures::StreamExt; +use shiplift::{Docker, Exec, ExecContainerOptions}; +use std::env; + +#[tokio::main] +async fn main() { + let docker = Docker::new(); + let mut args = env::args().skip(1); + + // First argument is container id + let id = args.next().expect("You need to specify a container id"); + // Rest is command to run in the container + let cmd = args.collect::<Vec<String>>(); + println!("{} {:?}", id, cmd); + + // Create options with specified command + let opts = ExecContainerOptions::builder() + .cmd(cmd.iter().map(String::as_str).collect()) + .attach_stdout(true) + .attach_stderr(true) + .build(); + + let exec = Exec::create(&docker, &id, &opts).await.unwrap(); + + println!("{:#?}", exec.inspect().await.unwrap()); + + let mut stream = exec.start(); + + stream.next().await; + + println!("{:#?}", exec.inspect().await.unwrap()); +} diff --git a/examples/execresize.rs b/examples/execresize.rs new file mode 100644 index 0000000..7c9cf11 --- /dev/null +++ b/examples/execresize.rs @@ -0,0 +1,30 @@ +use shiplift::{Docker, Exec, ExecContainerOptions, ExecResizeOptions}; +use std::env; + +#[tokio::main] +async fn main() { + let docker = Docker::new(); + let mut args = env::args().skip(1); + + // First argument is container id + let id = args.next().expect("You need to specify a container id"); + // Second is width + let width: u64 = args.next().map_or(Ok(0), |s| s.parse::<u64>()).unwrap(); + // Third is height + let height: u64 = args.next().map_or(Ok(0), |s| s.parse::<u64>()).unwrap(); + + // Create an exec instance + let exec_opts = ExecContainerOptions::builder() + .cmd(vec!["echo", "123"]) + .attach_stdout(true) + .attach_stderr(true) + .build(); + let exec = Exec::create(&docker, &id, &exec_opts).await.unwrap(); + + // Resize its window with given parameters + let resize_opts = ExecResizeOptions::builder() + .width(width) + .height(height) + .build(); + exec.resize(&resize_opts).await.unwrap(); +} diff --git a/examples/export.rs b/examples/export.rs index 34f460d..22c543e 100644 --- a/examples/export.rs +++ b/examples/export.rs @@ -14,9 +14,7 @@ async fn main() { .open(format!("{}.tar", &id)) .unwrap(); - let images = docker.images(); - - while let Some(export_result) = images.get(&id).export().next().await { + while let Some(export_result) = docker.images().get(&id).export().next().await { match export_result.and_then(|bytes| export_file.write(&bytes).map_err(Error::from)) { Ok(n) => println!("copied {} bytes", n), Err(e) => eprintln!("Error: {}", e), diff --git a/examples/imagebuild.rs b/examples/imagebuild.rs index 80d825c..01647d4 100644 --- a/examples/imagebuild.rs +++ b/examples/imagebuild.rs @@ -9,10 +9,7 @@ async fn main() { let options = BuildOptions::builder(path).tag("shiplift_test").build(); - let images = docker.images(); - - let mut stream = images.build(&options); - + let mut stream = docker.images().build(&options); while let Some(build_result) = stream.next().await { match build_result { Ok(output) => println!("{:?}", output), diff --git a/examples/networkdisconnect.rs b/examples/networkdisconnect.rs index 8d58b35..78f33c6 100644 --- a/examples/networkdisconnect.rs +++ b/examples/networkdisconnect.rs @@ -6,9 +6,8 @@ async fn network_disconnect( network_id: &str, ) { let docker = Docker::new(); - let networks = docker.networks(); - - if let Err(e) = networks + if let Err(e) = docker + .networks() .get(network_id) .disconnect(&ContainerConnectionOptions::builder(container_id).build()) .await diff --git a/examples/servicedelete.rs b/examples/servicedelete.rs new file mode 100644 index 0000000..e962276 --- /dev/null +++ b/examples/servicedelete.rs @@ -0,0 +1,14 @@ +use shiplift::Docker; +use std::env; + +#[tokio::main] +async fn main() { + let docker = Docker::new(); + let id = env::args() + .nth(1) + .expect("You need to specify an service name"); + + if let Err(e) = docker.services().get(&id).delete().await { + eprintln!("Error: {}", e) + } +} diff --git a/examples/serviceinspect.rs b/examples/serviceinspect.rs new file mode 100644 index 0000000..c281632 --- /dev/null +++ b/examples/serviceinspect.rs @@ -0,0 +1,15 @@ +use shiplift::Docker; +use std::env; + +#[tokio::main] +async fn main() { + let docker = Docker::new(); + let id = env::args() + .nth(1) + .expect("Usage: cargo run --example serviceinspect -- <service>"); + + match docker.services().get(&id).inspect().await { + Ok(service) => println!("{:#?}", service), + Err(e) => eprintln!("Error: {}", e), + } +} diff --git a/examples/servicelogs.rs b/examples/servicelogs.rs new file mode 100644 index 0000000..e5f97c0 --- /dev/null +++ b/examples/servicelogs.rs @@ -0,0 +1,31 @@ +use futures::StreamExt; +use shiplift::{tty::TtyChunk, Docker, LogsOptions}; +use std::env; + +#[tokio::main] +async fn main() { + let docker = Docker::new(); + let id = env::args() + .nth(1) + .expect("You need to specify a service name"); + + let mut logs_stream = docker + .services() + .get(&id) + .logs(&LogsOptions::builder().stdout(true).stderr(true).build()); + + while let Some(log_result) = logs_stream.next().await { + match log_result { + Ok(chunk) => print_chunk(chunk), + Err(e) => eprintln!("Error: {}", e), + } + } +} + +fn print_chunk(chunk: TtyChunk) { + match chunk { + TtyChunk::StdOut(bytes) => println!("Stdout: {}", std::str::from_utf8(&bytes).unwrap()), + TtyChunk::StdErr(bytes) => eprintln!("Stdout: {}", std::str::from_utf8(&bytes).unwrap()), + TtyChunk::StdIn(_) => unreachable!(), + } +} diff --git a/examples/services.rs b/examples/services.rs new file mode 100644 index 0000000..713c5f8 --- /dev/null +++ b/examples/services.rs @@ -0,0 +1,19 @@ +use shiplift::{Docker, ServiceListOptions}; + +#[tokio::main] +async fn main() { + env_logger::init(); + let docker = Docker::new(); + match docker + .services() + .list(&ServiceListOptions::builder().enable_status().build()) + .await + { + Ok(services) => { + for s in services { + println!("service -> {:#?}", s) + } + } + Err(e) => eprintln!("Error: {}", e), + } +} diff --git a/examples/volumecreate.rs b/examples/volumecreate.rs index a243be6..a95bb12 100644 --- a/examples/volumecreate.rs +++ b/examples/volumecreate.rs @@ -1,10 +1,9 @@ -use shiplift::{builder::VolumeCreateOptions, Docker}; +use shiplift::{Docker, VolumeCreateOptions}; use std::{collections::HashMap, env}; #[tokio::main] async fn main() { let docker = Docker::new(); - let volumes = docker.volumes(); let volume_name = env::args() .nth(1) @@ -13,7 +12,8 @@ async fn main() { let mut labels = HashMap::new(); labels.insert("com.github.softprops", "shiplift"); - match volumes + match docker + .volumes() .create( &VolumeCreateOptions::builder() .name(volume_name.as_ref()) diff --git a/examples/volumedelete.rs b/examples/volumedelete.rs index ec1da7e..46b37f6 100644 --- a/examples/volumedelete.rs +++ b/examples/volumedelete.rs @@ -4,13 +4,12 @@ use std::env; #[tokio::main] async fn main() { let docker = Docker::new(); - let volumes = docker.volumes(); let volume_name = env::args() .nth(1) .expect("You need to specify an volume name"); - if let Err(e) = volumes.get(&volume_name).delete().await { + if let Err(e) = docker.volumes().get(&volume_name).delete().await { eprintln!("Error: {}", e) } } diff --git a/examples/volumes.rs b/examples/volumes.rs index d45c00a..0bb82aa 100644 --- a/examples/volumes.rs +++ b/examples/volumes.rs @@ -3,9 +3,7 @@ use shiplift::Docker; #[tokio::main] async fn main() { let docker = Docker::new(); - let volumes = docker.volumes(); - - match volumes.list().await { + match docker.volumes().list().await { Ok(volumes) => { for v in volumes { println!("volume -> {:#?}", v) diff --git a/rustfmt.toml b/rustfmt.toml index 899a094..35ca786 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,4 @@ # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_args_layout fn_args_layout = "Vertical" -# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports -merge_imports = true
\ No newline at end of file +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#imports_granularity +imports_granularity="Crate"
\ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs deleted file mode 100644 index c14cbb7..0000000 --- a/src/builder.rs +++ /dev/null @@ -1,1900 +0,0 @@ -//! Interfaces for building various structures - -use crate::{errors::Error, Result}; -use serde::Serialize; -use serde_json::{self, json, map::Map, Value}; -use std::{ - cmp::Eq, - collections::{BTreeMap, HashMap}, - hash::Hash, - iter::{IntoIterator, Peekable}, -}; -use url::form_urlencoded; - -#[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(&c)) - .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()) - } -} - -#[derive(Default)] -pub struct PullOptionsBuilder { - auth: Option<RegistryAuth>, - params: HashMap<&'static str, String>, -} - -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. - 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>, |