diff --git a/enclave_build/src/docker.rs b/enclave_build/src/docker.rs index 3eafcf12..04aa6c65 100644 --- a/enclave_build/src/docker.rs +++ b/enclave_build/src/docker.rs @@ -1,10 +1,11 @@ -// Copyright 2019-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2019-2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 use crate::docker::DockerError::CredentialsError; use base64::{engine::general_purpose, Engine as _}; use bollard::auth::DockerCredentials; use bollard::image::{BuildImageOptions, CreateImageOptions}; +use bollard::secret::ImageInspect; use bollard::Docker; use flate2::{write::GzEncoder, Compression}; use futures::stream::StreamExt; @@ -62,67 +63,73 @@ impl DockerUtil { }) } - /// Returns the credentials by reading ${HOME}/.docker/config.json or ${DOCKER_CONFIG} - /// - /// config.json doesn't seem to have a schema that we could use to validate - /// 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 { - let image = self.docker_image.clone(); - let host = if let Ok(uri) = Url::parse(&image) { + fn parse_docker_host(docker_image: &str) -> Option { + if let Ok(uri) = Url::parse(docker_image) { uri.host().map(|s| s.to_string()) } else { // Some Docker URIs don't have the protocol included, so just use // a dummy one to trick Url that it's a properly defined Uri. - let uri = format!("dummy://{image}"); + let uri = format!("dummy://{docker_image}"); if let Ok(uri) = Url::parse(&uri) { uri.host().map(|s| s.to_string()) } else { None } - }; - - if let Some(registry_domain) = host { - let config_file = self.get_config_file()?; + } + } - let config_json: serde_json::Value = serde_json::from_reader(&config_file) - .map_err(|err| CredentialsError(format!("JSON was not well-formatted: {err}")))?; + /// Returns the credentials by reading ${HOME}/.docker/config.json or ${DOCKER_CONFIG} + /// + /// config.json doesn't seem to have a schema that we could use to validate + /// 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 { + let host = match Self::parse_docker_host(&self.docker_image) { + Some(host) => host, + None => return Err(CredentialsError("Invalid docker image URI!".to_string())), + }; - let auths = config_json.get("auths").ok_or_else(|| { - CredentialsError("Could not find auths key in config JSON".to_string()) - })?; + let config_file = self.get_config_file()?; - if let Value::Object(auths) = auths { - for (registry_name, registry_auths) in auths.iter() { - if !registry_name.to_string().contains(®istry_domain) { - continue; - } + let config_json: serde_json::Value = serde_json::from_reader(&config_file) + .map_err(|err| CredentialsError(format!("JSON was not well-formatted: {err}")))?; - let auth = registry_auths - .get("auth") - .ok_or_else(|| { - CredentialsError("Could not find auth key in config JSON".to_string()) - })? - .to_string(); + let auths = config_json.get("auths").ok_or_else(|| { + CredentialsError("Could not find auths key in config JSON".to_string()) + })?; - let auth = auth.replace('"', ""); - let decoded = general_purpose::STANDARD.decode(auth).map_err(|err| { - CredentialsError(format!("Invalid Base64 encoding for auth: {err}")) - })?; - let decoded = std::str::from_utf8(&decoded).map_err(|err| { - CredentialsError(format!("Invalid utf8 encoding for auth: {err}")) - })?; + if let Value::Object(auths) = auths { + for (registry_name, registry_auths) in auths.iter() { + if !registry_name.to_string().contains(&host) { + continue; + } - if let Some(index) = decoded.rfind(':') { - let (user, after_user) = decoded.split_at(index); - let (_, password) = after_user.split_at(1); - return Ok(DockerCredentials { - username: Some(user.to_string()), - password: Some(password.to_string()), - ..Default::default() - }); - } + let auth = registry_auths + .get("auth") + .ok_or_else(|| { + CredentialsError("Could not find auth key in config JSON".to_string()) + })? + .to_string(); + + let auth = auth.replace('"', ""); + + let decoded = general_purpose::STANDARD.decode(auth).map_err(|err| { + CredentialsError(format!("Invalid Base64 encoding for auth: {err}")) + })?; + + let decoded = std::str::from_utf8(&decoded).map_err(|err| { + CredentialsError(format!("Invalid utf8 encoding for auth: {err}")) + })?; + + if let Some(index) = decoded.rfind(':') { + let (user, after_user) = decoded.split_at(index); + let (_, password) = after_user.split_at(1); + return Ok(DockerCredentials { + username: Some(user.to_string()), + password: Some(password.to_string()), + ..Default::default() + }); } } } @@ -166,14 +173,16 @@ impl DockerUtil { /// Pull the image, with the tag provided in constructor, from the Docker registry pub fn pull_image(&self) -> Result<(), DockerError> { - let act = async { - // Check if the Docker image is locally available. - // If available, early exit. - if self.docker.inspect_image(&self.docker_image).await.is_ok() { - eprintln!("Using the locally available Docker image..."); - return Ok(()); - } + // Check if the Docker image is locally available. + // If available, early exit. + if self.inspect().is_ok() { + eprintln!("Using the locally available Docker image..."); + return Ok(()); + } + + let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; + runtime.block_on(async { let create_image_options = CreateImageOptions { from_image: self.docker_image.clone(), ..Default::default() @@ -194,47 +203,48 @@ impl DockerUtil { self.docker .create_image(Some(create_image_options), None, credentials); - loop { - if let Some(item) = stream.next().await { - match item { - Ok(output) => { - if let Some(err_msg) = &output.error { - error!("{:?}", err_msg); - break Err(DockerError::PullError); - } else { - info!("{:?}", output); - } - } - Err(e) => { - error!("{:?}", e); - break Err(DockerError::PullError); + while let Some(item) = stream.next().await { + match item { + Ok(output) => { + if let Some(err_msg) = &output.error { + error!("{:?}", err_msg); + return Err(DockerError::PullError); + } else { + info!("{:?}", output); } } - } else { - break Ok(()); + Err(e) => { + error!("{:?}", e); + return Err(DockerError::PullError); + } } } - }; - - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - runtime.block_on(act) + Ok(()) + }) } - /// 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 mut archive = tar::Builder::new(GzEncoder::new(Vec::default(), Compression::best())); + fn build_tarball(dockerfile_dir: String) -> Result, DockerError> { + let encoder = GzEncoder::new(Vec::default(), Compression::best()); + let mut archive = tar::Builder::new(encoder); + 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| { + + archive.into_inner().and_then(|c| c.finish()).map_err(|e| { error!("{:?}", e); DockerError::BuildError - })?; + }) + } - let act = async move { + /// 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 runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; + + runtime.block_on(async move { let mut stream = self.docker.build_image( BuildImageOptions { dockerfile: "Dockerfile".to_string(), @@ -242,148 +252,73 @@ impl DockerUtil { ..Default::default() }, None, - Some(bytes.into()), + Some(Self::build_tarball(dockerfile_dir)?.into()), ); - loop { - if let Some(item) = stream.next().await { - match item { - Ok(output) => { - if let Some(err_msg) = &output.error { - error!("{:?}", err_msg.clone()); - break Err(DockerError::BuildError); - } else { - info!("{:?}", output); - } - } - Err(e) => { - error!("{:?}", e); - break Err(DockerError::BuildError); + while let Some(item) = stream.next().await { + match item { + Ok(output) => { + if let Some(err_msg) = &output.error { + error!("{:?}", err_msg.clone()); + return Err(DockerError::BuildError); + } else { + info!("{:?}", output); } } - } else { - break Ok(()); + Err(e) => { + error!("{:?}", e); + return Err(DockerError::BuildError); + } } } - }; - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - - runtime.block_on(act) + Ok(()) + }) } - /// Inspect docker image and return its description as a json String - pub fn inspect_image(&self) -> Result { - let act = async { - match self.docker.inspect_image(&self.docker_image).await { - Ok(image) => Ok(json!(image)), + fn inspect(&self) -> Result { + let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; + let image_future = self.docker.inspect_image(&self.docker_image); + + runtime.block_on(async { + match image_future.await { + Ok(image) => Ok(image), Err(e) => { error!("{:?}", e); Err(DockerError::InspectError) } } - }; + }) + } - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - runtime.block_on(act) + /// Inspect docker image and return its description as a json String + pub fn inspect_image(&self) -> Result { + match self.inspect() { + Ok(image) => Ok(json!(image)), + Err(e) => { + error!("{:?}", e); + Err(DockerError::InspectError) + } + } } fn extract_image(&self) -> Result<(Vec, Vec), DockerError> { // First try to find CMD parameters (together with potential ENV bindings) - let act_cmd = async { - 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.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) - } - } - }; + let image = self.inspect()?; + let config = image.config.ok_or(DockerError::UnsupportedEntryPoint)?; - let check_cmd_runtime = Runtime::new() - .map_err(|_| DockerError::RuntimeError)? - .block_on(act_cmd); - let check_env_runtime = Runtime::new() - .map_err(|_| DockerError::RuntimeError)? - .block_on(act_env); + if let Some(cmd) = &config.cmd { + let env = config.env.unwrap_or_default(); + return Ok((cmd.clone(), env)); + } // 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.inspect_image(&self.docker_image).await { - Ok(image) => image - .config - .and_then(|c| c.entrypoint) - .ok_or(DockerError::UnsupportedEntryPoint), - Err(e) => { - error!("{:?}", e); - Err(DockerError::InspectError) - } - } - }; - - let check_entrypoint_runtime = Runtime::new() - .map_err(|_| DockerError::RuntimeError)? - .block_on(act_entrypoint); - - if check_entrypoint_runtime.is_err() { - return Err(DockerError::UnsupportedEntryPoint); - } - - let act = async { - match self.docker.inspect_image(&self.docker_image).await { - Ok(image) => Ok(( - image.config.clone().unwrap().entrypoint.unwrap(), - image - .config - .unwrap() - .env - .ok_or_else(Vec::::new) - .unwrap(), - )), - Err(e) => { - error!("{:?}", e); - Err(DockerError::InspectError) - } - } - }; - - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - - return runtime.block_on(act); + if let Some(entrypoint) = &config.entrypoint { + let env = config.env.unwrap_or_default(); + return Ok((entrypoint.clone(), env)); } - let act = async { - 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) - } - } - }; - - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - - runtime.block_on(act) + Err(DockerError::UnsupportedEntryPoint) } /// The main function of this struct. This needs to be called in order to @@ -400,19 +335,8 @@ impl DockerUtil { /// Fetch architecture information from an image pub fn architecture(&self) -> Result { - let arch = async { - match self.docker.inspect_image(&self.docker_image).await { - Ok(image) => Ok(image.architecture.unwrap_or_default()), - Err(e) => { - error!("{:?}", e); - Err(DockerError::InspectError) - } - } - }; - - let runtime = Runtime::new().map_err(|_| DockerError::RuntimeError)?; - - runtime.block_on(arch) + let image = self.inspect()?; + Ok(image.architecture.unwrap_or_default()) } }