Skip to content

Commit

Permalink
fix: Switch to bollard for docker API interaction
Browse files Browse the repository at this point in the history
The `shiplift` library which is currently used to interact with the
docker API is unmaintained. Switch to `bollard` which is an actively
maintained and fairly popular library.

This fixes #537
  • Loading branch information
jalaziz committed Mar 19, 2024
1 parent 79a2521 commit 3563d1d
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 299 deletions.
624 changes: 389 additions & 235 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "1.2.3"
authors = ["The AWS Nitro Enclaves Team <[email protected]>"]
edition = "2018"
license = "Apache-2.0"
rust-version = "1.60"
rust-version = "1.65"

[dependencies]
serde = { version = ">=1.0", features = ["derive"] }
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ TOOLCHAIN_ARCH_TARGET = $(ARCH_x86_64)
else ifeq ($(HOST_MACHINE),$(ARCH_aarch64))
TOOLCHAIN_ARCH_TARGET = $(ARCH_aarch64)
CC = musl-gcc # Required for openssl-sys cross-build
else ifeq ($(HOST_MACHINE),arm64)
TOOLCHAIN_ARCH_TARGET = $(ARCH_aarch64)
CC = musl-gcc # Required for openssl-sys cross-build
else
TOOLCHAIN_ARCH_TARGET =
endif
Expand Down
4 changes: 3 additions & 1 deletion enclave_build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bollard = "0.16.0"
clap = "3.2"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
serde_json = "1.0"
shiplift = "0.7"
tempfile = "3.5.0"
tokio = { version = "1.27", features = ["rt-multi-thread"] }
base64 = "0.21"
Expand All @@ -22,3 +22,5 @@ sha2 = "0.9.5"
futures = "0.3.28"

aws-nitro-enclaves-image-format = "0.2"
tar = "0.4.40"
flate2 = "1.0.28"
143 changes: 86 additions & 57 deletions enclave_build/src/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

use crate::docker::DockerError::CredentialsError;
use base64::{engine::general_purpose, Engine as _};
use bollard::auth::DockerCredentials;
use bollard::image::{BuildImageOptions, CreateImageOptions};
use bollard::Docker;
use flate2::{write::GzEncoder, Compression};
use futures::stream::StreamExt;
use log::{debug, error, info};
use serde_json::{json, Value};
use shiplift::RegistryAuth;
use shiplift::{BuildOptions, Docker, PullOptions};
use std::fs::File;
use std::io::Write;
use std::path::Path;
Expand All @@ -21,6 +23,7 @@ pub const DOCKER_ARCH_AMD64: &str = "amd64";

