Add missing SHELL build-arg

Change-Id: Ic645c0411928f8b69c4d5b21a90462170576e7bc
diff --git a/crates/rustainers/src/lib.rs b/crates/rustainers/src/lib.rs
new file mode 100644
index 0000000..f5d224f
--- /dev/null
+++ b/crates/rustainers/src/lib.rs
@@ -0,0 +1,244 @@
+extern crate tar;
+
+use bollard::container::{Config, CreateContainerOptions, StartContainerOptions};
+use bollard::exec::{CreateExecOptions, StartExecResults};
+use bollard::Docker;
+use bytes::{BufMut, BytesMut};
+use futures_util::stream::StreamExt;
+use futures_util::TryStreamExt;
+use passwd::PasswdEntry;
+use rand::Rng;
+use std::collections::HashMap;
+use std::io::Read;
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum DockerContainerGuardError {
+    #[error("Docker API error: {0}")]
+    DockerError(#[from] bollard::errors::Error),
+
+    #[error("File not found: {0}")]
+    FileNotFound(String),
+
+    #[error("Failed to extract file: {0}")]
+    FileExtractionFailed(#[from] std::io::Error),
+
+    #[error("Too many files in tarball")]
+    TooManyFilesInTarball,
+
+    #[error("Failed to parse password entry: {0}")]
+    FailedToParsePasswdEntry(#[from] passwd::PasswdEntryError),
+
+    #[error("User not found: {0}")]
+    UserNotFound(String),
+}
+
+#[derive(Debug)]
+pub struct DockerContainerGuard {
+    pub id: String,
+    pub image: String,
+    docker: Docker,
+}
+
+impl DockerContainerGuard {
+    // Spawns a new container using Bollard.
+    //
+    // The container is automatically cleaned up when the guard goes out of scope.
+    pub async fn spawn(image_name: &str) -> Result<Self, DockerContainerGuardError> {
+        let docker = Docker::connect_with_local_defaults()?;
+
+        let container_name: String = rand::thread_rng()
+            .sample_iter(&rand::distributions::Alphanumeric)
+            .take(10)
+            .map(char::from)
+            .collect();
+
+        docker
+            .create_image(
+                Some(bollard::image::CreateImageOptions {
+                    from_image: image_name,
+                    ..Default::default()
+                }),
+                None,
+                None,
+            )
+            .try_collect::<Vec<_>>()
+            .await?;
+
+        let container = docker
+            .create_container(
+                Some(CreateContainerOptions {
+                    name: container_name,
+                    ..Default::default()
+                }),
+                Config {
+                    image: Some(image_name),
+                    cmd: Some(vec!["sh"]),
+                    tty: Some(true),
+                    ..Default::default()
+                },
+            )
+            .await?;
+
+        docker
+            .start_container(&container.id, None::<StartContainerOptions<String>>)
+            .await?;
+
+        Ok(Self {
+            id: container.id,
+            image: image_name.to_string(),
+            docker,
+        })
+    }
+
+    /// Executes a command inside the container using Bollard.
+    ///
+    /// Returns the output as a String.
+    pub async fn exec(&self, cmd: Vec<&str>) -> Result<String, bollard::errors::Error> {
+        let exec_instance = self
+            .docker
+            .create_exec(
+                &self.id,
+                CreateExecOptions {
+                    attach_stdout: Some(true),
+                    attach_stderr: Some(true),
+                    cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
+                    ..Default::default()
+                },
+            )
+            .await?;
+        let start_exec_result = self.docker.start_exec(&exec_instance.id, None).await?;
+
+        if let StartExecResults::Attached {
+            output: out_stream, ..
+        } = start_exec_result
+        {
+            let output = out_stream
+                .filter_map(|chunk| async {
+                    match chunk {
+                        Ok(bollard::container::LogOutput::StdOut { message })
+                        | Ok(bollard::container::LogOutput::StdErr { message }) => {
+                            Some(String::from_utf8_lossy(&message).to_string())
+                        }
+                        _ => None,
+                    }
+                })
+                .fold(String::new(), |mut acc, item| async move {
+                    acc.push_str(&item);
+                    acc
+                })
+                .await;
+
+            return Ok(output);
+        }
+
+        Ok(String::new())
+    }
+
+    // Read a file from the container.
+    pub async fn read_file(&self, path: &str) -> Result<String, DockerContainerGuardError> {
+        let bytes = self
+            .docker
+            .download_from_container::<String>(
+                &self.id,
+                Some(bollard::container::DownloadFromContainerOptions { path: path.into() }),
+            )
+            .try_fold(BytesMut::new(), |mut bytes, b| async move {
+                bytes.put(b);
+                Ok(bytes)
+            })
+            .await?;
+
+        if bytes.len() == 0 {
+            return Err(DockerContainerGuardError::FileNotFound(path.into()));
+        }
+
+        for file in tar::Archive::new(&bytes[..]).entries()? {
+            let mut s = String::new();
+            file?.read_to_string(&mut s).unwrap();
+            return Ok(s);
+        }
+
+        Err(DockerContainerGuardError::FileNotFound(path.into()))
+    }
+
+    // Get a HashMap of all users in the container.
+    pub async fn get_users(
+        &self,
+    ) -> Result<HashMap<String, PasswdEntry>, DockerContainerGuardError> {
+        let output = self.read_file("/etc/passwd").await?;
+
+        return output
+            .lines()
+            .map(|line| {
+                PasswdEntry::from_line(line)
+                    .map(|entry| (entry.name.clone(), entry))
+                    .map_err(DockerContainerGuardError::from)
+            })
+            .collect();
+    }
+
+    // Get a specific user from the container.
+    pub async fn get_user(&self, name: &str) -> Result<PasswdEntry, DockerContainerGuardError> {
+        let users = self.get_users().await?;
+        let user = users
+            .get(name)
+            .ok_or_else(|| DockerContainerGuardError::UserNotFound(name.into()))?;
+
+        Ok(user.clone())
+    }
+}
+
+impl Drop for DockerContainerGuard {
+    fn drop(&mut self) {
+        let docker = self.docker.clone();
+        let container_id = self.id.clone();
+
+        tokio::spawn(async move {
+            docker
+                .remove_container(
+                    &container_id,
+                    Some(bollard::container::RemoveContainerOptions {
+                        force: true,
+                        ..Default::default()
+                    }),
+                )
+                .await
+        });
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[tokio::test]
+    async fn test_container_exec() -> Result<(), DockerContainerGuardError> {
+        let guard = DockerContainerGuard::spawn("alpine:latest").await?;
+
+        let output = guard.exec(vec!["echo", "hello from container"]).await?;
+        assert!(output.contains("hello from container"));
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    async fn test_container_read_file() -> Result<(), DockerContainerGuardError> {
+        let guard = DockerContainerGuard::spawn("alpine:latest").await?;
+
+        let file = guard.read_file("/usr/lib/os-release").await?;
+        assert!(file.len() > 0);
+
+        Ok(())
+    }
+
+    #[tokio::test]
+    async fn test_container_get_user() -> Result<(), DockerContainerGuardError> {
+        let guard = DockerContainerGuard::spawn("alpine:latest").await?;
+
+        let user = guard.get_user("root").await?;
+        assert_eq!(user.name, "root");
+
+        Ok(())
+    }
+}