Skip to content

Commit a22b9e2

Browse files
committed
split deploy components into separate files
Signed-off-by: Robert Detjens <github@detjens.dev>
1 parent eb42308 commit a22b9e2

File tree

6 files changed

+168
-147
lines changed

6 files changed

+168
-147
lines changed

src/builder/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ pub(super) use image_tag_str;
3333
pub struct BuildResult {
3434
/// Container image tags of all containers in the challenge, if the challenge has container images.
3535
/// Will be empty if challenge has no images built from source.
36-
tags: Vec<TagWithSource>,
36+
pub tags: Vec<TagWithSource>,
3737
/// Path on disk to local assets (both built and static).
3838
/// Will be empty if challenge has no file assets
39-
assets: Vec<PathBuf>,
39+
pub assets: Vec<PathBuf>,
4040
}
4141

4242
/// Tag string with added context of where it came from (built locally or an upstream image)

src/commands/deploy.rs

Lines changed: 6 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
1-
use anyhow::{anyhow, bail, Context, Error, Result};
2-
use itertools::Itertools;
3-
use k8s_openapi::api::core::v1::Secret;
4-
use kube::api::ListParams;
51
use simplelog::*;
6-
use std::env::current_exe;
72
use std::process::exit;
83

9-
use crate::clients::kube_client;
10-
use crate::cluster_setup;
11-
use crate::configparser::config::ProfileConfig;
12-
use crate::configparser::{get_config, get_profile_config};
13-
144
use crate::builder::build_challenges;
15-
use crate::deploy::{deploy_challenges, update_frontend, upload_assets};
5+
use crate::configparser::{get_config, get_profile_config};
6+
use crate::deploy;
167

