Skip to content

Commit

Permalink
enclave_build: add OCI image manager
Browse files Browse the repository at this point in the history
Implement an OCI manager that is able to extract the required information
(ENV, CMD, ENTRYPOINT) for the EIF build. The image details are fetched
either from the local storage or pulled from the registry. The failover
logic makes sure that the image details are returned even if they are
not stored or the storage is missing/corrupt. The manager creation
succedes only if the image details can be fetched from either source.

Added unit tests for fetching image details based on present or missing
storage and for image manager creation.

Signed-off-by: Calin-Alexandru Coman <[email protected]>
Signed-off-by: Raul-Ovidiu Moldovan <[email protected]>
  • Loading branch information
raulmldv committed Apr 5, 2023
1 parent 5d8a455 commit 3dad5a0
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 3 deletions.
2 changes: 1 addition & 1 deletion enclave_build/src/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ impl DockerUtil {
}
}

fn write_config(config: Vec<String>) -> Result<NamedTempFile> {
pub fn write_config(config: Vec<String>) -> Result<NamedTempFile> {
let mut file = NamedTempFile::new().map_err(|_| EnclaveBuildError::ConfigError)?;

for line in config {
Expand Down
4 changes: 4 additions & 0 deletions enclave_build/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ impl ImageDetails {
config: deserialize_from_reader(image_data.config.data.as_slice())?,
})
}

pub fn config(&self) -> &ImageConfiguration {
&self.config
}
}

/// URIs that are missing a domain will be converted to a reference using the Docker defaults.
Expand Down
180 changes: 180 additions & 0 deletions enclave_build/src/image_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![allow(dead_code)]

use std::convert::TryFrom;

use oci_distribution::Reference;

use crate::image::ImageDetails;
use crate::storage::OciStorage;
use crate::{EnclaveBuildError, Result};

pub struct OciImageManager {
/// Name of the container image.
image_name: String,
/// Image details needed for inspect and extract commands operations
image_details: ImageDetails,
}

impl OciImageManager {
/// When calling this constructor, it also tries to initialize the storage at the default path.
/// If this fails, the ImageManager is still created, but the 'storage' field is set to 'None'.
pub async fn new(image_name: &str) -> Result<Self> {
// Add the default ":latest" tag if the image tag is missing
let image_name = normalize_tag(image_name)?;

// The docker daemon is not used, so a local storage needs to be created
let storage =
match OciStorage::get_default_root_path().map_err(|err| eprintln!("{:?}", err)) {
Ok(root_path) => {
// Try to create/read the storage. If the storage could not be created, log the error
OciStorage::new(&root_path)
.map_err(|err| eprintln!("{:?}", err))
.ok()
}
Err(_) => None,
};

let image_details = Self::fetch_image_details(&image_name, storage).await?;

Ok(Self {
image_name,
image_details,
})
}

/// Returns a struct containing image metadata.
///
/// If the image is stored correctly, the function tries to fetch the image from the storage.
///
/// If the image is not stored or a storage was not created (the 'storage' field is None),
/// it pulls the image, stores it (if the 'storage' field is not None) and returns its metadata.
///
/// If the pull succeeded but the store operation failed, it returns the pulled image metadata.
async fn fetch_image_details(
image_name: &str,
mut storage: Option<OciStorage>,
) -> Result<ImageDetails> {
let local_storage = storage.as_mut();

let image_details = if let Some(storage) = local_storage {
// Try to fetch the image from the storage
storage.fetch_image_details(image_name).map_err(|err| {
// Log the fetching error
eprintln!("{:?}", err);
err
})
} else {
Err(EnclaveBuildError::OciStorageNotFound(
"Local storage missing".to_string(),
))
};

// If the fetching failed, pull it from remote and store it
match image_details {
Ok(details) => Ok(details),
Err(_) => {
// The image is not stored, so try to pull and then store it
let image_data = crate::pull::pull_image_data(image_name).await?;

// If the store operation fails, discard error and proceed with getting the details
if let Some(local_storage) = storage.as_mut() {
local_storage
.store_image_data(image_name, &image_data)
.map_err(|err| eprintln!("Failed to store image: {:?}", err))
.ok();
}

// Get the image metadata from the pulled struct
ImageDetails::build_details(image_name, &image_data)
}
}
}
}

