diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cadb323..c1713ff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,5 +47,5 @@ jobs: - name: Test run: | docker pull ubuntu:latest - cargo test --all-features --all-targets + cargo test --all-features --all-targets -j1 cargo test --doc diff --git a/examples/container.rs b/examples/container.rs index 0d5b2de..0d29ac4 100644 --- a/examples/container.rs +++ b/examples/container.rs @@ -129,7 +129,7 @@ async fn main() -> Result<(), Box> { match opts.subcmd { Cmd::Attach { id } => { let container = docker.containers().get(&id); - let tty_multiplexer = container.attach().await?; + let tty_multiplexer = container.attach(false).await?; let (mut reader, _writer) = tty_multiplexer.split(); diff --git a/src/api/container.rs b/src/api/container.rs index 04b3e71..278e12e 100644 --- a/src/api/container.rs +++ b/src/api/container.rs @@ -47,13 +47,18 @@ impl Container { /// The [`TtyMultiplexer`](TtyMultiplexer) implements Stream for returning Stdout and Stderr chunks. It also implements [`AsyncWrite`](futures_util::io::AsyncWrite) for writing to Stdin. /// /// The multiplexer can be split into its read and write halves with the [`split`](TtyMultiplexer::split) method - pub async fn attach(&self) -> Result { + pub async fn attach(&self, logs: bool) -> Result { let inspect = self.inspect().await?; + let logs = if logs { + 1 + } else { + 0 + }; let is_tty = inspect.config.and_then(|c| c.tty).unwrap_or_default(); stream::attach( self.docker.clone(), format!( - "/containers/{}/attach?stream=1&stdout=1&stderr=1&stdin=1", + "/containers/{}/attach?stream=1&stdout=1&stderr=1&stdin=1&logs={logs}", self.id ), Payload::empty(), @@ -171,6 +176,20 @@ impl Container { .map(|_| ()) }} + api_doc! { Container => Resize + | + /// Resize the TTY for a container + pub async fn resize(&self, width: u16, height: u16) -> Result<()> { + self.docker + .post_string( + &format!("/containers/{}/resize?w={width}&h={height}", self.id), + Payload::empty(), + Headers::none() + ) + .await + .map(|_| ()) + }} + api_doc! { Container => Pause | /// Pause the container instance. diff --git a/src/api/image.rs b/src/api/image.rs index 0248698..dd34e9f 100644 --- a/src/api/image.rs +++ b/src/api/image.rs @@ -89,7 +89,7 @@ impl Image { let headers = opts .auth_header() .map(|auth| Headers::single(AUTH_HEADER, auth)) - .unwrap_or_else(Headers::default); + .unwrap_or_default(); self.docker .post_string(&ep, Payload::empty(), Some(headers)) diff --git a/src/api/mod.rs b/src/api/mod.rs index 609dd38..9b695da 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -28,7 +28,7 @@ pub mod swarm; #[cfg_attr(docsrs, doc(cfg(feature = "swarm")))] pub mod task; -pub use {container::*, exec::*, image::*, network::*, system::*, volume::*}; +pub use {container::*, exec::*, image::*, network::*, volume::*}; #[cfg(feature = "swarm")] #[cfg_attr(docsrs, doc(cfg(feature = "swarm")))] diff --git a/src/lib.rs b/src/lib.rs index 730483c..a355841 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,9 @@ mod stream; pub mod conn { //! Connection related items pub(crate) use containers_api::conn::*; - pub use containers_api::conn::{Error, Transport, TtyChunk}; + pub use containers_api::conn::{ + tty::Multiplexer as TtyMultiplexer, Error, Transport, TtyChunk, + }; } pub mod docker; pub mod errors; diff --git a/src/opts/container.rs b/src/opts/container.rs index 0ef099c..be173ed 100644 --- a/src/opts/container.rs +++ b/src/opts/container.rs @@ -6,6 +6,7 @@ use containers_api::{ impl_str_field, impl_url_bool_field, impl_url_str_field, impl_vec_field, }; +use std::fmt::{Display, Formatter}; use std::net::SocketAddr; use std::{ collections::HashMap, @@ -331,9 +332,9 @@ impl FromStr for PublishPort { } } -impl ToString for PublishPort { - fn to_string(&self) -> String { - format!("{}/{}", self.port, self.protocol.as_ref()) +impl Display for PublishPort { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.port, self.protocol.as_ref()) } } @@ -388,15 +389,16 @@ pub enum IpcMode { Host, } -impl ToString for IpcMode { - fn to_string(&self) -> String { - match &self { +impl Display for IpcMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match &self { IpcMode::None => String::from("none"), IpcMode::Private => String::from("private"), IpcMode::Shareable => String::from("shareable"), IpcMode::Container(id) => format!("container:{}", id), IpcMode::Host => String::from("host"), - } + }; + write!(f, "{}", str) } } @@ -408,12 +410,13 @@ pub enum PidMode { Host, } -impl ToString for PidMode { - fn to_string(&self) -> String { - match &self { +impl Display for PidMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match &self { PidMode::Container(id) => format!("container:{}", id), PidMode::Host => String::from("host"), - } + }; + write!(f, "{}", str) } } @@ -524,6 +527,11 @@ impl ContainerCreateOptsBuilder { security_options => "HostConfig.SecurityOpt" ); + impl_field!( + /// Mount the container's root filesystem as read only. + readonly_rootfs: bool => "HostConfig.ReadonlyRootfs" + ); + impl_vec_field!( /// Specify any bind mounts, taking the form of `/some/host/path:/some/container/path` volumes => "HostConfig.Binds" @@ -859,6 +867,13 @@ mod tests { r#"{"HostConfig":{"AutoRemove":true,"NetworkMode":"host","Privileged":true},"Image":"test_image"}"# ); + test_case!( + ContainerCreateOptsBuilder::default() + .image("test_image") + .readonly_rootfs(true), + r#"{"HostConfig":{"ReadonlyRootfs":true},"Image":"test_image"}"# + ); + test_case!( ContainerCreateOptsBuilder::default() .image("test_image") diff --git a/src/opts/image.rs b/src/opts/image.rs index 26e9a66..3bec582 100644 --- a/src/opts/image.rs +++ b/src/opts/image.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -370,16 +371,17 @@ pub enum ImageName { Digest { image: String, digest: String }, } -impl ToString for ImageName { - fn to_string(&self) -> String { - match &self { +impl Display for ImageName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match &self { ImageName::Tag { image, tag } => match tag { Some(tag) => format!("{image}:{tag}"), None => image.to_owned(), }, ImageName::Id(id) => id.to_owned(), ImageName::Digest { image, digest } => format!("{image}@{digest}"), - } + }; + write!(f, "{}", str) } } diff --git a/src/opts/mod.rs b/src/opts/mod.rs index 4e9d4ec..f303704 100644 --- a/src/opts/mod.rs +++ b/src/opts/mod.rs @@ -129,8 +129,7 @@ mod tests { #[cfg(feature = "chrono")] #[test] fn logs_options() { - let timestamp = chrono::NaiveDateTime::from_timestamp_opt(2_147_483_647, 0); - let since = chrono::DateTime::::from_utc(timestamp.unwrap(), chrono::Utc); + let timestamp = chrono::DateTime::from_timestamp(2_147_483_647, 0).unwrap(); let options = LogsOptsBuilder::default() .follow(true) @@ -138,7 +137,7 @@ mod tests { .stderr(true) .timestamps(true) .all() - .since(&since) + .since(×tamp) .build(); let serialized = options.serialize().unwrap(); @@ -150,7 +149,10 @@ mod tests { assert!(serialized.contains("tail=all")); assert!(serialized.contains("since=2147483647")); - let options = LogsOptsBuilder::default().n_lines(5).until(&since).build(); + let options = LogsOptsBuilder::default() + .n_lines(5) + .until(×tamp) + .build(); let serialized = options.serialize().unwrap(); diff --git a/tests/common.rs b/tests/common.rs index 44bed52..52786df 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -3,8 +3,12 @@ use std::env; use std::path::PathBuf; -pub use docker_api::{api, conn, models, models::ImageBuildChunk, opts, Docker}; -pub use futures_util::{StreamExt, TryStreamExt}; +#[cfg(test)] +pub use docker_api::conn; +pub use docker_api::{api, models, models::ImageBuildChunk, opts, Docker}; +pub use futures_util::StreamExt; +#[cfg(test)] +pub use futures_util::TryStreamExt; pub use tempfile::TempDir; pub const DEFAULT_IMAGE: &str = "ubuntu:latest"; diff --git a/tests/container_tests.rs b/tests/container_tests.rs index b8ee2ce..0ceefda 100644 --- a/tests/container_tests.rs +++ b/tests/container_tests.rs @@ -501,6 +501,13 @@ async fn container_stats() { async fn container_top() { let docker = init_runtime(); + let version = docker.version().await.unwrap(); + let is_podman = version + .components + .unwrap_or_default() + .iter() + .any(|component| component.name == "Podman Engine"); + let container_name = "test-top-container"; let container = create_base_container(&docker, container_name, None).await; @@ -508,7 +515,15 @@ async fn container_top() { let top_result = container.top(None).await; assert!(top_result.is_ok()); - assert!(top_result.unwrap().processes.unwrap_or_default()[0].contains(&DEFAULT_CMD.to_string())); + + if is_podman { + assert!(top_result.unwrap().processes.unwrap_or_default()[0][0] + .contains(&DEFAULT_CMD.to_string())); + } else { + assert!( + top_result.unwrap().processes.unwrap_or_default()[0].contains(&DEFAULT_CMD.to_string()) + ); + } cleanup_container(&docker, container_name).await; } @@ -676,13 +691,12 @@ async fn container_attach() { let _ = container.start().await; - let mut multiplexer = container.attach().await.unwrap(); - while let Some(chunk) = multiplexer.next().await { + let mut multiplexer = container.attach(false).await.unwrap(); + if let Some(chunk) = multiplexer.next().await { match chunk { Ok(TtyChunk::StdOut(chunk)) => { let logs = String::from_utf8_lossy(&chunk); assert_eq!(logs, "123456\r\n"); - break; } chunk => { eprintln!("invalid chunk {chunk:?}"); @@ -712,13 +726,12 @@ async fn container_attach() { let _ = container.start().await; - let mut multiplexer = container.attach().await.unwrap(); - while let Some(chunk) = multiplexer.next().await { + let mut multiplexer = container.attach(false).await.unwrap(); + if let Some(chunk) = multiplexer.next().await { match chunk { Ok(TtyChunk::StdOut(chunk)) => { let logs = String::from_utf8_lossy(&chunk); assert_eq!(logs, "123456\n"); - break; } chunk => { eprintln!("invalid chunk {chunk:?}"); diff --git a/tests/docker_tests.rs b/tests/docker_tests.rs index fbf4f23..013721d 100644 --- a/tests/docker_tests.rs +++ b/tests/docker_tests.rs @@ -9,11 +9,6 @@ async fn docker_info() { let info_result = docker.info().await; assert!(info_result.is_ok()); - let info_data = info_result.unwrap(); - assert_eq!( - info_data.name.unwrap(), - gethostname::gethostname().into_string().unwrap() - ); } #[tokio::test] diff --git a/tests/image_tests.rs b/tests/image_tests.rs index 1577325..0edd6d9 100644 --- a/tests/image_tests.rs +++ b/tests/image_tests.rs @@ -2,9 +2,11 @@ mod common; use common::{ create_base_image, get_image_full_id, init_runtime, opts, tempdir_with_dockerfile, StreamExt, - TryStreamExt, DEFAULT_IMAGE, + DEFAULT_IMAGE, }; +use futures_util::TryStreamExt; + #[tokio::test] async fn image_create_inspect_delete() { let docker = init_runtime(); @@ -41,7 +43,8 @@ async fn image_inspect() { .repo_tags .as_ref() .unwrap() - .contains(&format!("{image_name}:latest"))); + .iter() + .any(|tag| tag.contains(&format!("{image_name}:latest")))); assert!(image.delete().await.is_ok()); } @@ -61,7 +64,7 @@ async fn image_history() { println!("{history_data:#?}"); assert!(history_data .iter() - .any(|item| item.tags.iter().any(|t| t == DEFAULT_IMAGE))); + .any(|item| item.tags.iter().any(|t| t.contains(DEFAULT_IMAGE)))); } #[tokio::test] @@ -89,7 +92,8 @@ async fn image_tag() { .expect("image inspect data") .repo_tags .expect("repo tags") - .contains(&new_tag)); + .iter() + .any(|tag| tag.contains(&new_tag))); //cleanup let _ = image.delete().await; diff --git a/tests/network_tests.rs b/tests/network_tests.rs index 2490cb2..c963871 100644 --- a/tests/network_tests.rs +++ b/tests/network_tests.rs @@ -187,8 +187,7 @@ async fn network_connect_disconnect() { .unwrap() .networks .unwrap() - .get(network_name) - .is_some()); + .contains_key(network_name)); let _ = network.delete().await; let _ = container.delete().await;