178
#[tokio::main(flavor = "current_thread")] // make this a sync function
189
pub async fn run(profile_name: &str, no_build: &bool, _dry_run: &bool) {
1910
let profile = get_profile_config(profile_name).unwrap();
2011

2112
// has the cluster been setup?
22-
if let Err(e) = check_setup(profile).await {
13+
if let Err(e) = deploy::check_setup(profile).await {
2314
error!("{e:?}");
2415
exit(1);
2516
}
@@ -51,116 +42,22 @@ pub async fn run(profile_name: &str, no_build: &bool, _dry_run: &bool) {
5142

5243
// A)
5344
info!("deploying challenges...");
54-
if let Err(e) = deploy_challenges(profile_name, &build_results).await {
45+
if let Err(e) = deploy::kubernetes::deploy_challenges(profile_name, &build_results).await {
5546
error!("{e:?}");
5647
exit(1);
5748
}
5849

5950
// B)
6051
info!("deploying challenges...");
61-
if let Err(e) = upload_assets(profile_name, &build_results).await {
52+
if let Err(e) = deploy::s3::upload_assets(profile_name, &build_results).await {
6253
error!("{e:?}");
6354
exit(1);
6455
}
6556

6657
// A)
6758
info!("deploying challenges...");
68-
if let Err(e) = update_frontend(profile_name, &build_results).await {
59+
if let Err(e) = deploy::frontend::update_frontend(profile_name, &build_results).await {
6960
error!("{e:?}");
7061
exit(1);
7162
}
7263
}
73-
74-
/// check to make sure that the needed ingress charts are deployed and running
75-
async fn check_setup(profile: &ProfileConfig) -> Result<()> {
76-
let kube = kube_client(profile).await?;
77-
let secrets: kube::Api<Secret> = kube::Api::namespaced(kube, cluster_setup::INGRESS_NAMESPACE);
78-
79-
let all_releases = secrets
80-
.list_metadata(&ListParams::default().labels("owner=helm"))
81-
.await?;
82-
83-
// pull helm release version from secret label
84-
macro_rules! helm_version {
85-
($s:ident) => {
86-
$s.get("version")
87-
.unwrap_or(&"".to_string())
88-
.parse::<usize>()
89-
.unwrap_or(0)
90-
};
91-
}
92-
let expected_charts = ["ingress-nginx", "cert-manager", "external-dns"];
93-
let latest_releases = expected_charts
94-
.iter()
95-
.map(|chart| {
96-
// pick latest release
97-
all_releases
98-
.iter()
99-
.map(|r| r.metadata.labels.as_ref().unwrap())
100-
.filter(|rl| rl.get("name") == Some(&chart.to_string()))
101-
.max_by(|a, b| helm_version!(a).cmp(&helm_version!(b)))
102-
})
103-
.collect_vec();
104-
105-
enum ChartFailure {
106-
Missing(String),
107-
DeploymentFailed(String),
108-
}
109-
110-
// make sure all releases are present and deployed successfully
111-
let missing = latest_releases
112-
.iter()
113-
.zip(expected_charts)
114-
.filter_map(|(r, c)| {
115-
// is label status=deployed ?
116-
if r.is_none() {
117-
return Some(ChartFailure::Missing(c.to_string()));
118-
}
119-
120-
if r.unwrap().get("status") == Some(&"deployed".to_string()) {
121-
// all is good
122-
None
123-
} else {
124-
Some(ChartFailure::DeploymentFailed(c.to_string()))
125-
}
126-
})
127-
.collect_vec();
128-
129-
if !missing.is_empty() {
130-
// if any errors are present, collect/reduce them all into one error via
131-
// anyhow context() calls.
132-
//
133-
// TODO: this should probably be returning Vec<Error> instead of a
134-
// single Error chain. should this be in run() to present errors there
135-
// instead of chaining and returning one combined Error here?
136-
#[allow(clippy::manual_try_fold)] // need to build the Result ourselves
137-
missing
138-
.iter()
139-
.fold(Err(anyhow!("")), |e, reason| match reason {
140-
ChartFailure::Missing(c) => e.with_context(|| {
141-
format!(
142-
"chart {}/{c} is not deployed",
143-
cluster_setup::INGRESS_NAMESPACE
144-
)
145-
}),
146-
ChartFailure::DeploymentFailed(c) => e.with_context(|| {
147-
format!(
148-
"chart {}/{c} is in a failed state",
149-
cluster_setup::INGRESS_NAMESPACE
150-
)
151-
}),
152-
})
153-
.with_context(|| {
154-
format!(
155-
"cluster has not been set up with needed charts (run `{} cluster-setup`)",
156-
current_exe()
157-
.unwrap()
158-
.file_name()
159-
.unwrap_or_default()
160-
.to_string_lossy()
161-
)
162-
})
163-
} else {
164-
Ok(())
165-
}
166-
}

src/deploy/frontend.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::{anyhow, bail, Context, Error, Ok, Result};
4+
use itertools::Itertools;
5+
use simplelog::*;
6+
7+
use crate::builder::BuildResult;
8+
use crate::configparser::config::ProfileConfig;
9+
use crate::configparser::{enabled_challenges, get_config, get_profile_config, ChallengeConfig};
10+
11+
/// Sync deployed challenges with rCTF frontend
12+
pub async fn update_frontend(
13+
profile_name: &str,
14+
build_results: &[(&ChallengeConfig, BuildResult)],
15+
) -> Result<()> {
16+
let profile = get_profile_config(profile_name)?;
17+
let enabled_challenges = enabled_challenges(profile_name)?;
18+
19+
todo!()
20+
}

src/deploy/kubernetes.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use std::path::PathBuf;
2+
3+
use anyhow::{anyhow, bail, Context, Error, Ok, Result};
4+
use itertools::Itertools;
5+
use simplelog::*;
6+
7+
use crate::builder::BuildResult;
8+
use crate::clients::kube_client;
9+
use crate::configparser::config::ProfileConfig;
10+
use crate::configparser::{enabled_challenges, get_config, get_profile_config, ChallengeConfig};
11+
12+
/// Render challenge manifest templates and apply to cluster
13+
pub async fn deploy_challenges(
14+
profile_name: &str,
15+
build_results: &[(&ChallengeConfig, BuildResult)],
16+
) -> Result<()> {
17+
let profile = get_profile_config(profile_name)?;
18+
let enabled_challenges = enabled_challenges(profile_name)?;
19+
20+
todo!()
21+
}

src/deploy/mod.rs

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,108 @@
1-
use std::path::PathBuf;
1+
pub mod frontend;
2+
pub mod kubernetes;
3+
pub mod s3;
24

35
use anyhow::{anyhow, bail, Context, Error, Result};
46
use itertools::Itertools;
7+
use k8s_openapi::api::core::v1::Secret;
8+
use kube::api::ListParams;
59
use simplelog::*;
10+
use std::env::current_exe;
611

7-
use crate::builder::BuildResult;
8-
use crate::clients::{bucket_client, kube_client};
9-
use crate::cluster_setup as setup;
12+
use crate::clients::kube_client;
13+
use crate::cluster_setup;
1014
use crate::configparser::config::ProfileConfig;
11-
use crate::configparser::{enabled_challenges, get_config, get_profile_config, ChallengeConfig};
1215

13-
/// Render challenge manifest templates and apply to cluster
14-
pub async fn deploy_challenges(
15-
profile_name: &str,
16-
build_results: &[(&ChallengeConfig, BuildResult)],
17-
) -> Result<()> {
18-
let profile = get_profile_config(profile_name)?;
19-
let enabled_challenges = enabled_challenges(profile_name)?;
16+
/// check to make sure that the needed ingress charts are deployed and running
17+
pub async fn check_setup(profile: &ProfileConfig) -> Result<()> {
18+
let kube = kube_client(profile).await?;
19+
let secrets: kube::Api<Secret> = kube::Api::namespaced(kube, cluster_setup::INGRESS_NAMESPACE);
2020

21-
todo!()
22-
}
23-
24-
/// Upload files to frontend asset bucket
25-
/// Returns urls of upload files.
26-
pub async fn upload_assets(
27-
profile_name: &str,
28-
build_results: &[(&ChallengeConfig, BuildResult)],
29-
) -> Result<Vec<String>> {
30-
let profile = get_profile_config(profile_name)?;
31-
let enabled_challenges = enabled_challenges(profile_name)?;
21+
let all_releases = secrets
22+
.list_metadata(&ListParams::default().labels("owner=helm"))
23+
.await?;
3224

33-
let bucket = bucket_client(&profile.s3)?;
25+
// pull helm release version from secret label
26+
macro_rules! helm_version {
27+
($s:ident) => {
28+
$s.get("version")
29+
.unwrap_or(&"".to_string())
30+
.parse::<usize>()
31+
.unwrap_or(0)
32+
};
33+
}
34+
let expected_charts = ["ingress-nginx", "cert-manager", "external-dns"];
35+
let latest_releases = expected_charts
36+
.iter()
37+
.map(|chart| {
38+
// pick latest release
39+
all_releases
40+
.iter()
41+
.map(|r| r.metadata.labels.as_ref().unwrap())
42+
.filter(|rl| rl.get("name") == Some(&chart.to_string()))
43+
.max_by(|a, b| helm_version!(a).cmp(&helm_version!(b)))
44+
})
45+
.collect_vec();
3446

35-
todo!()
47+
enum ChartFailure {
48+
Missing(String),
49+
DeploymentFailed(String),
50+
}
3651

37-
// TODO: should uploaded URLs be a (generated) part of the challenge config
38-
// struct?
39-
}
52+
// make sure all releases are present and deployed successfully
53+
let missing = latest_releases
54+
.iter()
55+
.zip(expected_charts)
56+
.filter_map(|(r, c)| {
57+
// is label status=deployed ?
58+
if r.is_none() {
59+
return Some(ChartFailure::Missing(c.to_string()));
60+
}
4061

41-
/// Sync deployed challenges with rCTF frontend
42-
pub async fn update_frontend(
43-
profile_name: &str,
44-
build_results: &[(&ChallengeConfig, BuildResult)],
45-
) -> Result<()> {
46-
let profile = get_profile_config(profile_name)?;
47-
let enabled_challenges = enabled_challenges(profile_name)?;
62+
if r.unwrap().get("status") == Some(&"deployed".to_string()) {
63+
// all is good
64+
None
65+
} else {
66+
Some(ChartFailure::DeploymentFailed(c.to_string()))
67+
}
68+
})
69+
.collect_vec();
4870

49-
todo!()
71+
if !missing.is_empty() {
72+
// if any errors are present, collect/reduce them all into one error via
73+
// anyhow context() calls.
74+
//
75+
// TODO: this should probably be returning Vec<Error> instead of a
76+
// single Error chain. should this be in run() to present errors there
77+
// instead of chaining and returning one combined Error here?
78+
#[allow(clippy::manual_try_fold)] // need to build the Result ourselves
79+
missing
80+
.iter()
81+
.fold(Err(anyhow!("")), |e, reason| match reason {
82+
ChartFailure::Missing(c) => e.with_context(|| {
83+
format!(
84+
"chart {}/{c} is not deployed",
85+
cluster_setup::INGRESS_NAMESPACE
86+
)
87+
}),
88+
ChartFailure::DeploymentFailed(c) => e.with_context(|| {
89+
format!(
90+
"chart {}/{c} is in a failed state",
91+
cluster_setup::INGRESS_NAMESPACE
92+
)
93+
}),
94+
})
95+
.with_context(|| {
96+
format!(
97+
"cluster has not been set up with needed charts (run `{} cluster-setup`)",
98+
current_exe()
99+
.unwrap()
100+
.file_name()
101+
.unwrap_or_default()
102+
.to_string_lossy()
103+
)
104+
})
105+
} else {
106+
Ok(())
107+
}
50108
}

src/deploy/s3.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use std::fs::File;
2+
use std::path::PathBuf;
3+
4+
use anyhow::{anyhow, bail, Context, Error, Ok, Result};
5+
use itertools::Itertools;
6+
use simplelog::*;
7+
8+
use crate::builder::BuildResult;
9+
use crate::clients::bucket_client;
10+
use crate::configparser::config::ProfileConfig;
11+
use crate::configparser::{enabled_challenges, get_config, get_profile_config, ChallengeConfig};
12+
13+
/// Upload files to frontend asset bucket
14+
/// Returns urls of upload files.
15+
pub async fn upload_assets(
16+
profile_name: &str,
17+
build_results: &[(&ChallengeConfig, BuildResult)],
18+
) -> Result<Vec<String>> {
19+
let profile = get_profile_config(profile_name)?;
20+
let enabled_challenges = enabled_challenges(profile_name)?;
21+
22+
let bucket = bucket_client(&profile.s3)?;
23+
24+
todo!();
25+
}

0 commit comments

Comments
 (0)