Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
196504a
init command main workflow
cacama-valvata Apr 9, 2025
082ae15
init command, formatted
cacama-valvata Apr 9, 2025
0346bf3
init command, lump external-dns configuration options into 'not our p…
cacama-valvata Apr 9, 2025
ce58111
init cmd added inquire placeholders and cleaned up prompt and help me…
cacama-valvata Apr 10, 2025
5b9b11e
init command - allowed difficulty class to be a string instead of a n…
cacama-valvata Apr 10, 2025
cbe9e94
changed structs to upper pascal case
cacama-valvata Apr 10, 2025
cb36287
cleaning up & addressing robert's feedback in the old branch/PR
cacama-valvata Apr 10, 2025
b75b3d6
Fix result handling in init command runner
detjensrobert Sep 27, 2025
4b3723c
Clippy fixes, use Default trait for blank
detjensrobert Sep 27, 2025
bdfd2bd
Tracing logs and use render_strict helper
detjensrobert Sep 27, 2025
77498dc
use to_string, inquire default values
detjensrobert Sep 28, 2025
fe18a8e
simplify init imports
detjensrobert Oct 8, 2025
72dcc88
Show external-dns config notice as warning
detjensrobert Oct 15, 2025
2293423
Use our internal config structs for init handling -- example init
detjensrobert Oct 15, 2025
e4368cc
Use our internal config structs for init handling -- blank init
detjensrobert Oct 15, 2025
bba1d48
Convert points difficulty category name to a string
detjensrobert Oct 15, 2025
c368a4d
Use our internal config structs for init handling -- interactive prompts
detjensrobert Oct 15, 2025
5c2316e
Update rcds config template with help comments and use new structs
detjensrobert Oct 23, 2025
6569a5e
Handle empty profile/points correctly
detjensrobert Oct 23, 2025
2221222
Add trailing template comment to preserve trailing newline when blank
detjensrobert Oct 24, 2025
3cdf345
Make default `init` behaviour interactive
detjensrobert Oct 24, 2025
0fbb636
Fix blank points and reuse default tag format from parsing
detjensrobert Oct 24, 2025
ef57447
Switch to serde_yaml_ng
detjensrobert Oct 24, 2025
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
258 changes: 220 additions & 38 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ clap = { version = "4.5.4", features = ["unicode", "env", "derive"] }
itertools = "0.12.1"
glob = "0.3.1"
serde = { version = "1.0", features = ["derive"] }
serde_yml = "0.0.12"
serde_yaml_ng = "0.10.0"
serde_nested_with = "0.2.5"
fully_pub = "0.1.4"
void = "1"
futures = "0.3.30"
figment = { version = "0.10.19", features = ["env", "yaml", "test"] }
zip = { version = "2.2.2", default-features = false, features = ["deflate"] }
regex = "1.11.1"
inquire = "0.7.5"

# tracing
tracing = { version = "0.1.41", features = ["attributes"] }
Expand Down
80 changes: 80 additions & 0 deletions src/asset_files/rcds.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Used to check that all challenges' flags are in the correct format,
# and by the scoreboard frontend as a first check for invalid submissions.
flag_regex: "{{ flag_regex }}"

# Registry configuration for challenge images.
registry:
domain: "{{ registry.domain }}"
# This is the default tag format; it will create a separate image for each
# challenge pod. Most container registries (Docker, GHCR, Gitlab, Quay, ...)
# are fine with this. If you are using a container registry that requires
# every image within the repository to be created ahead-of-time (AWS ECR)
# before it can be pushed, you can change this to use tags for each separate
# challenge within one image in the registry.
tag_format: "{{ registry.tag_format }}"
# Build-time credentials used to push images during `beavercds deploy`.
build:
user: "{{ registry.build.user }}"
pass: "{{ registry.build.pass }}"
# Used by the cluster to pull the built images.
cluster:
user: "{{ registry.cluster.user }}"
pass: "{{ registry.cluster.pass }}"

# Default difficulty class and resource requests used for challenges that did
# not set their own.
defaults:
difficulty: "{{ defaults.difficulty }}"
resources: { cpu: {{ defaults.resources.cpu }}, memory: "{{ defaults.resources.memory }}" }

