Skip to content

Commit 238d6e5

Browse files
Merge pull request #39 from osusec/dr/deploy-upload
S3 asset upload for deploy command
2 parents 275ad4b + 3eceb00 commit 238d6e5

File tree

4 files changed

+125
-36
lines changed

4 files changed

+125
-36
lines changed

src/access_handlers/docker.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ pub async fn check(profile_name: &str) -> Result<()> {
3030
debug!("will push test image to {}", test_image);
3131

3232
// push alpine image with build credentials
33-
check_build_credentials(&client, &test_image)
33+
check_build_credentials(client, &test_image)
3434
.await
3535
.with_context(|| "Could not push images to registry (bad build credentials?)")?;
3636

3737
// try pulling that image with cluster credentials
38-
check_cluster_credentials(&client, &test_image)
38+
check_cluster_credentials(client, &test_image)
3939
.await
4040
.with_context(|| "Could not pull images from registry (bad cluster credentials?)")?;
4141

src/clients.rs

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// Builders for the various client structs for Docker/Kube etc.
22

3+
use std::sync::OnceLock;
4+
35
use anyhow::{anyhow, bail, Context, Error, Result};
46
use bollard;
57
use futures::TryFutureExt;
@@ -17,16 +19,27 @@ use crate::configparser::config;
1719
//
1820
// Docker stuff
1921
//
20-
pub async fn docker() -> Result<bollard::Docker> {
21-
debug!("connecting to docker...");
22-
let client = bollard::Docker::connect_with_defaults()?;
23-
client
24-
.ping()
25-
.await
26-
// truncate error chain with new error (returned error is way too verbose)
27-
.map_err(|_| anyhow!("could not talk to Docker daemon (is DOCKER_HOST correct?)"))?;
2822

29-
Ok(client)
23+
static DOCKER_CLIENT: OnceLock<bollard::Docker> = OnceLock::new();
24+
25+
/// Return existing or create new Docker client
26+
pub async fn docker() -> Result<&'static bollard::Docker> {
27+
match DOCKER_CLIENT.get() {
28+
Some(d) => Ok(d),
29+
None => {
30+
debug!("connecting to docker...");
31+
let client = bollard::Docker::connect_with_defaults()?;
32+
client
33+
.ping()
34+
.await
35+
// truncate error chain with new error (returned error is way too verbose)
36+
.map_err(|_| {
37+
anyhow!("could not talk to Docker daemon (is DOCKER_HOST correct?)")
38+
})?;
39+
40+
Ok(DOCKER_CLIENT.get_or_init(|| client))
41+
}
42+
}
3043
}
3144

3245
#[derive(Debug)]
@@ -54,27 +67,38 @@ pub async fn engine_type() -> EngineType {
5467
// S3 stuff
5568
//
5669

57-
/// create bucket client for passed profile config
58-
pub fn bucket_client(config: &config::S3Config) -> Result<Box<s3::Bucket>> {
59-
trace!("creating bucket client");
60-
// TODO: once_cell this so it reuses the same bucket?
61-
let region = s3::Region::Custom {
62-
region: config.region.clone(),
63-
endpoint: config.endpoint.clone(),
64-
};
65-
let creds = s3::creds::Credentials::new(
66-
Some(&config.access_key),
67-
Some(&config.secret_key),
68-
None,
69-
None,
70-
None,
71-
)?;
72-
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();
73-
74-
Ok(bucket)
70+
// this does need to be a OnceLock instead of a LazyLock, even though how this
71+
// is used is more inline with a LazyLock. Lazy does not allow for passing
72+
// anything into the init function, and this needs a parameter to know what
73+
// profile to fetch creds for.
74+
static BUCKET_CLIENT: OnceLock<Box<s3::Bucket>> = OnceLock::new();
75+
76+
/// return existing or create new bucket client for passed profile config
77+
pub fn bucket_client(config: &config::S3Config) -> Result<&s3::Bucket> {
78+
match BUCKET_CLIENT.get() {
79+
Some(b) => Ok(b),
80+
None => {
81+
trace!("creating bucket client");
82+
let region = s3::Region::Custom {
83+
region: config.region.clone(),
84+
endpoint: config.endpoint.clone(),
85+
};
86+
let creds = s3::creds::Credentials::new(
87+
Some(&config.access_key),
88+
Some(&config.secret_key),
89+
None,
90+
None,
91+
None,
92+
)?;
93+
let bucket = s3::Bucket::new(&config.bucket_name, region, creds)?.with_path_style();
94+
95+
Ok(BUCKET_CLIENT.get_or_init(|| bucket))
96+
}
97+
}
7598
}
7699

77100
/// create public/anonymous bucket client for passed profile config
101+
// this does not need a oncelock and can be created on-demand, as this is not used in very many places
78102
pub fn bucket_client_anonymous(config: &config::S3Config) -> Result<Box<s3::Bucket>> {
79103
trace!("creating anon bucket client");
80104
// TODO: once_cell this so it reuses the same bucket?
@@ -92,6 +116,10 @@ pub fn bucket_client_anonymous(config: &config::S3Config) -> Result<Box<s3::Buck
92116
// Kubernetes stuff
93117
//
94118

119+
// no OnceLock caching for K8S client. Some operations with the client require
120+
// their own owned `kube::Client`, so always returning a borrowed client from
121+
// the OnceLock would not work.
122+
95123
/// Returns Kubernetes Client for selected profile
96124
pub async fn kube_client(profile: &config::ProfileConfig) -> Result<kube::Client> {
97125
debug!("building kube client");

src/commands/deploy.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use itertools::Itertools;
12
use simplelog::*;
23
use std::process::exit;
34

@@ -31,6 +32,11 @@ pub async fn run(profile_name: &str, no_build: &bool, _dry_run: &bool) {
3132
}
3233
};
3334

35+
trace!(
36+
"got built results: {:#?}",
37+
build_results.iter().map(|b| &b.1).collect_vec()
38+
);
39+
3440
// deploy needs to:
3541
// A) render kubernetes manifests
3642
// - namespace, deployment, service, ingress
@@ -41,21 +47,18 @@ pub async fn run(profile_name: &str, no_build: &bool, _dry_run: &bool) {
4147
// C) update frontend with new state of challenges
4248

4349
// A)
44-
info!("deploying challenges...");
4550
if let Err(e) = deploy::kubernetes::deploy_challenges(profile_name, &build_results).await {
4651
error!("{e:?}");
4752
exit(1);
4853
}
4954

5055
// B)
51-
info!("deploying challenges...");
5256
if let Err(e) = deploy::s3::upload_assets(profile_name, &build_results).await {
5357
error!("{e:?}");
5458
exit(1);
5559
}
5660

57-
// A)
58-
info!("deploying challenges...");
61+
// C)
5962
if let Err(e) = deploy::frontend::update_frontend(profile_name, &build_results).await {
6063
error!("{e:?}");
6164
exit(1);

src/deploy/s3.rs

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use std::fs::File;
2-
use std::path::PathBuf;
2+
use std::path::{Path, PathBuf};
33

44
use anyhow::{anyhow, bail, Context, Error, Ok, Result};
5+
use futures::future::try_join_all;
56
use itertools::Itertools;
7+
use s3::Bucket;
68
use simplelog::*;
9+
use tokio;
710

811
use crate::builder::BuildResult;
912
use crate::clients::bucket_client;
@@ -16,11 +19,66 @@ use crate::utils::TryJoinAll;
1619
pub async fn upload_assets(
1720
profile_name: &str,
1821
build_results: &[(&ChallengeConfig, BuildResult)],
19-
) -> Result<Vec<String>> {
22+
) -> Result<Vec<BuildResult>> {
2023
let profile = get_profile_config(profile_name)?;
2124
let enabled_challenges = enabled_challenges(profile_name)?;
2225

2326
let bucket = bucket_client(&profile.s3)?;
2427

25-
todo!();
28+
info!("uploading assets...");
29+
30+
// upload all files for each challenge
31+
build_results
32+
.iter()
33+
.map(|(chal, result)| async move {
34+
// upload all files for a specific challenge
35+
36+
info!(" for chal {:?}...", chal.directory);
37+
38+
let uploaded = result
39+
.assets
40+
.iter()
41+
.map(|asset_file| async move {
42+
upload_single_file(bucket, chal, asset_file)
43+
.await
44+
.with_context(|| format!("failed to upload file {asset_file:?}"))
45+
})
46+
.try_join_all()
47+
.await
48+
.with_context(|| {
49+
format!("failed to upload asset files for chal {:?}", chal.directory)
50+
})?;
51+
52+
// return new BuildResult with assets as bucket path
53+
Ok(BuildResult {
54+
tags: result.tags.clone(),
55+
assets: uploaded,
56+
})
57+
})
58+
.try_join_all()
59+
.await
60+
}
61+
62+
async fn upload_single_file(
63+
bucket: &Bucket,
64+
chal: &ChallengeConfig,
65+
file: &Path,
66+
) -> Result<PathBuf> {
67+
// e.g. s3.example.domain/assets/misc/foo/stuff.zip
68+
let path_in_bucket = format!(
69+
"assets/{chal_slug}/{file}",
70+
chal_slug = chal.directory.to_string_lossy(),
71+
file = file.file_name().unwrap().to_string_lossy()
72+
);
73+
74+
trace!("uploading {:?} to bucket path {:?}", file, &path_in_bucket);
75+
76+
// TODO: move to async/streaming to better handle large files and report progress
77+
let mut asset_file = tokio::fs::File::open(file).await?;
78+
let r = bucket
79+
.put_object_stream(&mut asset_file, &path_in_bucket)
80+
.await?;
81+
trace!("uploaded {} bytes for file {:?}", r.uploaded_bytes(), file);
82+
83+
Ok(PathBuf::from(path_in_bucket))
2684
}

0 commit comments

Comments
 (0)