#[derive(Debug, PartialEq, Eq)]
pub enum DockerError {
ConnectionError,
BuildError,
InspectError,
PullError,
Expand All @@ -38,20 +41,25 @@ pub struct DockerUtil {

impl DockerUtil {
/// Constructor that takes as argument a tag for the docker image to be used
pub fn new(docker_image: String) -> Self {
pub fn new(docker_image: String) -> Result<Self, DockerError> {
let mut docker_image = docker_image;

if !docker_image.contains(':') {
docker_image.push_str(":latest");
}

DockerUtil {
// DOCKER_HOST environment variable is parsed inside
// if docker daemon address needs to be substituted.
// By default it tries to connect to 'unix:///var/run/docker.sock'
docker: Docker::new(),
// DOCKER_HOST environment variable is parsed inside
// if docker daemon address needs to be substituted.
// By default, it tries to connect to 'unix:///var/run/docker.sock'
let docker = Docker::connect_with_defaults().map_err(|e| {
error!("{:?}", e);
DockerError::ConnectionError
})?;

Ok(DockerUtil {
docker,
docker_image,
}
})
}

/// Returns the credentials by reading ${HOME}/.docker/config.json or ${DOCKER_CONFIG}
Expand All @@ -60,7 +68,7 @@ impl DockerUtil {
/// we are parsing it correctly, so the parsing mechanism had been infered by
/// reading a config.json created by:
// Docker version 19.03.2
fn get_credentials(&self) -> Result<RegistryAuth, DockerError> {
fn get_credentials(&self) -> Result<DockerCredentials, DockerError> {
let image = self.docker_image.clone();
let host = if let Ok(uri) = Url::parse(&image) {
uri.host().map(|s| s.to_string())
Expand Down Expand Up @@ -109,10 +117,11 @@ impl DockerUtil {
if let Some(index) = decoded.rfind(':') {
let (user, after_user) = decoded.split_at(index);
let (_, password) = after_user.split_at(1);
return Ok(RegistryAuth::builder()
.username(user)
.password(password)
.build());
return Ok(DockerCredentials {
username: Some(user.to_string()),
password: Some(password.to_string()),
..Default::default()
});
}
}
}
Expand Down Expand Up @@ -160,46 +169,40 @@ impl DockerUtil {
let act = async {
// Check if the Docker image is locally available.
// If available, early exit.
if self
.docker
.images()
.get(&self.docker_image)
.inspect()
.await
.is_ok()
{
if self.docker.inspect_image(&self.docker_image).await.is_ok() {
eprintln!("Using the locally available Docker image...");
return Ok(());
}

let mut pull_options_builder = PullOptions::builder();
pull_options_builder.image(&self.docker_image);
let create_image_options = CreateImageOptions {
from_image: self.docker_image.clone(),
..Default::default()
};

match self.get_credentials() {
Ok(auth) => {
pull_options_builder.auth(auth);
}
let credentials = match self.get_credentials() {
Ok(auth) => Some(auth),
// It is not mandatory to have the credentials set, but this is
// the most likely reason for failure when pulling, so log the
// error.
Err(err) => {
debug!("WARNING!! Credential could not be set {:?}", err);
None
}
};

let mut stream = self.docker.images().pull(&pull_options_builder.build());
let mut stream =
self.docker
.create_image(Some(create_image_options), None, credentials);

loop {
if let Some(item) = stream.next().await {
match item {
Ok(output) => {
let msg = &output;

if let Some(err_msg) = msg.get("error") {
error!("{:?}", err_msg.clone());
if let Some(err_msg) = &output.error {
error!("{:?}", err_msg);
break Err(DockerError::PullError);
} else {
info!("{}", msg);
info!("{:?}", output);
}
}
Err(e) => {
Expand All @@ -221,24 +224,36 @@ impl DockerUtil {
/// Build an image locally, with the tag provided in constructor, using a
/// directory that contains a Dockerfile
pub fn build_image(&self, dockerfile_dir: String) -> Result<(), DockerError> {
let act = async {
let mut stream = self.docker.images().build(
&BuildOptions::builder(dockerfile_dir)
.tag(self.docker_image.clone())
.build(),
let mut archive = tar::Builder::new(GzEncoder::new(Vec::default(), Compression::best()));
archive.append_dir_all(".", &dockerfile_dir).map_err(|e| {
error!("{:?}", e);
DockerError::BuildError
})?;
let bytes = archive.into_inner().and_then(|c| c.finish()).map_err(|e| {
error!("{:?}", e);
DockerError::BuildError
})?;

let act = async move {
let mut stream = self.docker.build_image(
BuildImageOptions {
dockerfile: "Dockerfile".to_string(),
t: self.docker_image.clone(),
..Default::default()
},
None,
Some(bytes.into()),
);

loop {
if let Some(item) = stream.next().await {
match item {
Ok(output) => {
let msg = &output;

if let Some(err_msg) = msg.get("error") {
if let Some(err_msg) = &output.error {
error!("{:?}", err_msg.clone());
break Err(DockerError::BuildError);
} else {
info!("{}", msg);
info!("{:?}", output);
}
}
Err(e) => {
Expand All @@ -260,7 +275,7 @@ impl DockerUtil {
/// Inspect docker image and return its description as a json String
pub fn inspect_image(&self) -> Result<serde_json::Value, DockerError> {
let act = async {
match self.docker.images().get(&self.docker_image).inspect().await {
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => Ok(json!(image)),
Err(e) => {
error!("{:?}", e);
Expand All @@ -276,17 +291,23 @@ impl DockerUtil {
fn extract_image(&self) -> Result<(Vec<String>, Vec<String>), DockerError> {
// First try to find CMD parameters (together with potential ENV bindings)
let act_cmd = async {
match self.docker.images().get(&self.docker_image).inspect().await {
Ok(image) => image.config.cmd.ok_or(DockerError::UnsupportedEntryPoint),
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => image
.config
.and_then(|c| c.cmd)
.ok_or(DockerError::UnsupportedEntryPoint),
Err(e) => {
error!("{:?}", e);
Err(DockerError::InspectError)
}
}
};
let act_env = async {
match self.docker.images().get(&self.docker_image).inspect().await {
Ok(image) => image.config.env.ok_or(DockerError::UnsupportedEntryPoint),
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => image
.config
.and_then(|c| c.env)
.ok_or(DockerError::UnsupportedEntryPoint),
Err(e) => {
error!("{:?}", e);
Err(DockerError::InspectError)
Expand All @@ -304,10 +325,10 @@ impl DockerUtil {
// If no CMD instructions are found, try to locate an ENTRYPOINT command
if check_cmd_runtime.is_err() || check_env_runtime.is_err() {
let act_entrypoint = async {
match self.docker.images().get(&self.docker_image).inspect().await {
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => image
.config
.entrypoint
.and_then(|c| c.entrypoint)
.ok_or(DockerError::UnsupportedEntryPoint),
Err(e) => {
error!("{:?}", e);
Expand All @@ -325,10 +346,15 @@ impl DockerUtil {
}

let act = async {
match self.docker.images().get(&self.docker_image).inspect().await {
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => Ok((
image.config.entrypoint.unwrap(),
image.config.env.ok_or_else(Vec::<String>::new).unwrap(),
image.config.clone().unwrap().entrypoint.unwrap(),
image
.config
.unwrap()
.env
.ok_or_else(Vec::<String>::new)
.unwrap(),
)),
Err(e) => {
error!("{:?}", e);
Expand All @@ -343,8 +369,11 @@ impl DockerUtil {
}

let act = async {
match self.docker.images().get(&self.docker_image).inspect().await {
Ok(image) => Ok((image.config.cmd.unwrap(), image.config.env.unwrap())),
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => Ok((
image.config.clone().unwrap().cmd.unwrap(),
image.config.unwrap().env.unwrap(),
)),
Err(e) => {
error!("{:?}", e);
Err(DockerError::InspectError)
Expand Down Expand Up @@ -372,8 +401,8 @@ impl DockerUtil {
/// Fetch architecture information from an image
pub fn architecture(&self) -> Result<String, DockerError> {
let arch = async {
match self.docker.images().get(&self.docker_image).inspect().await {
Ok(image) => Ok(image.architecture),
match self.docker.inspect_image(&self.docker_image).await {
Ok(image) => Ok(image.architecture.unwrap_or_default()),
Err(e) => {
error!("{:?}", e);
Err(DockerError::InspectError)
Expand Down
5 changes: 4 additions & 1 deletion enclave_build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ impl<'a> Docker2Eif<'a> {
metadata_path: Option<String>,
build_info: EifBuildInfo,
) -> Result<Self, Docker2EifError> {
let docker = DockerUtil::new(docker_image.clone());
let docker = DockerUtil::new(docker_image.clone()).map_err(|e| {
eprintln!("Docker error: {e:?}");
Docker2EifError::DockerError
})?;

if !Path::new(&init_path).is_file() {
return Err(Docker2EifError::InitPathError);
Expand Down
2 changes: 1 addition & 1 deletion src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub enum EnclaveProcessReply {
}

/// Struct that is passed along the backtrace and accumulates error messages.
#[derive(Debug, Default, PartialEq)]
#[derive(Debug, Default, PartialEq, Eq)]
pub struct NitroCliFailure {
/// Main action which was attempted and failed.
pub action: String,
Expand Down
6 changes: 3 additions & 3 deletions tools/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ RUN cd /tmp/openssl_src/openssl-${OPENSSL_VERSION} && \
make install_sw

# Setup the right rust ver
ENV RUST_VERSION=1.60.0
ENV RUST_VERSION=1.65.0
RUN source $HOME/.cargo/env && \
ARCH=$(uname -m) && \
rustup toolchain install ${RUST_VERSION}-${ARCH}-unknown-linux-gnu && \
rustup default ${RUST_VERSION}-${ARCH}-unknown-linux-gnu && \
rustup target add --toolchain ${RUST_VERSION} ${ARCH}-unknown-linux-musl && \
cargo install cargo-audit --version 0.16.0 --locked && \
cargo install cargo-about --version 0.5.1 --locked
cargo install cargo-audit --version 0.17.6 --locked && \
cargo install cargo-about --version 0.5.6 --locked

# Install docker for nitro-cli build-enclave runs
RUN apt-get update && \
Expand Down

0 comments on commit 3563d1d

Please sign in to comment.