# The list of different difficulties that challenges can be assigned, and how
# many points challenges of that difficulty class should be worth. All
# challenges use dynamic scoring; for static points set both min and max to the
# same value.
points:
{% for pts in points -%}
- difficulty: "{{ pts.difficulty }}"
min: {{ pts.min }}
max: {{ pts.max }}
{%- else -%}
[]
{% endfor %}

# Control what challenges are deployed in each environment profile.
deploy:
{% for name, _conf in profiles | items -%}
{{ name }}: {}
{%- else -%}
{}
{% endfor %}

# Separate environment profiles to allow for multiple independent deployments
# of challenges, e.g. staging and production to test challenges internally
# before going live for all users.
profiles:
{% for name, p in profiles | items -%}
{{ name }}:
# Used to push challenge information into the frontend/scoreboard.
frontend_url: {{ p.frontend_url }}
frontend_token: {{ p.frontend_token }}
# Root domain to expose all challenges under.
challenges_domain: {{ p.challenges_domain }}
# TODO: kubeconfig
kubecontext: {{ p.kubecontext }}
# Credentials for the public challenge file asset bucket.
s3:
bucket_name: {{ p.s3.bucket_name }}
endpoint: {{ p.s3.endpoint }}
region: {{ p.s3.region }}
access_key: {{ p.s3.access_key }}
secret_key: {{ p.s3.secret_key }}
# Config for the environment's external-dns deployment.
dns:
# Place external-dns configuration options here;
# this yaml will be passed directly to external-dns without modification
# Reference: https://github.com/bitnami/charts/tree/main/bitnami/external-dns
{%- else -%}
{}
{% endfor %}

