diff options
author | Wojciech Kępka <46892771+wojciechkepka@users.noreply.github.com> | 2021-02-06 07:57:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-06 01:57:49 -0500 |
commit | ef3dfad8f691f64e41fb1d369399471cde6ad8c0 (patch) | |
tree | e8b5bba9d148b3522171eaf72b33624334774416 | |
parent | 9b85dc8a9d370139e8eb3cafadf5a0f8f6dc6597 (diff) |
Add Exec struct for easier manipulation of exec instances (#251)
* Add ExecDetails and ProcessConfig
* Add exec_with_id, exec_inspect
* Add example how to inspect an exec instance
* Make clippy happy
* exit_code is an Option on ExecDetails
* Add Exec struct
* Update example
* Fix typo
* Add Exec::resize and ExecResizeOptions
* Add resize example
-rw-r--r-- | examples/execinspect.rs | 32 | ||||
-rw-r--r-- | examples/execresize.rs | 30 | ||||
-rw-r--r-- | src/builder.rs | 69 | ||||
-rw-r--r-- | src/lib.rs | 178 | ||||
-rw-r--r-- | src/rep.rs | 27 |
5 files changed, 291 insertions, 45 deletions
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/src/builder.rs b/src/builder.rs index 110abcc..6971be9 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1701,6 +1701,75 @@ impl VolumeCreateOptionsBuilder { } } } +/// +/// Interface for creating volumes +#[derive(Serialize, Debug)] +pub struct ExecResizeOptions { + params: HashMap<&'static str, Value>, +} + +impl ExecResizeOptions { + /// serialize options as a string. returns None if no options are defined + pub fn serialize(&self) -> Result<String> { + serde_json::to_string(&self.params).map_err(Error::from) + } + + pub fn parse_from<'a, K, V>( + &self, + params: &'a HashMap<K, V>, + body: &mut BTreeMap<String, Value>, + ) where + &'a HashMap<K, V>: IntoIterator, + K: ToString + Eq + Hash, + V: Serialize, + { + for (k, v) in params.iter() { + let key = k.to_string(); + let value = serde_json::to_value(v).unwrap(); + + body.insert(key, value); + } + } + + /// return a new instance of a builder for options + pub fn builder() -> ExecResizeOptionsBuilder { + ExecResizeOptionsBuilder::new() + } +} + +#[derive(Default)] +pub struct ExecResizeOptionsBuilder { + params: HashMap<&'static str, Value>, +} + +impl ExecResizeOptionsBuilder { + pub(crate) fn new() -> Self { + let params = HashMap::new(); + ExecResizeOptionsBuilder { params } + } + + pub fn height( + &mut self, + height: u64, + ) -> &mut Self { + self.params.insert("Name", json!(height)); + self + } + + pub fn width( + &mut self, + width: u64, + ) -> &mut Self { + self.params.insert("Name", json!(width)); + self + } + + pub fn build(&self) -> ExecResizeOptions { + ExecResizeOptions { + params: self.params.clone(), + } + } +} #[cfg(test)] mod tests { @@ -28,16 +28,16 @@ mod tarball; pub use crate::{ builder::{ BuildOptions, ContainerConnectionOptions, ContainerFilter, ContainerListOptions, - ContainerOptions, EventsOptions, ExecContainerOptions, ImageFilter, ImageListOptions, - LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, RegistryAuth, - RmContainerOptions, TagOptions, VolumeCreateOptions, + ContainerOptions, EventsOptions, ExecContainerOptions, ExecResizeOptions, ImageFilter, + ImageListOptions, LogsOptions, NetworkCreateOptions, NetworkListOptions, PullOptions, + RegistryAuth, RmContainerOptions, TagOptions, VolumeCreateOptions, }, errors::Error, }; use crate::{ rep::{ - Change, Container as ContainerRep, ContainerCreateInfo, ContainerDetails, Event, Exit, - History, Image as ImageRep, ImageDetails, Info, NetworkCreateInfo, + Change, Container as ContainerRep, ContainerCreateInfo, ContainerDetails, Event, + ExecDetails, Exit, History, Image as ImageRep, ImageDetails, Info, NetworkCreateInfo, NetworkDetails as NetworkInfo, SearchResult, Stats, Status, Top, Version, Volume as VolumeRep, VolumeCreateInfo, Volumes as VolumesRep, }, @@ -514,52 +514,15 @@ impl<'a> Container<'a> { Ok(()) } - async fn exec_create( - &self, - opts: &ExecContainerOptions, - ) -> Result<String> { - #[derive(serde::Deserialize)] - #[serde(rename_all = "PascalCase")] - struct Response { - id: String, - } - - let body: Body = opts.serialize()?.into(); - - let Response { id } = self - .docker - .post_json( - &format!("/containers/{}/exec", self.id)[..], - Some((body, mime::APPLICATION_JSON)), - ) - .await?; - - Ok(id) - } - - fn exec_start( - &self, - id: String, - ) -> impl Stream<Item = Result<tty::TtyChunk>> + 'a { - let bytes: &[u8] = b"{}"; - - let stream = Box::pin(self.docker.stream_post( - format!("/exec/{}/start", id), - Some((bytes.into(), mime::APPLICATION_JSON)), - None::<iter::Empty<_>>, - )); - - tty::decode(stream) - } - + /// Execute a command in this container pub fn exec( &'a self, opts: &'a ExecContainerOptions, ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'a { Box::pin( async move { - let id = self.exec_create(opts).await?; - Ok(self.exec_start(id)) + let id = Exec::create_id(&self.docker, &self.id, opts).await?; + Ok(Exec::_start(&self.docker, &id)) } .try_flatten_stream(), ) @@ -693,6 +656,131 @@ impl<'a> Containers<'a> { .await } } +/// Interface for docker exec instance +pub struct Exec<'a> { + docker: &'a Docker, + id: String, +} + +impl<'a> Exec<'a> { + fn new<S>( + docker: &'a Docker, + id: S, + ) -> Exec<'a> + where + S: Into<String>, + { + Exec { + docker, + id: id.into(), + } + } + + /// Creates an exec instance in docker and returns its id + pub(crate) async fn create_id( + docker: &'a Docker, + container_id: &str, + opts: &ExecContainerOptions, + ) -> Result<String> { + #[derive(serde::Deserialize)] + #[serde(rename_all = "PascalCase")] + struct Response { + id: String, + } + + let body: Body = opts.serialize()?.into(); + + docker + .post_json( + &format!("/containers/{}/exec", container_id)[..], + Some((body, mime::APPLICATION_JSON)), + ) + .await + .map(|resp: Response| resp.id) + } + + /// Starts an exec instance with id exec_id + pub(crate) fn _start( + docker: &'a Docker, + exec_id: &str, + ) -> impl Stream<Item = Result<tty::TtyChunk>> + 'a { + let bytes: &[u8] = b"{}"; + + let stream = Box::pin(docker.stream_post( + format!("/exec/{}/start", &exec_id), + Some((bytes.into(), mime::APPLICATION_JSON)), + None::<iter::Empty<_>>, + )); + + tty::decode(stream) + } + + /// Creates a new exec instance that will be executed in a container with id == container_id + pub async fn create( + docker: &'a Docker, + container_id: &str, + opts: &ExecContainerOptions, + ) -> Result<Exec<'a>> { + Ok(Exec::new( + docker, + Exec::create_id(docker, container_id, opts).await?, + )) + } + + /// Get a reference to a set of operations available to an already created exec instance. + /// + /// It's in callers responsibility to ensure that exec instance with specified id actually + /// exists. Use [Exec::create](Exec::create) to ensure that the exec instance is created + /// beforehand. + pub async fn get<S>( + docker: &'a Docker, + id: S, + ) -> Exec<'a> + where + S: Into<String>, + { + Exec::new(docker, id) + } + + /// Starts this exec instance returning a multiplexed tty stream + pub fn start(&'a self) -> impl Stream<Item = Result<tty::TtyChunk>> + 'a { + Box::pin( + async move { + let bytes: &[u8] = b"{}"; + + let stream = Box::pin(self.docker.stream_post( + format!("/exec/{}/start", &self.id), + Some((bytes.into(), mime::APPLICATION_JSON)), + None::<iter::Empty<_>>, + )); + + Ok(tty::decode(stream)) + } + .try_flatten_stream(), + ) + } + + /// Inspect this exec instance to aquire detailed information + pub async fn inspect(&self) -> Result<ExecDetails> { + self.docker + .get_json(&format!("/exec/{}/json", &self.id)[..]) + .await + } + + pub async fn resize( + &self, + opts: &ExecResizeOptions, + ) -> Result<()> { + let body: Body = opts.serialize()?.into(); + + self.docker + .post_json( + &format!("/exec/{}/resize", &self.id)[..], + Some((body, mime::APPLICATION_JSON)), + ) + .await + } +} /// Interface for docker network pub struct Networks<'a> { @@ -495,6 +495,33 @@ pub struct Event { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct ExecDetails { + pub can_remove: bool, + #[serde(rename = "ContainerID")] + pub container_id: String, + pub detach_keys: String, + pub exit_code: Option<u64>, + #[serde(rename = "ID")] + pub id: String, + pub open_stderr: bool, + pub open_stdin: bool, + pub open_stdout: bool, + pub process_config: ProcessConfig, + pub running: bool, + pub pid: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ProcessConfig { + pub arguments: Vec<String>, + pub entrypoint: String, + pub privileged: bool, + pub tty: bool, + pub user: Option<String>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Actor { #[serde(rename = "ID")] pub id: String, |