From 5b77368c4a911908680239e922caa7683f3f5aaf Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Fri, 14 Jan 2022 18:01:30 +0000 Subject: [PATCH 1/2] Replace all Shiplift usages with Bollard --- Cargo.toml | 2 +- src/clients/cli.rs | 9 +- src/clients/http.rs | 244 ++++++++++++++++++++---------------- src/core/container.rs | 8 +- src/core/container_async.rs | 15 ++- src/core/ports.rs | 48 +++++-- tests/http_client.rs | 17 +-- 7 files changed, 205 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4b977c79..eaae32db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ description = "A library for integration-testing against docker containers from [dependencies] async-trait = "0.1" +bollard = "0.11" futures = "0.3" hex = "0.4" hmac = "0.12" @@ -19,7 +20,6 @@ rand = "0.8" serde = { version = "1", features = [ "derive" ] } serde_json = "1" sha2 = "0.10" -shiplift = { version = "0.7", default-features = false, features = [ "chrono", "unix-socket" ] } tokio = { version = "1", features = [ "macros" ] } [features] diff --git a/src/clients/cli.rs b/src/clients/cli.rs index 67f77da9..6b00a1ab 100644 --- a/src/clients/cli.rs +++ b/src/clients/cli.rs @@ -2,7 +2,7 @@ use crate::{ core::{env, env::GetEnvValue, logs::LogStream, ports::Ports, ContainerState, Docker, WaitFor}, Container, Image, ImageArgs, RunnableImage, }; -use shiplift::rep::ContainerDetails; +use bollard::models::ContainerInspectResponse; use std::{ collections::HashMap, ffi::{OsStr, OsString}, @@ -298,12 +298,13 @@ impl Docker for Cli { fn ports(&self, id: &str) -> Ports { self.inspect(id) .network_settings + .unwrap_or_default() .ports - .map(Ports::new) + .map(Ports::from) .unwrap_or_default() } - fn inspect(&self, id: &str) -> ContainerDetails { + fn inspect(&self, id: &str) -> ContainerInspectResponse { let child = self .inner .command() @@ -315,7 +316,7 @@ impl Docker for Cli { let stdout = child.stdout.unwrap(); - let mut infos: Vec = serde_json::from_reader(stdout).unwrap(); + let mut infos: Vec = serde_json::from_reader(stdout).unwrap(); let info = infos.remove(0); diff --git a/src/clients/http.rs b/src/clients/http.rs index d973cf9b..c54c915b 100644 --- a/src/clients/http.rs +++ b/src/clients/http.rs @@ -3,14 +3,16 @@ use crate::{ ContainerAsync, Image, RunnableImage, }; use async_trait::async_trait; -use futures::{executor::block_on, stream::StreamExt, TryStreamExt}; -use shiplift::{ - builder::ContainerOptionsBuilder, - rep::{ContainerCreateInfo, ContainerDetails}, - ContainerOptions, Docker, LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, - RmContainerOptions, +use bollard::{ + container::{Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions}, + image::CreateImageOptions, + models::{ContainerCreateResponse, ContainerInspectResponse, HostConfig, PortBinding}, + network::CreateNetworkOptions, + Docker, }; +use futures::{executor::block_on, stream::StreamExt, TryStreamExt}; use std::{ + collections::HashMap, fmt, io, sync::{Arc, RwLock}, }; @@ -27,7 +29,7 @@ pub struct Http { /// This exists so we don't have to make the outer client clonable and still can have only a single instance around which is important for `Drop` behaviour. struct Client { command: env::Command, - shiplift: Docker, + bollard: Docker, created_networks: RwLock>, } @@ -47,11 +49,19 @@ impl Default for Http { impl Http { pub async fn run(&self, image: impl Into>) -> ContainerAsync<'_, I> { let image = image.into(); - let mut options_builder = ContainerOptions::builder(image.descriptor().as_str()); + let mut create_options: Option> = None; + let mut config: Config = Config { + image: Some(image.descriptor()), + host_config: Some(HostConfig::default()), + ..Default::default() + }; // Create network and add it to container creation if let Some(network) = image.network() { - options_builder.network_mode(network.as_str()); + config.host_config = config.host_config.map(|mut host_config| { + host_config.network_mode = Some(network.to_string()); + host_config + }); if self.create_network_if_not_exists(network).await { let mut guard = self .inner @@ -64,7 +74,9 @@ impl Http { // name of the container if let Some(name) = image.container_name() { - options_builder.name(name.as_str()); + create_options = Some(CreateContainerOptions { + name: name.to_owned(), + }) } // handle environment variables @@ -73,64 +85,85 @@ impl Http { .into_iter() .map(|(k, v)| format!("{}={}", k, v)) .collect(); - - // the fact .env and .volumes takes Vec<&str> instead of AsRef is making - // this more difficult than it needs to be - let envs_str: Vec<&str> = envs.iter().map(|s| s.as_ref()).collect(); - options_builder.env(envs_str); + config.env = Some(envs); // volumes - let vols: Vec = image + let vols: HashMap> = image .volumes() .into_iter() - .map(|(orig, dest)| format!("{}:{}", orig, dest)) + .map(|(orig, dest)| (format!("{}:{}", orig, dest), HashMap::new())) .collect(); - let vols_str: Vec<&str> = vols.iter().map(|s| s.as_ref()).collect(); - options_builder.volumes(vols_str); + config.volumes = Some(vols); // entrypoint if let Some(entrypoint) = image.entrypoint() { - options_builder.entrypoint(entrypoint.as_str()); + config.entrypoint = Some(vec![entrypoint]); } // ports if let Some(ports) = image.ports() { + config.host_config = config.host_config.map(|mut host_config| { + host_config.port_bindings = Some(HashMap::new()); + host_config + }); + for port in ports { - // casting u16 to u32 - options_builder.expose(port.internal as u32, "tcp", port.local as u32); + config.host_config = config.host_config.map(|mut host_config| { + host_config.port_bindings = + host_config.port_bindings.map(|mut port_bindings| { + port_bindings.insert( + format!("{}/tcp", port.internal), + Some(vec![PortBinding { + host_ip: Some(String::from("127.0.0.1")), + host_port: Some(port.local.to_string()), + }]), + ); + + port_bindings + }); + + host_config + }); } } else { - options_builder.publish_all_ports(); + config.host_config = config.host_config.map(|mut host_config| { + host_config.publish_all_ports = Some(true); + host_config + }); } // create the container with options - let create_result = self.create_container(&options_builder).await; + let create_result = self + .create_container(create_options.clone(), config.clone()) + .await; let id = { match create_result { Ok(container) => container.id, - Err(shiplift::Error::Fault { code, .. }) if code == 404 => { + Err(bollard::errors::Error::DockerResponseNotFoundError { message: _ }) => { { - let mut options_builder = PullOptions::builder(); - options_builder.image(image.descriptor()); - let mut pulling = - self.inner.shiplift.images().pull(&options_builder.build()); + let pull_options = Some(CreateImageOptions { + from_image: image.descriptor(), + ..Default::default() + }); + let mut pulling = self.inner.bollard.create_image(pull_options, None, None); while let Some(result) = pulling.next().await { if result.is_err() { result.unwrap(); } } } - self.create_container(&options_builder).await.unwrap().id + self.create_container(create_options, config) + .await + .unwrap() + .id } Err(err) => panic!("{}", err), } }; self.inner - .shiplift - .containers() - .get(&id) - .start() + .bollard + .start_container::(&id, None) .await .unwrap(); @@ -147,18 +180,20 @@ impl Http { Http { inner: Arc::new(Client { command: env::command::().unwrap_or_default(), - shiplift: Docker::new(), + bollard: Docker::connect_with_http_defaults().unwrap(), created_networks: RwLock::new(Vec::new()), }), } } async fn create_network_if_not_exists(&self, network: &str) -> bool { - if !network_exists(&self.inner.shiplift, network).await { + if !network_exists(&self.inner.bollard, network).await { self.inner - .shiplift - .networks() - .create(&NetworkCreateOptions::builder(network).build()) + .bollard + .create_network(CreateNetworkOptions { + name: network.to_owned(), + ..Default::default() + }) .await .unwrap(); @@ -170,28 +205,24 @@ impl Http { async fn create_container( &self, - options_builder: &ContainerOptionsBuilder, - ) -> shiplift::Result { - self.inner - .shiplift - .containers() - .create(&options_builder.build()) - .await + options: Option>, + config: Config, + ) -> Result { + self.inner.bollard.create_container(options, config).await } - fn logs(&self, container_id: String, options: LogsOptions) -> LogStreamAsync<'_> { + fn logs(&self, container_id: String, options: LogsOptions) -> LogStreamAsync<'_> { let stream = self .inner - .shiplift - .containers() - .get(container_id) - .logs(&options) + .bollard + .logs(&container_id, Some(options)) .map_err(|err| io::Error::new(io::ErrorKind::Other, err)) .map(|chunk| { - let string = String::from_utf8(Vec::from(chunk?)) + let bytes = chunk?.into_bytes(); + let str = std::str::from_utf8(bytes.as_ref()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(string) + Ok(String::from(str)) }) .boxed(); @@ -200,13 +231,10 @@ impl Http { } async fn network_exists(client: &Docker, network: &str) -> bool { - // There's no public builder for NetworkListOptions yet - // might need to add one in shiplift - // this is doing unnecessary stuff - let network_list_optons = NetworkListOptions::default(); - let networks = client.networks().list(&network_list_optons).await.unwrap(); - - networks.iter().any(|i| i.name == network) + let networks = client.list_networks::(None).await.unwrap(); + networks + .iter() + .any(|i| matches!(&i.name, Some(name) if name == network)) } impl Drop for Client { @@ -215,14 +243,7 @@ impl Drop for Client { env::Command::Remove => { let guard = self.created_networks.read().expect("failed to lock RwLock"); for network in guard.iter() { - block_on(async { - self.shiplift - .networks() - .get(network) - .delete() - .await - .unwrap() - }); + block_on(async { self.bollard.remove_network(network).await.unwrap() }); } } env::Command::Keep => {} @@ -235,14 +256,24 @@ impl DockerAsync for Http { fn stdout_logs(&self, id: &str) -> LogStreamAsync<'_> { self.logs( id.to_owned(), - LogsOptions::builder().stdout(true).stderr(false).build(), + LogsOptions { + follow: true, + stdout: true, + tail: "all".to_owned(), + ..Default::default() + }, ) } fn stderr_logs(&self, id: &str) -> LogStreamAsync<'_> { self.logs( id.to_owned(), - LogsOptions::builder().stdout(false).stderr(true).build(), + LogsOptions { + follow: true, + stderr: true, + tail: "all".to_owned(), + ..Default::default() + }, ) } @@ -250,52 +281,43 @@ impl DockerAsync for Http { self.inspect(id) .await .network_settings + .unwrap_or_default() .ports - .map(Ports::new) + .map(Ports::from) .unwrap_or_default() } - async fn inspect(&self, id: &str) -> ContainerDetails { + async fn inspect(&self, id: &str) -> ContainerInspectResponse { self.inner - .shiplift - .containers() - .get(id) - .inspect() + .bollard + .inspect_container(id, None) .await .unwrap() } async fn rm(&self, id: &str) { self.inner - .shiplift - .containers() - .get(id) - .remove( - RmContainerOptions::builder() - .volumes(true) - .force(true) - .build(), + .bollard + .remove_container( + id, + Some(RemoveContainerOptions { + force: true, + v: true, + ..Default::default() + }), ) .await .unwrap(); } async fn stop(&self, id: &str) { - self.inner - .shiplift - .containers() - .get(id) - .stop(Option::None) - .await - .unwrap(); + self.inner.bollard.stop_container(id, None).await.unwrap(); } async fn start(&self, id: &str) { self.inner - .shiplift - .containers() - .get(id) - .start() + .bollard + .start_container::(id, None) .await .unwrap(); } @@ -305,11 +327,10 @@ impl DockerAsync for Http { mod tests { use super::*; use crate::images::{generic::GenericImage, hello_world::HelloWorld}; - use shiplift::rep::ContainerDetails; use spectral::prelude::*; - async fn inspect(client: &shiplift::Docker, id: &str) -> ContainerDetails { - client.containers().get(id).inspect().await.unwrap() + async fn inspect(client: &bollard::Docker, id: &str) -> ContainerInspectResponse { + client.inspect_container(id, None).await.unwrap() } #[tokio::test(flavor = "multi_thread")] @@ -319,8 +340,9 @@ mod tests { let container = docker.run(image).await; // inspect volume and env - let container_details = inspect(&docker.inner.shiplift, container.id()).await; - assert_that!(container_details.host_config.publish_all_ports).is_equal_to(true); + let container_details = inspect(&docker.inner.bollard, container.id()).await; + assert_that!(container_details.host_config.unwrap().publish_all_ports) + .is_equal_to(Some(true)); } #[tokio::test(flavor = "multi_thread")] @@ -332,9 +354,13 @@ mod tests { .with_mapped_port((555, 888)); let container = docker.run(image).await; - let container_details = inspect(&docker.inner.shiplift, container.id()).await; + let container_details = inspect(&docker.inner.bollard, container.id()).await; - let port_bindings = container_details.host_config.port_bindings.unwrap(); + let port_bindings = container_details + .host_config + .unwrap() + .port_bindings + .unwrap(); assert_that!(&port_bindings).contains_key(&"456/tcp".into()); assert_that!(&port_bindings).contains_key(&"888/tcp".into()); } @@ -346,8 +372,12 @@ mod tests { let image = RunnableImage::from(image).with_network("awesome-net-1"); let container = docker.run(image).await; - let container_details = inspect(&docker.inner.shiplift, container.id()).await; - let networks = container_details.network_settings.networks; + let container_details = inspect(&docker.inner.bollard, container.id()).await; + let networks = container_details + .network_settings + .unwrap() + .networks + .unwrap(); assert!( networks.contains_key("awesome-net-1"), @@ -363,13 +393,13 @@ mod tests { let image = RunnableImage::from(image).with_container_name("hello_container"); let container = docker.run(image).await; - let container_details = inspect(&docker.inner.shiplift, container.id()).await; - assert_that!(container_details.name).ends_with("hello_container"); + let container_details = inspect(&docker.inner.bollard, container.id()).await; + assert_that!(container_details.name.unwrap()).ends_with("hello_container"); } #[tokio::test(flavor = "multi_thread")] async fn http_should_create_network_if_image_needs_it_and_drop_it_in_the_end() { - let client = shiplift::Docker::new(); + let client = bollard::Docker::connect_with_http_defaults().unwrap(); { let docker = Http::new(); diff --git a/src/core/container.rs b/src/core/container.rs index b9308929..8071ddbb 100644 --- a/src/core/container.rs +++ b/src/core/container.rs @@ -2,7 +2,7 @@ use crate::{ core::{env::Command, logs::LogStream, ports::Ports, ExecCommand, WaitFor}, Image, RunnableImage, }; -use shiplift::rep::ContainerDetails; +use bollard::models::ContainerInspectResponse; use std::{fmt, marker::PhantomData, net::IpAddr, str::FromStr}; /// Represents a running docker container. @@ -133,7 +133,9 @@ where .docker_client .inspect(&self.id) .network_settings - .ip_address, + .unwrap_or_default() + .ip_address + .unwrap_or_default(), ) .unwrap_or_else(|_| panic!("container {} has missing or invalid bridge IP", self.id)) } @@ -195,7 +197,7 @@ pub(crate) trait Docker { fn stdout_logs(&self, id: &str) -> LogStream; fn stderr_logs(&self, id: &str) -> LogStream; fn ports(&self, id: &str) -> Ports; - fn inspect(&self, id: &str) -> ContainerDetails; + fn inspect(&self, id: &str) -> ContainerInspectResponse; fn rm(&self, id: &str); fn stop(&self, id: &str); fn start(&self, id: &str); diff --git a/src/core/container_async.rs b/src/core/container_async.rs index 53c07db3..192f6d45 100644 --- a/src/core/container_async.rs +++ b/src/core/container_async.rs @@ -3,8 +3,8 @@ use crate::{ Image, RunnableImage, }; use async_trait::async_trait; +use bollard::models::ContainerInspectResponse; use futures::{executor::block_on, FutureExt}; -use shiplift::rep::ContainerDetails; use std::{fmt, marker::PhantomData, net::IpAddr, str::FromStr}; /// Represents a running docker container that has been started using an async client.. @@ -76,8 +76,15 @@ where pub async fn get_bridge_ip_address(&self) -> IpAddr { self.docker_client .inspect(&self.id) - .map(|details: ContainerDetails| { - IpAddr::from_str(&details.network_settings.ip_address).unwrap_or_else(|_| { + .map(|details| { + IpAddr::from_str( + &details + .network_settings + .unwrap_or_default() + .ip_address + .unwrap_or_default(), + ) + .unwrap_or_else(|_| { panic!("container {} has missing or invalid bridge IP", self.id) }) }) @@ -132,7 +139,7 @@ where fn stdout_logs(&self, id: &str) -> LogStreamAsync<'_>; fn stderr_logs(&self, id: &str) -> LogStreamAsync<'_>; async fn ports(&self, id: &str) -> Ports; - async fn inspect(&self, id: &str) -> ContainerDetails; + async fn inspect(&self, id: &str) -> ContainerInspectResponse; async fn rm(&self, id: &str); async fn stop(&self, id: &str); async fn start(&self, id: &str); diff --git a/src/core/ports.rs b/src/core/ports.rs index a87423c0..eb5ac07c 100644 --- a/src/core/ports.rs +++ b/src/core/ports.rs @@ -1,3 +1,4 @@ +use bollard::models::{PortBinding, PortMap}; use std::collections::HashMap; /// The exposed ports of a running container. @@ -8,18 +9,47 @@ pub struct Ports { impl Ports { pub fn new(ports: HashMap>>>) -> Self { + let port_binding = ports + .into_iter() + .filter_map(|(internal, external)| { + Some(( + internal, + Some( + external? + .into_iter() + .map(|external| PortBinding { + host_ip: external.get("HostIp").cloned(), + host_port: external.get("HostPort").cloned(), + }) + .collect(), + ), + )) + }) + .collect::>(); + + Self::from(port_binding) + } + + /// Returns the host port for the given internal port. + pub fn map_to_host_port(&self, internal_port: u16) -> Option { + self.mapping.get(&internal_port).cloned() + } +} + +impl From for Ports { + fn from(ports: PortMap) -> Self { let mapping = ports .into_iter() .filter_map(|(internal, external)| { // internal is '8332/tcp', split off the protocol ... let internal = internal.split('/').next()?; - // external is a an optional list of maps: [ { "HostIp": "0.0.0.0", "HostPort": "33078" } ] // get the first entry and get the value of the `HostPort` field - let external = external?.first()?.get("HostPort").cloned()?; + let external = external?; + let external_port = external.first()?.host_port.as_ref()?; let internal = parse_port(internal); - let external = parse_port(&external); + let external = parse_port(external_port); log::debug!("Registering port mapping: {} -> {}", internal, external); @@ -29,11 +59,6 @@ impl Ports { Self { mapping } } - - /// Returns the host port for the given internal port. - pub fn map_to_host_port(&self, internal_port: u16) -> Option { - self.mapping.get(&internal_port).cloned() - } } fn parse_port(port: &str) -> u16 { @@ -44,11 +69,11 @@ fn parse_port(port: &str) -> u16 { #[cfg(test)] mod tests { use super::*; - use shiplift::rep::ContainerDetails; + use bollard::models::ContainerInspectResponse; #[test] fn can_deserialize_docker_inspect_response_into_api_ports() { - let container_details = serde_json::from_str::( + let container_details = serde_json::from_str::( r#"{ "Id": "1233c36b54a5bac19efbf92728aa33b2faf67f3364f24db506d90fd46a5d0e8c", "Created": "2021-02-19T04:57:38.081442827Z", @@ -276,8 +301,9 @@ mod tests { let parsed_ports = container_details .network_settings + .unwrap_or_default() .ports - .map(Ports::new) + .map(Ports::from) .unwrap_or_default(); let mut expected_ports = Ports::default(); diff --git a/tests/http_client.rs b/tests/http_client.rs index 39585284..4f97219a 100644 --- a/tests/http_client.rs +++ b/tests/http_client.rs @@ -1,6 +1,5 @@ #![cfg(feature = "experimental")] -use shiplift::ImageListOptions; use std::time::Duration; use testcontainers::{ core::WaitFor, @@ -9,7 +8,7 @@ use testcontainers::{ }; #[tokio::test(flavor = "multi_thread")] -async fn shiplift_can_run_hello_world() { +async fn bollard_can_run_hello_world() { let _ = pretty_env_logger::try_init(); let docker = clients::Http::default(); @@ -18,23 +17,25 @@ async fn shiplift_can_run_hello_world() { } async fn cleanup_hello_world_image() { - let docker = shiplift::Docker::new(); + let docker = bollard::Docker::connect_with_http_defaults().unwrap(); futures::future::join_all( docker - .images() - .list(&ImageListOptions::builder().build()) + .list_images::(None) .await .unwrap() .into_iter() - .flat_map(|image| image.repo_tags.into_iter().flatten()) + .flat_map(|image| image.repo_tags.into_iter()) .filter(|tag| tag.starts_with("hello-world")) - .map(|tag| async { docker.images().get(tag).delete().await }), + .map(|tag| async { + let tag_captured = tag; + docker.remove_image(&tag_captured, None, None).await + }), ) .await; } #[tokio::test(flavor = "multi_thread")] -async fn shiplift_pull_missing_image_hello_world() { +async fn bollard_pull_missing_image_hello_world() { let _ = pretty_env_logger::try_init(); cleanup_hello_world_image().await; let docker = clients::Http::default(); From 9a19fd7a97a2b9c86c69597f39f8eba45ed9d07c Mon Sep 17 00:00:00 2001 From: Andrey Yantsen Date: Mon, 17 Jan 2022 09:22:13 +0000 Subject: [PATCH 2/2] Use `bollard-stubs` directly for models `bollard-stubs` is a crate with autogenerated models for the Docker API. Previously, we were using models embedded in `bollard` inside CLI, while the client itself wasn't required. Now we're making `bollard` an optional dependency and including it only when we need the heavy-lifting, i.e. the client logic. The internal implementation of the CLI client uses the models from `bollard-stubs` from now on. --- Cargo.toml | 5 +++-- src/clients/cli.rs | 2 +- src/core/container.rs | 2 +- src/core/ports.rs | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eaae32db..ceff8466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ description = "A library for integration-testing against docker containers from [dependencies] async-trait = "0.1" -bollard = "0.11" +bollard = { version = "0.11", optional = true } +bollard-stubs = "1.41" futures = "0.3" hex = "0.4" hmac = "0.12" @@ -23,7 +24,7 @@ sha2 = "0.10" tokio = { version = "1", features = [ "macros" ] } [features] -experimental = [ ] +experimental = [ "bollard" ] [dev-dependencies] bitcoincore-rpc = "0.14" diff --git a/src/clients/cli.rs b/src/clients/cli.rs index 6b00a1ab..1139aa1b 100644 --- a/src/clients/cli.rs +++ b/src/clients/cli.rs @@ -2,7 +2,7 @@ use crate::{ core::{env, env::GetEnvValue, logs::LogStream, ports::Ports, ContainerState, Docker, WaitFor}, Container, Image, ImageArgs, RunnableImage, }; -use bollard::models::ContainerInspectResponse; +use bollard_stubs::models::ContainerInspectResponse; use std::{ collections::HashMap, ffi::{OsStr, OsString}, diff --git a/src/core/container.rs b/src/core/container.rs index 8071ddbb..4019cda4 100644 --- a/src/core/container.rs +++ b/src/core/container.rs @@ -2,7 +2,7 @@ use crate::{ core::{env::Command, logs::LogStream, ports::Ports, ExecCommand, WaitFor}, Image, RunnableImage, }; -use bollard::models::ContainerInspectResponse; +use bollard_stubs::models::ContainerInspectResponse; use std::{fmt, marker::PhantomData, net::IpAddr, str::FromStr}; /// Represents a running docker container. diff --git a/src/core/ports.rs b/src/core/ports.rs index eb5ac07c..dc4efbad 100644 --- a/src/core/ports.rs +++ b/src/core/ports.rs @@ -1,4 +1,4 @@ -use bollard::models::{PortBinding, PortMap}; +use bollard_stubs::models::{PortBinding, PortMap}; use std::collections::HashMap; /// The exposed ports of a running container. @@ -69,7 +69,7 @@ fn parse_port(port: &str) -> u16 { #[cfg(test)] mod tests { use super::*; - use bollard::models::ContainerInspectResponse; + use bollard_stubs::models::ContainerInspectResponse; #[test] fn can_deserialize_docker_inspect_response_into_api_ports() {