{# comment to preserve trailing newline -#}
17 changes: 17 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,21 @@ pub enum Commands {
#[arg(short, long, value_name = "PROFILE")]
profile: String,
},

/// Create an initial `rcds.yaml` to the current working directory.
///
/// By default, this command will prompt for each field of the config file
/// interactively.
#[group(multiple = false)]
Init {
/// Prompt user for each field interactively. [Default if no flags specified]
#[arg(short = 'i', long)]
interactive: bool,
/// Create a minimal config file with all fields blank.
#[arg(short = 'b', long)]
blank: bool,
/// Create a config file with example placeholder values.
#[arg(short = 'p', long)]
placeholders: bool,
},
}
10 changes: 5 additions & 5 deletions src/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ pub async fn apply_manifest_yaml(

// this manifest has multiple documents (crds, deployment)
for yaml in multidoc_deserialize(manifest)? {
let obj: DynamicObject = serde_yml::from_value(yaml)?;
let obj: DynamicObject = serde_yaml_ng::from_value(yaml)?;
trace!(
"applying resource {} {}",
obj.types.clone().unwrap_or_default().kind,
Expand Down Expand Up @@ -334,14 +334,14 @@ pub async fn apply_manifest_yaml(
}

/// Deserialize multi-document yaml string into a Vec of the documents
fn multidoc_deserialize(data: &str) -> Result<Vec<serde_yml::Value>> {
fn multidoc_deserialize(data: &str) -> Result<Vec<serde_yaml_ng::Value>> {
use serde::Deserialize;

let mut docs = vec![];
for de in serde_yml::Deserializer::from_str(data) {
match serde_yml::Value::deserialize(de)? {
for de in serde_yaml_ng::Deserializer::from_str(data) {
match serde_yaml_ng::Value::deserialize(de)? {
// discard any empty documents (e.g. from trailing ---)
serde_yml::Value::Null => (),
serde_yaml_ng::Value::Null => (),
not_null => docs.push(not_null),
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/cluster_setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use kube::{Api, ResourceExt};
use minijinja;
use owo_colors::OwoColorize;
use serde;
use serde_yml;
use serde_yaml_ng;
use tempfile;
use tracing::{debug, error, info, trace, warn};

Expand Down Expand Up @@ -95,7 +95,7 @@ pub async fn install_extdns(profile: &config::ProfileConfig) -> Result<()> {
let values = render_strict(
VALUES_TEMPLATE,
minijinja::context! {
provider_credentials => serde_yml::to_string(&profile.dns)?,
provider_credentials => serde_yaml_ng::to_string(&profile.dns)?,
chal_domain => profile.challenges_domain
},
)?;
Expand Down
30 changes: 30 additions & 0 deletions src/commands/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use anyhow::{Context, Result};
use std::fs::File;
use std::io::Write;
use std::process::exit;
use tracing::{error, warn};

use crate::init;
use crate::{access_handlers::frontend, commands::deploy};

pub fn run(_interactive: &bool, placeholders: &bool, blank: &bool) -> Result<()> {
let options = if *blank {
init::blank_init()
} else if *placeholders {
init::example_init()
} else {
// default to interactive if no flags given
init::interactive_init()?
};

let configuration = init::templatize_init(&options).context("could not render template")?;

let mut f = File::create("rcds.yaml")?;
f.write_all(configuration.as_bytes())?;

// Note about external-dns
warn!("Note: external-dns configuration settings will need to be provided in rcds.yaml after file creation, under the `profiles.name.dns` key.");
warn!("Reference: https://github.com/bitnami/charts/tree/main/bitnami/external-dns");

Ok(())
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod build;
pub mod check_access;
pub mod cluster_setup;
pub mod deploy;
pub mod init;
pub mod validate;

// These modules should not do much and act mostly as a thunk to handle
Expand Down
11 changes: 6 additions & 5 deletions src/configparser/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ struct Registry {
/// Container registry login for pulling images in cluster. Can and should be read-only.
cluster: UserPass,
}
fn default_tag_format() -> String {
pub fn default_tag_format() -> String {
"{{domain}}/{{challenge}}-{{container}}:{{profile}}".to_string()
}

Expand All @@ -121,7 +121,7 @@ struct Resource {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[fully_pub]
struct Defaults {
difficulty: i64,
difficulty: String,
resources: Resource,
}

Expand All @@ -142,13 +142,14 @@ struct ProfileConfig {
kubeconfig: Option<String>,
kubecontext: String,
s3: S3Config,
dns: serde_yml::Value,
dns: serde_yaml_ng::Value,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[fully_pub]
struct ChallengePoints {
difficulty: i64,
/// Name of this difficulty level
difficulty: String,
min: i64,
max: i64,
}
Expand Down
28 changes: 28 additions & 0 deletions src/init/example_values.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Example strings for rcds.yaml

pub static FLAG_REGEX: &str = "ctf{.*}";

pub static REGISTRY_DOMAIN: &str = "ghcr.io/youraccount";
pub static REGISTRY_BUILD_USER: &str = "admin";
pub static REGISTRY_BUILD_PASS: &str = "notrealcreds";
pub static REGISTRY_CLUSTER_USER: &str = "cluster_user";
pub static REGISTRY_CLUSTER_PASS: &str = "alsofake";

pub static DEFAULTS_DIFFICULTY: &str = "easy";
pub static DEFAULTS_RESOURCES_CPU: i64 = 1;
pub static DEFAULTS_RESOURCES_MEMORY: &str = "500M";

pub static POINTS_DIFFICULTY: &str = "easy";
pub static POINTS_MIN: i64 = 200;
pub static POINTS_MAX: i64 = 500;

pub static PROFILES_PROFILE_NAME: &str = "default";
pub static PROFILES_FRONTEND_URL: &str = "https://ctf.coolguy.invalid";
pub static PROFILES_FRONTEND_TOKEN: &str = "secretsecretsecret";
pub static PROFILES_CHALLENGES_DOMAIN: &str = "chals.coolguy.invalid";
pub static PROFILES_KUBECONTEXT: &str = "ctf-cluster";
pub static PROFILES_S3_BUCKET_NAME: &str = "ctf-bucket";
pub static PROFILES_S3_ENDPOINT: &str = "s3.coolguy.invalid";
pub static PROFILES_S3_REGION: &str = "us-west-2";
pub static PROFILES_S3_ACCESSKEY: &str = "accesskey";
pub static PROFILES_S3_SECRETACCESSKEY: &str = "secretkey";
Loading