/// Adds the default ":latest" tag to an image if it is untagged
fn normalize_tag(image_name: &str) -> Result<String> {
let image_ref = Reference::try_from(image_name).map_err(|err| {
EnclaveBuildError::ImageBuildError(format!("Invalid image name format: {}", err))
})?;

match image_ref.tag() {
Some(_) => Ok(image_name.to_string()),
None => Ok(format!("{}:latest", image_name)),
}
}

#[cfg(test)]
pub mod tests {
use sha2::Digest;

use super::{normalize_tag, OciImageManager};

#[cfg(target_arch = "x86_64")]
const SAMPLE_IMAGE: &str =
"667861386598.dkr.ecr.us-east-1.amazonaws.com/enclaves-samples:vsock-sample-server-x86_64";
#[cfg(target_arch = "aarch64")]
const SAMPLE_IMAGE: &str =
"667861386598.dkr.ecr.us-east-1.amazonaws.com/enclaves-samples:vsock-sample-server-aarch64";
#[cfg(target_arch = "x86_64")]
const IMAGE_HASH: &str =
"sha256:03e42b437a0d900e2c6e2f7f4b65d818adfea6dbadfaad30027af42a68c5c183";
#[cfg(target_arch = "aarch64")]
const IMAGE_HASH: &str =
"sha256:1405e46c329b17bf4bb6eb9ff97d2a6085a8055948e9ffeb4e3227ea6b024e39";

#[tokio::test]
async fn test_fetch_storage_missing() {
let image_details =
OciImageManager::fetch_image_details(&normalize_tag(SAMPLE_IMAGE).unwrap(), None)
.await
.unwrap();

let config_string = image_details.config().to_string().unwrap();
let config_hash = format!(
"sha256:{:x}",
sha2::Sha256::digest(config_string.as_bytes())
);

assert_eq!(&config_hash, IMAGE_HASH);
}

#[tokio::test]
async fn test_fetch_from_storage() {
let (_root_path, storage) = crate::storage::tests::setup_temp_storage();

let image_details = OciImageManager::fetch_image_details(
&normalize_tag(crate::image::tests::TEST_IMAGE_NAME).unwrap(),
Some(storage),
)
.await
.unwrap();

let config_string = image_details.config().to_string().unwrap();
let config_hash = format!(
"sha256:{:x}",
sha2::Sha256::digest(config_string.as_bytes())
);

assert_eq!(
config_hash,
"sha256:44445ae0eab6eead16f7546a10ee41eb2869145ca9260d78700fba095da646b7"
);
}

#[tokio::test]
async fn test_create_manager() {
let image_manager = OciImageManager::new(SAMPLE_IMAGE).await.unwrap();

assert_eq!(image_manager.image_name, SAMPLE_IMAGE);

let config_string = image_manager.image_details.config().to_string().unwrap();
let config_hash = format!(
"sha256:{:x}",
sha2::Sha256::digest(config_string.as_bytes())
);

assert_eq!(&config_hash, IMAGE_HASH);
}
}
1 change: 1 addition & 0 deletions enclave_build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::process::Command;

mod docker;
mod image;
mod image_manager;
mod pull;
mod storage;
mod yaml_generator;
Expand Down
3 changes: 1 addition & 2 deletions enclave_build/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#![allow(dead_code)]
use std::{
collections::HashMap,
fs::{self, File},
Expand Down Expand Up @@ -488,7 +487,7 @@ pub mod tests {

/// This function stores the test image in a temporary directory and returns that directory and
/// the storage manager initialized with it as root path.
fn setup_temp_storage() -> (TempDir, OciStorage) {
pub fn setup_temp_storage() -> (TempDir, OciStorage) {
// Use a temporary dir as the storage root path
// Create temporary random path so tests running in parallel won't overlap
let root_dir = TempDir::new().unwrap();
Expand Down

0 comments on commit 3dad5a0

Please sign in to comment.