Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/access_handlers/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub async fn check(profile_name: &str) -> Result<()> {
let registry_config = &get_config()?.registry;

let client = docker().await?;

// build test image string
// registry.example.com/somerepo/testimage:pleaseignore
let test_image = format!("{}/credstestimage", registry_config.domain);
Expand Down
35 changes: 1 addition & 34 deletions src/access_handlers/s3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use s3;
use simplelog::*;
use tokio;

use crate::clients::{bucket_client, bucket_client_anonymous};
use crate::configparser::{
config::{ProfileConfig, S3Config},
get_config, get_profile_config,
Expand Down Expand Up @@ -60,37 +61,3 @@ pub async fn check(profile_name: &str) -> Result<()> {

Ok(())
}

/// create bucket client for passed profile config
pub fn bucket_client(config: &S3Config) -> Result<Box<s3::Bucket>> {
trace!("creating bucket client");
// TODO: once_cell this so it reuses the same bucket?
let region = s3::Region::Custom {
region: config.region.clone(),
endpoint: config.endpoint.clone(),
};
let creds = s3::creds::Credentials::new(
Some(&config.access_key),
Some(&config.secret_key),
None,
None,
None,
)?;
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();

Ok(bucket)
}

/// create public/anonymous bucket client for passed profile config
pub fn bucket_client_anonymous(config: &S3Config) -> Result<Box<s3::Bucket>> {
trace!("creating anon bucket client");
// TODO: once_cell this so it reuses the same bucket?
let region = s3::Region::Custom {
region: config.region.clone(),
endpoint: config.endpoint.clone(),
};
let creds = s3::creds::Credentials::anonymous()?;
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();

Ok(bucket)
}
181 changes: 139 additions & 42 deletions src/builder/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,144 @@ use tempfile::tempdir_in;
use zip;

use crate::builder::docker;
use crate::clients::docker;
use crate::configparser::challenge::{ChallengeConfig, ProvideConfig};

/// extract assets from given container name and provide config to challenge directory, return file path(s) extracted
/// extract assets from provide config and possible container to challenge directory, return file path(s) extracted
#[tokio::main(flavor = "current_thread")] // make this a sync function
pub async fn extract_asset(
chal: &ChallengeConfig,
provide: &ProvideConfig,
container: &docker::ContainerInfo,
profile_name: &str,
) -> Result<Vec<PathBuf>> {
debug!("extracting assets from container {}", &container.name);
// This needs to handle three cases:
// - single or multiple files without renaming (no as: field)
// - single file with rename (one item with as:)
// - multiple files as archive (multiple items with as:)
// This needs to handle three cases * 2 sources:
// - single or multiple files without renaming (no as: field)
// - single file with rename (one item with as:)
// - multiple files as archive (multiple items with as:)
// and whether the file is coming from
// - the repo
// - or a container

// TODO: since this puts artifacts in the repo source folder, this should
// try to not overwrite any existing files.

match &provide.as_file {
// no renaming, copy out all as-is
None => extract_files(chal, container, &provide.include).await,
// (as is keyword, so add underscore)
Some(as_) => {
if provide.include.len() == 1 {
// single file, rename
extract_rename(chal, container, &provide.include[0], as_).await
} else {
// multiple files, zip as archive
extract_archive(chal, container, &provide.include, as_).await
}
debug!(
"extracting assets for challenge {:?} provide {:?}",
chal.directory, &provide
);

let docker = docker().await?;

match provide {
// Repo file paths are relative to the challenge directory, so prepend chal dir

// No action necessary, return path as-is
ProvideConfig::FromRepo { files } => {
Ok(files.iter().map(|f| chal.directory.join(f)).collect_vec())
}
ProvideConfig::FromRepoRename { from, to } => {
std::fs::copy(chal.directory.join(from), chal.directory.join(to))
.with_context(|| format!("could not copy repo file {from:?} to {to:?}"))?;
Ok(vec![to.clone()])
}
ProvideConfig::FromRepoArchive {
files,
archive_name,
} => {
zip_files(
&chal.directory.join(archive_name),
&files.iter().map(|f| chal.directory.join(f)).collect_vec(),
)
.with_context(|| format!("could not create archive {archive_name:?}"))?;
Ok(vec![archive_name.clone()])
}

ProvideConfig::FromContainer {
container: container_name,
files,
} => {
let tag = chal.container_tag_for_pod(profile_name, container_name)?;

let name = format!(
"asset-container-{}-{}",
chal.directory.to_string_lossy().replace("/", "-"),
container_name
);
let container = docker::create_container(&tag, &name).await?;

let files = extract_files(chal, &container, files).await;

docker::remove_container(container).await?;

files
}
.with_context(|| format!("could not copy files {files:?} from container {container_name}")),

ProvideConfig::FromContainerRename {
container: container_name,
from,
to,
} => {
let tag = chal.container_tag_for_pod(profile_name, container_name)?;

let name = format!(
"asset-container-{}-{}",
chal.directory.to_string_lossy().replace("/", "-"),
container_name
);
let container = docker::create_container(&tag, &name).await?;

let files = extract_rename(chal, &container, from, &chal.directory.join(to)).await;

docker::remove_container(container).await?;

files
}
.with_context(|| format!("could not copy file {from:?} from container {container_name}")),

ProvideConfig::FromContainerArchive {
container: container_name,
files,
archive_name,
} => {
let tag = chal.container_tag_for_pod(profile_name, container_name)?;

let name = format!(
"asset-container-{}-{}",
chal.directory.to_string_lossy().replace("/", "-"),
container_name
);
let container = docker::create_container(&tag, &name).await?;

let files =
extract_archive(chal, &container, files, &chal.directory.join(archive_name)).await;

docker::remove_container(container).await?;

files
}
.with_context(|| {
format!("could not create archive {archive_name:?} from container {container_name}")
}),
}
}

/// Extract multiple files from container
async fn extract_files(
chal: &ChallengeConfig,
container: &docker::ContainerInfo,
files: &Vec<String>,
files: &[PathBuf],
) -> Result<Vec<PathBuf>> {
debug!(
"extracting {} files without renaming: {:?}",
files.len(),
files
);

try_join_all(files.iter().map(|f| {
let from = PathBuf::from(f);
try_join_all(files.iter().map(|from| async {
// use basename of source file as target name
let to = chal.directory.join(from.file_name().unwrap());

docker::copy_file(container, from, to)
docker::copy_file(container, from, &to).await
}))
.await
}
Expand All @@ -70,13 +156,12 @@ async fn extract_files(
async fn extract_rename(
chal: &ChallengeConfig,
container: &docker::ContainerInfo,
file: &str,
new_name: &str,
file: &Path,
new_name: &Path,
) -> Result<Vec<PathBuf>> {
debug!("extracting file {:?} renamed to {:?}", file, new_name);

let new_file =
docker::copy_file(container, PathBuf::from(file), PathBuf::from(new_name)).await?;
let new_file = docker::copy_file(container, file, new_name).await?;

Ok(vec![new_file])
}
Expand All @@ -85,8 +170,9 @@ async fn extract_rename(
async fn extract_archive(
chal: &ChallengeConfig,
container: &docker::ContainerInfo,
files: &Vec<String>,
archive_name: &str,
// files: &Vec<PathBuf>,
files: &[PathBuf],
archive_name: &Path,
) -> Result<Vec<PathBuf>> {
debug!(
"extracting {} files {:?} into archive {:?}",
Expand All @@ -96,30 +182,41 @@ async fn extract_archive(
);

// copy all listed files to tempdir
let tempdir = tempdir_in(".")?;
let copied_files = try_join_all(files.iter().map(|f| {
let from = PathBuf::from(f);
let tempdir = tempfile::Builder::new()
.prefix(".beavercds-archive-")
.tempdir_in(".")?;
let copied_files = try_join_all(files.iter().map(|from| async {
let to = tempdir.path().join(from.file_name().unwrap());

docker::copy_file(container, from, to)
docker::copy_file(container, from, &to).await
}))
.await?;

// write them all to new zip
let zipfile = File::create(chal.directory.join(archive_name))?;
// archive_name already has the chal dir prepended
zip_files(archive_name, &copied_files)?;

Ok(vec![chal.directory.join(archive_name)])
}

/// Add multiple local `files` to a zipfile at `zip_name`
pub fn zip_files(archive_name: &Path, files: &[PathBuf]) -> Result<PathBuf> {
debug!("creating zip at {:?}", archive_name);
let zipfile = File::create(archive_name)?;
let mut z = zip::ZipWriter::new(zipfile);
let opts = zip::write::SimpleFileOptions::default();

let mut buf = vec![];
for path in copied_files.into_iter() {
trace!("adding {:?} to zip", &path);
File::open(&path)?.read_to_end(&mut buf)?;
z.start_file(path.to_string_lossy(), opts)?;
for path in files.iter() {
trace!("adding {:?} to zip", path);
// TODO: dont read entire file into memory
File::open(path)?.read_to_end(&mut buf)?;
// TODO: should this always do basename? some chals might need specific
// file structure but including dirs should work fine
z.start_file(path.file_name().unwrap().to_string_lossy(), opts)?;
z.write_all(&buf)?;
buf.clear();
}

z.finish();

Ok(vec![chal.directory.join(archive_name)])
Ok(archive_name.to_path_buf())
}
11 changes: 6 additions & 5 deletions src/builder/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ pub async fn push_image(image_tag: &str, creds: &UserPass) -> Result<String> {
Ok(tag.to_string())
}

#[tokio::main(flavor = "current_thread")] // make this a sync function
pub async fn create_container(image_tag: &str, name: &str) -> Result<ContainerInfo> {
debug!("creating container {name:?} from image {image_tag:?}");
let client = docker().await?;
Expand All @@ -143,7 +142,6 @@ pub async fn create_container(image_tag: &str, name: &str) -> Result<ContainerIn
})
}

#[tokio::main(flavor = "current_thread")] // make this a sync function
pub async fn remove_container(container: ContainerInfo) -> Result<()> {
debug!("removing container {}", &container.name);
let client = docker().await?;
Expand All @@ -157,7 +155,7 @@ pub async fn remove_container(container: ContainerInfo) -> Result<()> {
Ok(())
}

pub async fn copy_file(container: &ContainerInfo, from: PathBuf, to: PathBuf) -> Result<PathBuf> {
pub async fn copy_file(container: &ContainerInfo, from: &Path, to: &Path) -> Result<PathBuf> {
trace!("copying {}:{from:?} to {to:?}", container.name);

let client = docker().await?;
Expand All @@ -182,7 +180,10 @@ pub async fn copy_file(container: &ContainerInfo, from: PathBuf, to: PathBuf) ->
});

// collect byte stream chunks into full file
let mut temptar = Builder::new().suffix(".tar").tempfile_in(".")?;
let mut temptar = Builder::new()
.prefix(".beavercds-")
.suffix(".tar")
.tempfile_in(".")?;
while let Some(chunk) = dl_stream.next().await {
temptar.as_file_mut().write_all(&chunk?)?;
}
Expand All @@ -197,7 +198,7 @@ pub async fn copy_file(container: &ContainerInfo, from: PathBuf, to: PathBuf) ->
if let Some(mut entry_r) = tar.entries()?.next() {
let mut entry = entry_r?;
trace!("got entry: {:?}", entry.path());
let mut target = File::create(&to)?;
let mut target = File::create(to)?;
io::copy(&mut entry, &mut target)?;
} else {
bail!(
Expand Down
Loading