From 196504a832a29928db20fcc12236f38958119280 Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Tue, 8 Apr 2025 22:12:18 -0700 Subject: [PATCH 01/26] init command main workflow --- Cargo.lock | 236 +++++++++++++++++++++-- Cargo.toml | 2 + src/asset_files/rcds.yaml.j2 | 46 +++++ src/cli.rs | 10 + src/commands/init.rs | 55 ++++++ src/commands/mod.rs | 1 + src/init/mod.rs | 362 +++++++++++++++++++++++++++++++++++ src/init/templates.rs | 4 + src/lib.rs | 1 + src/main.rs | 5 + 10 files changed, 701 insertions(+), 21 deletions(-) create mode 100644 src/asset_files/rcds.yaml.j2 create mode 100644 src/commands/init.rs create mode 100644 src/init/mod.rs create mode 100644 src/init/templates.rs diff --git a/Cargo.lock b/Cargo.lock index ffbfd58..8ea334f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -285,12 +285,14 @@ dependencies = [ "futures", "glob", "http 1.2.0", + "inquire", "itertools", "k8s-openapi", "kube", "minijinja", "owo-colors", "pretty_assertions", + "regex", "rust-s3", "serde", "serde_nested_with", @@ -304,6 +306,12 @@ dependencies = [ "zip", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -375,6 +383,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -406,7 +420,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -430,7 +444,7 @@ dependencies = [ "clap_lex", "strsim", "unicase", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -526,6 +540,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -903,6 +942,24 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1463,6 +1520,23 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1669,7 +1743,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "redox_syscall", ] @@ -1775,6 +1849,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" @@ -1787,6 +1873,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1900,7 +1995,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2106,7 +2201,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -2228,7 +2323,7 @@ version = "0.38.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2429,7 +2524,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2626,6 +2721,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2861,7 +2977,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2950,7 +3066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "base64 0.22.1", - "bitflags", + "bitflags 2.6.0", "bytes", "http 1.2.0", "http-body 1.0.1", @@ -3081,6 +3197,18 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" @@ -3254,7 +3382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3263,7 +3391,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", ] [[package]] @@ -3272,7 +3409,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3281,7 +3418,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3290,28 +3442,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3324,24 +3494,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 0ee4c3d..13f2c83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ 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"] } diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 new file mode 100644 index 0000000..df5176b --- /dev/null +++ b/src/asset_files/rcds.yaml.j2 @@ -0,0 +1,46 @@ +flag_regex: {{ options.flag_regex }} + +registry: + domain: {{ options.registry_domain }} + build: + user: {{ options.registry_build_user }} + pass: {{ options.registry_build_pass }} + cluster: + user: {{ options.registry_cluster_user }} + pass: {{ options.registry_cluster_pass }} + +defaults: + difficulty: {{ options.defaults_difficulty }} + resources: { cpu: {{ options.defaults_resources_cpu }}, memory: {{ options.defaults_resources_memory }} } + +points: {% for pts in options.points %} + - difficulty: {{ pts.difficulty }} + min: {{ pts.min }} + max: {{ pts.max }} +{% endfor %} +# TODO +deploy: + # control challenge deployment status explicitly per environment/profile + testing: + misc/garf: true + pwn/notsh: true + web/bar: true + +profiles: +{% for prof in options.profiles %} + {{ prof.profile_name}}: + frontend_url: {{ prof.frontend_url }} + frontend_token: {{ prof.frontend_token }} + challenges_domain: {{ prof.challenges_domain }} + kubecontext: {{ prof.kubecontext }} + s3: + bucket_name: {{ prof.s3_bucket_name }} + endpoint: {{ prof.s3_endpoint }} + region: {{ prof.s3_region }} + access_key: {{ prof.s3_accesskey }} + secret_key: {{ prof.s3_secretaccesskey }} + dns: + provider: {{ prof.dns_provider }} + {{ prof.dns_provider }}: + # TODO: provider-specific external-dns +{% endfor %} \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index fb1ef10..32bc58c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -87,4 +87,14 @@ pub enum Commands { #[arg(short, long, value_name = "PROFILE")] profile: String, }, + + /// Create an initial rcds.yaml to the current working directory. + Init { + /// Cannot be used with -b. If enabled, will prompt for each field of the config file. If disabled, behavior depends on --blank. + #[arg(short = 'i', long)] + interactive: bool, + /// Cannot be used with -i. If enabled, will create the file without any fields set. If disabled, will create an example config file (fields set with fake data). + #[arg(short = 'b', long, conflicts_with = "interactive")] + blank: bool + }, } diff --git a/src/commands/init.rs b/src/commands/init.rs new file mode 100644 index 0000000..016ab7f --- /dev/null +++ b/src/commands/init.rs @@ -0,0 +1,55 @@ +use simplelog::error; +use std::process::exit; +use std::fs::File; +use std::io::Write; + +use crate::{access_handlers::frontend, commands::deploy}; +use crate::init::{self as init, templatize_init}; + + +pub fn run(_interactive: &bool, _blank: &bool) +{ + let options: init::init_vars; + + if *_interactive + { + options = match init::interactive_init() + { + Ok(t) => t, + Err(e) => + { + error!("Error in init: {e}"); + exit(1); + } + }; + } + else if *_blank + { + options = init::blank_init(); + } + else { + options = init::example_init(); + } + + // TODO write to disk + let configuration = templatize_init(options); + let mut f = match File::create("rcds.yaml") + { + Ok(t) => t, + Err(e) => + { + error!("Error in init: {e}"); + exit(1); + } + }; + match f.write_all(configuration.as_bytes()) + { + Ok(_) => (), + Err(e) => + { + error!("Error in init: {e}"); + exit(1); + } + } + +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 62beb26..6a3ae54 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,7 @@ pub mod build; pub mod check_access; pub mod cluster_setup; +pub mod init; pub mod deploy; pub mod validate; diff --git a/src/init/mod.rs b/src/init/mod.rs new file mode 100644 index 0000000..3b518c8 --- /dev/null +++ b/src/init/mod.rs @@ -0,0 +1,362 @@ + +use inquire; +use minijinja; +use serde; +use std::fmt; +use regex::Regex; + +pub mod templates; + +#[derive(serde::Serialize)] +pub struct init_vars +{ + pub flag_regex: String, + pub registry_domain: String, + pub registry_build_user: String, + pub registry_build_pass: String, + pub registry_cluster_user: String, + pub registry_cluster_pass: String, + pub defaults_difficulty: String, //u64, + pub defaults_resources_cpu: String, //u64, + pub defaults_resources_memory: String, //(u64, Option(String)), + pub points: Vec, + pub profiles: Vec +} + +#[derive(Clone)] +#[derive(serde::Serialize)] +pub struct points { + pub difficulty: String, //u64, + pub min: String, //u64, + pub max: String, //u64 +} + +impl fmt::Display for points { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({} Points: {}-{})", self.difficulty, self.min, self.max) + } +} + +#[derive(serde::Serialize)] +pub struct profile { + pub profile_name: String, + pub frontend_url: String, + pub frontend_token: String, + pub challenges_domain: String, + pub kubecontext: String, + pub s3_bucket_name: String, + pub s3_endpoint: String, + pub s3_region: String, + pub s3_accesskey: String, + pub s3_secretaccesskey: String, + // TODO external dns garbage + pub dns_provider: String, + // dns_provider_values: HashMap + // dns_txtOwnerId: Option< +} + + +pub fn interactive_init() -> inquire::error::InquireResult +{ + println!("For all prompts below, simply press Enter to leave blank."); + println!("All fields that can be set in rcds.yaml can also be set via environment variables."); + + let points_ranks_reference: Vec; + + let options = init_vars { + + flag_regex: { + //TODO: + // - also provide regex examples in help + // - is this even a good idea to have the user provide the regex + // - with placeholder? + inquire::Text::new("Flag regex:") + .with_help_message("This regex will be used to validate the individual flags of your challenges later.") + .prompt()? + }, + + registry_domain: { + inquire::Text::new ("Container registry:") + .with_help_message("This is the domain of your remote container registry, which includes both the endpoint details and your repository name.") //where you will push images to and where your cluster will pull challenge images from.") // TODO + .prompt()? + }, + + registry_build_user: { + inquire::Text::new ("Container registry user (YOURS):") + .with_help_message("Your username to the remote container registry, which you will use to push containers to.") + .prompt()? + }, + + // TODO: do we actually want to be in charge of these credentials vs letting the container building utility take care of it? + registry_build_pass: { + inquire::Password::new("Container registry password (YOURS):") + .with_help_message("Your password to the remote container registry, which you will use to push containers to.") // TODO: could this support username:pat too? + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? + }, + + registry_cluster_user: { + inquire::Text::new ("Container registry user (CLUSTER'S):") + .with_help_message("The cluster's username to the remote container registry, which it will use to pull containers from.") + .prompt()? + }, + + // TODO: would the cluster not use a token of some sort? + registry_cluster_pass: { + inquire::Password::new("Container registry password (CLUSTER'S):") + .with_help_message("The cluster's password to the remote container registry, which it will use to pull containers from.") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? + }, + + points: { + println!("You can define several challenge difficulty classes below."); + let mut again = inquire::Confirm::new("Do you want to provide a difficulty class?") + .with_default(false) + .prompt()?; + let mut points_ranks: Vec = Vec::new(); + while again + { + let points_obj = points { + // TODO: theres no reason these need to be numbers instead of open strings, e.g. for "easy" + difficulty: { + inquire::CustomType::::new("Difficulty rank:") + // default parser calls std::u64::from_str + .with_error_message("Please type a valid number.") + .with_help_message("The rank of the difficulty class as an unsigned integer, with lower numbers being \"easier.\"") + .prompt()? + .to_string() + }, + // TODO: support static-point challenges + min: { + inquire::CustomType::::new("Minimum number of points:") + // default parser calls std::u64::from_str + .with_error_message("Please type a valid number.") + .with_help_message("Challenge points are dynamic: the minimum number of points that challenges within this difficulty class are worth.") + .prompt()? + .to_string() + }, + max: { + inquire::CustomType::::new("Maximum number of points:") + // default parser calls std::u64::from_str + .with_error_message("Please type a valid number.") + .with_help_message("Challenge points are dynamic: the maximum number of points that challenges within this difficulty class are worth.") + .prompt()? + .to_string() + } + }; + points_ranks.push(points_obj); + + again = inquire::Confirm::new("Do you want to provide another difficulty class?") + .with_default(false) + .prompt()?; + } + points_ranks_reference = points_ranks.clone(); + points_ranks + }, + + // TODO: how much format validation should these two do now vs offloading to validate() later? current inquire replacement calls are temporary and do the zero checking, just grabbing a String + defaults_difficulty: { + if points_ranks_reference.is_empty() { + String::new() + } + else { + inquire::Select::new("Please choose the default difficulty class:", points_ranks_reference) + .prompt()? + .difficulty + } + }, + + defaults_resources_cpu: { + let resources = inquire::Text::new("Default limit of CPUs per challenge:") + .with_help_message("The maximum limit of CPU resources per instance of challenge deployment (\"pod\").") + .with_placeholder("1") + .prompt()?; + + if resources.is_empty() { + String::from("1") + } + else { + resources + } + }, + + defaults_resources_memory: { + let resources = inquire::Text::new("Default limit of memory per challenge:") + .with_help_message("The maximum limit of memory resources per instance of challenge deployment (\"pod\").") + .with_placeholder("500M") + .prompt()?; + + if resources.is_empty() { + String::from("500M") + } + else { + resources + } + }, + + profiles: { + println!("You can define several environment profiles below."); + + let mut again = inquire::Confirm::new("Do you want to provide a profile?") + .with_default(false) + .prompt()?; + let mut profiles: Vec = Vec::new(); + while again { + let prof = profile { + profile_name: { + inquire::Text::new("Profile name:") + .with_help_message("The name of the deployment profile. One profile named \"default\" is recommended. You can add additional profiles.") + .with_placeholder("default") + .prompt()? + }, + frontend_url: { + inquire::Text::new("Frontend URL:") + .with_help_message("The URL of the RNG scoreboard.") // TODO: can definitely say more about why this is significant + .prompt()? + }, + frontend_token: { + inquire::Text::new("Frontend token:") + .with_help_message("The token for RNG to authenticate itself into the scoreboard.") // TODO: again, say more + .prompt()? + }, + challenges_domain: { + inquire::Text::new("Challenges domain:") + .with_help_message("Domain that challenges are hosted under.") + .prompt()? + }, + kubecontext: { + inquire::Text::new("Kube context:") + .with_help_message("The name of the context that kubectl looks for to interface with the cluster.") + .prompt()? + }, + s3_bucket_name: { + inquire::Text::new("S3 bucket name:") + .with_help_message("Challenge artifacts and static files will be hosted on and served from S3. The name of the S3 bucket.") + .prompt()? + }, + s3_endpoint: { + inquire::Text::new("S3 endpoint:") + .with_help_message("The endpoint of the S3 bucket server.") + .prompt()? + }, + s3_region: { + inquire::Text::new("S3 region:") + .with_help_message("The region that the S3 bucket is hosted.") + .prompt()? + }, + s3_accesskey: { + inquire::Text::new("S3 access key:") + .with_help_message("The public access key to the S3 bucket.") + .prompt()? + }, + s3_secretaccesskey: { + inquire::Text::new("S3 secret key:") + .with_help_message("The secret acess key to the S3 bucket.") + .prompt()? + }, + dns_provider: { + // TODO : literally all of the external DNS settings + inquire::Text::new("DNS provider:") + .with_help_message("The name of the cloud DNS provider being used.") + .with_placeholder("route53") + .prompt()? + } + }; + profiles.push(prof); + + again = inquire::Confirm::new("Do you want to provide another profile?") + .with_default(false) + .prompt()?; + } + profiles + } + }; + return Ok(options); +} + +pub fn blank_init() -> init_vars +{ + return init_vars { + flag_regex: String::new(), + registry_domain: String::new(), + registry_build_user: String::new(), + registry_build_pass: String::new(), + registry_cluster_user: String::new(), + registry_cluster_pass: String::new(), + defaults_difficulty: String::new(), + defaults_resources_cpu: String::new(), + defaults_resources_memory: String::new(), + points: vec![ + points { + difficulty: String::new(), + min: String::new(), + max: String::new() + } + ], + profiles: vec![ + profile { + profile_name: String::from("default"), + frontend_url: String::new(), + frontend_token: String::new(), + challenges_domain: String::new(), + kubecontext: String::new(), + s3_bucket_name: String::new(), + s3_endpoint: String::new(), + s3_region: String::new(), + s3_accesskey: String::new(), + s3_secretaccesskey: String::new(), + dns_provider: String::from("aws") + } + ] + }; +} + +pub fn example_init() -> init_vars +{ + return init_vars { + flag_regex: String::from("ctf{.*}"), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles + registry_domain: String::from("ghcr.io/youraccount"), + registry_build_user: String::from("admin"), + registry_build_pass: String::from("notrealcreds"), + registry_cluster_user: String::from("cluster_user"), + registry_cluster_pass: String::from("alsofake"), + defaults_difficulty: String::from("1"), + defaults_resources_cpu: String::from("1"), + defaults_resources_memory: String::from("500M"), + points: vec![ + points { + difficulty: String::from("1"), + min: String::from("1"), + max: String::from("1337") + }, + points { + difficulty: String::from("2"), + min: String::from("200"), + max: String::from("500") + } + ], + profiles: vec![ + profile { + profile_name: String::from("default"), + frontend_url: String::from("https://ctf.coolguy.xyz"), + frontend_token: String::from("secretsecretsecret"), + challenges_domain: String::from("chals.coolguy.xyz"), + kubecontext: String::from("ctf-cluster"), + s3_bucket_name: String::from("ctf-bucket"), + s3_endpoint: String::from("s3.coolguy.xyz"), + s3_region: String::from("us-west-2"), + s3_accesskey: String::from("accesskey"), + s3_secretaccesskey: String::from("secretkey"), + dns_provider: String::from("aws") + } + ] + }; +} + +pub fn templatize_init(options: init_vars) -> String { + let filled_template = minijinja::render!(templates::RCDS, options); + return filled_template; +} diff --git a/src/init/templates.rs b/src/init/templates.rs new file mode 100644 index 0000000..18700e6 --- /dev/null +++ b/src/init/templates.rs @@ -0,0 +1,4 @@ +// Embed template file into binary + +pub static RCDS: &str = + include_str!("../asset_files/rcds.yaml.j2"); diff --git a/src/lib.rs b/src/lib.rs index 061b46f..85e3043 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod commands; pub mod configparser; pub mod deploy; pub mod utils; +pub mod init; #[cfg(test)] mod tests; diff --git a/src/main.rs b/src/main.rs index f5c71c5..31d8a05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,5 +89,10 @@ fn dispatch(cli: cli::Cli) -> anyhow::Result<()> { commands::validate::run()?; commands::cluster_setup::run(profile) } + + cli::Commands::Init { + interactive, + blank + } => commands::init::run (interactive, blank) } } From 082ae158ac881e3af8d75fa0e6cd4418a42647eb Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Tue, 8 Apr 2025 22:14:44 -0700 Subject: [PATCH 02/26] init command, formatted --- src/cli.rs | 2 +- src/commands/init.rs | 37 +++----- src/commands/mod.rs | 2 +- src/init/mod.rs | 191 ++++++++++++++++++++---------------------- src/init/templates.rs | 3 +- src/lib.rs | 2 +- src/main.rs | 5 +- 7 files changed, 108 insertions(+), 134 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 32bc58c..5a6a164 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -95,6 +95,6 @@ pub enum Commands { interactive: bool, /// Cannot be used with -i. If enabled, will create the file without any fields set. If disabled, will create an example config file (fields set with fake data). #[arg(short = 'b', long, conflicts_with = "interactive")] - blank: bool + blank: bool, }, } diff --git a/src/commands/init.rs b/src/commands/init.rs index 016ab7f..8e23963 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,55 +1,42 @@ use simplelog::error; -use std::process::exit; use std::fs::File; use std::io::Write; +use std::process::exit; -use crate::{access_handlers::frontend, commands::deploy}; use crate::init::{self as init, templatize_init}; +use crate::{access_handlers::frontend, commands::deploy}; - -pub fn run(_interactive: &bool, _blank: &bool) -{ +pub fn run(_interactive: &bool, _blank: &bool) { let options: init::init_vars; - if *_interactive - { - options = match init::interactive_init() - { + if *_interactive { + options = match init::interactive_init() { Ok(t) => t, - Err(e) => - { + Err(e) => { error!("Error in init: {e}"); exit(1); } }; - } - else if *_blank - { + } else if *_blank { options = init::blank_init(); - } - else { + } else { options = init::example_init(); } // TODO write to disk let configuration = templatize_init(options); - let mut f = match File::create("rcds.yaml") - { + let mut f = match File::create("rcds.yaml") { Ok(t) => t, - Err(e) => - { + Err(e) => { error!("Error in init: {e}"); exit(1); } }; - match f.write_all(configuration.as_bytes()) - { + match f.write_all(configuration.as_bytes()) { Ok(_) => (), - Err(e) => - { + Err(e) => { error!("Error in init: {e}"); exit(1); } } - } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 6a3ae54..c12fd1e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,8 +1,8 @@ pub mod build; pub mod check_access; pub mod cluster_setup; -pub mod init; pub mod deploy; +pub mod init; pub mod validate; // These modules should not do much and act mostly as a thunk to handle diff --git a/src/init/mod.rs b/src/init/mod.rs index 3b518c8..a637767 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -1,39 +1,40 @@ - use inquire; use minijinja; +use regex::Regex; use serde; use std::fmt; -use regex::Regex; pub mod templates; #[derive(serde::Serialize)] -pub struct init_vars -{ +pub struct init_vars { pub flag_regex: String, pub registry_domain: String, pub registry_build_user: String, pub registry_build_pass: String, pub registry_cluster_user: String, pub registry_cluster_pass: String, - pub defaults_difficulty: String, //u64, - pub defaults_resources_cpu: String, //u64, + pub defaults_difficulty: String, //u64, + pub defaults_resources_cpu: String, //u64, pub defaults_resources_memory: String, //(u64, Option(String)), pub points: Vec, - pub profiles: Vec + pub profiles: Vec, } -#[derive(Clone)] -#[derive(serde::Serialize)] +#[derive(Clone, serde::Serialize)] pub struct points { pub difficulty: String, //u64, - pub min: String, //u64, - pub max: String, //u64 + pub min: String, //u64, + pub max: String, //u64 } impl fmt::Display for points { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "({} Points: {}-{})", self.difficulty, self.min, self.max) + write!( + f, + "({} Points: {}-{})", + self.difficulty, self.min, self.max + ) } } @@ -55,16 +56,13 @@ pub struct profile { // dns_txtOwnerId: Option< } - -pub fn interactive_init() -> inquire::error::InquireResult -{ +pub fn interactive_init() -> inquire::error::InquireResult { println!("For all prompts below, simply press Enter to leave blank."); println!("All fields that can be set in rcds.yaml can also be set via environment variables."); let points_ranks_reference: Vec; - let options = init_vars { - + let options = init_vars { flag_regex: { //TODO: // - also provide regex examples in help @@ -114,11 +112,10 @@ pub fn interactive_init() -> inquire::error::InquireResult points: { println!("You can define several challenge difficulty classes below."); let mut again = inquire::Confirm::new("Do you want to provide a difficulty class?") - .with_default(false) - .prompt()?; + .with_default(false) + .prompt()?; let mut points_ranks: Vec = Vec::new(); - while again - { + while again { let points_obj = points { // TODO: theres no reason these need to be numbers instead of open strings, e.g. for "easy" difficulty: { @@ -145,13 +142,13 @@ pub fn interactive_init() -> inquire::error::InquireResult .with_help_message("Challenge points are dynamic: the maximum number of points that challenges within this difficulty class are worth.") .prompt()? .to_string() - } + }, }; points_ranks.push(points_obj); again = inquire::Confirm::new("Do you want to provide another difficulty class?") - .with_default(false) - .prompt()?; + .with_default(false) + .prompt()?; } points_ranks_reference = points_ranks.clone(); points_ranks @@ -161,9 +158,11 @@ pub fn interactive_init() -> inquire::error::InquireResult defaults_difficulty: { if points_ranks_reference.is_empty() { String::new() - } - else { - inquire::Select::new("Please choose the default difficulty class:", points_ranks_reference) + } else { + inquire::Select::new( + "Please choose the default difficulty class:", + points_ranks_reference, + ) .prompt()? .difficulty } @@ -177,8 +176,7 @@ pub fn interactive_init() -> inquire::error::InquireResult if resources.is_empty() { String::from("1") - } - else { + } else { resources } }, @@ -191,20 +189,19 @@ pub fn interactive_init() -> inquire::error::InquireResult if resources.is_empty() { String::from("500M") - } - else { + } else { resources } }, profiles: { println!("You can define several environment profiles below."); - + let mut again = inquire::Confirm::new("Do you want to provide a profile?") - .with_default(false) - .prompt()?; + .with_default(false) + .prompt()?; let mut profiles: Vec = Vec::new(); - while again { + while again { let prof = profile { profile_name: { inquire::Text::new("Profile name:") @@ -214,18 +211,20 @@ pub fn interactive_init() -> inquire::error::InquireResult }, frontend_url: { inquire::Text::new("Frontend URL:") - .with_help_message("The URL of the RNG scoreboard.") // TODO: can definitely say more about why this is significant - .prompt()? + .with_help_message("The URL of the RNG scoreboard.") // TODO: can definitely say more about why this is significant + .prompt()? }, frontend_token: { inquire::Text::new("Frontend token:") - .with_help_message("The token for RNG to authenticate itself into the scoreboard.") // TODO: again, say more - .prompt()? + .with_help_message( + "The token for RNG to authenticate itself into the scoreboard.", + ) // TODO: again, say more + .prompt()? }, challenges_domain: { inquire::Text::new("Challenges domain:") - .with_help_message("Domain that challenges are hosted under.") - .prompt()? + .with_help_message("Domain that challenges are hosted under.") + .prompt()? }, kubecontext: { inquire::Text::new("Kube context:") @@ -239,46 +238,45 @@ pub fn interactive_init() -> inquire::error::InquireResult }, s3_endpoint: { inquire::Text::new("S3 endpoint:") - .with_help_message("The endpoint of the S3 bucket server.") - .prompt()? + .with_help_message("The endpoint of the S3 bucket server.") + .prompt()? }, s3_region: { inquire::Text::new("S3 region:") - .with_help_message("The region that the S3 bucket is hosted.") - .prompt()? + .with_help_message("The region that the S3 bucket is hosted.") + .prompt()? }, s3_accesskey: { inquire::Text::new("S3 access key:") - .with_help_message("The public access key to the S3 bucket.") - .prompt()? + .with_help_message("The public access key to the S3 bucket.") + .prompt()? }, s3_secretaccesskey: { inquire::Text::new("S3 secret key:") - .with_help_message("The secret acess key to the S3 bucket.") - .prompt()? + .with_help_message("The secret acess key to the S3 bucket.") + .prompt()? }, dns_provider: { // TODO : literally all of the external DNS settings inquire::Text::new("DNS provider:") - .with_help_message("The name of the cloud DNS provider being used.") - .with_placeholder("route53") - .prompt()? - } + .with_help_message("The name of the cloud DNS provider being used.") + .with_placeholder("route53") + .prompt()? + }, }; profiles.push(prof); again = inquire::Confirm::new("Do you want to provide another profile?") - .with_default(false) - .prompt()?; + .with_default(false) + .prompt()?; } profiles - } + }, }; return Ok(options); } -pub fn blank_init() -> init_vars -{ +pub fn blank_init() -> init_vars { return init_vars { flag_regex: String::new(), registry_domain: String::new(), @@ -289,33 +287,28 @@ pub fn blank_init() -> init_vars defaults_difficulty: String::new(), defaults_resources_cpu: String::new(), defaults_resources_memory: String::new(), - points: vec![ - points { - difficulty: String::new(), - min: String::new(), - max: String::new() - } - ], - profiles: vec![ - profile { - profile_name: String::from("default"), - frontend_url: String::new(), - frontend_token: String::new(), - challenges_domain: String::new(), - kubecontext: String::new(), - s3_bucket_name: String::new(), - s3_endpoint: String::new(), - s3_region: String::new(), - s3_accesskey: String::new(), - s3_secretaccesskey: String::new(), - dns_provider: String::from("aws") - } - ] + points: vec![points { + difficulty: String::new(), + min: String::new(), + max: String::new(), + }], + profiles: vec![profile { + profile_name: String::from("default"), + frontend_url: String::new(), + frontend_token: String::new(), + challenges_domain: String::new(), + kubecontext: String::new(), + s3_bucket_name: String::new(), + s3_endpoint: String::new(), + s3_region: String::new(), + s3_accesskey: String::new(), + s3_secretaccesskey: String::new(), + dns_provider: String::from("aws"), + }], }; } -pub fn example_init() -> init_vars -{ +pub fn example_init() -> init_vars { return init_vars { flag_regex: String::from("ctf{.*}"), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles registry_domain: String::from("ghcr.io/youraccount"), @@ -330,29 +323,27 @@ pub fn example_init() -> init_vars points { difficulty: String::from("1"), min: String::from("1"), - max: String::from("1337") + max: String::from("1337"), }, points { difficulty: String::from("2"), min: String::from("200"), - max: String::from("500") - } + max: String::from("500"), + }, ], - profiles: vec![ - profile { - profile_name: String::from("default"), - frontend_url: String::from("https://ctf.coolguy.xyz"), - frontend_token: String::from("secretsecretsecret"), - challenges_domain: String::from("chals.coolguy.xyz"), - kubecontext: String::from("ctf-cluster"), - s3_bucket_name: String::from("ctf-bucket"), - s3_endpoint: String::from("s3.coolguy.xyz"), - s3_region: String::from("us-west-2"), - s3_accesskey: String::from("accesskey"), - s3_secretaccesskey: String::from("secretkey"), - dns_provider: String::from("aws") - } - ] + profiles: vec![profile { + profile_name: String::from("default"), + frontend_url: String::from("https://ctf.coolguy.xyz"), + frontend_token: String::from("secretsecretsecret"), + challenges_domain: String::from("chals.coolguy.xyz"), + kubecontext: String::from("ctf-cluster"), + s3_bucket_name: String::from("ctf-bucket"), + s3_endpoint: String::from("s3.coolguy.xyz"), + s3_region: String::from("us-west-2"), + s3_accesskey: String::from("accesskey"), + s3_secretaccesskey: String::from("secretkey"), + dns_provider: String::from("aws"), + }], }; } diff --git a/src/init/templates.rs b/src/init/templates.rs index 18700e6..6ae95d2 100644 --- a/src/init/templates.rs +++ b/src/init/templates.rs @@ -1,4 +1,3 @@ // Embed template file into binary -pub static RCDS: &str = - include_str!("../asset_files/rcds.yaml.j2"); +pub static RCDS: &str = include_str!("../asset_files/rcds.yaml.j2"); diff --git a/src/lib.rs b/src/lib.rs index 85e3043..917754c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ pub mod cluster_setup; pub mod commands; pub mod configparser; pub mod deploy; -pub mod utils; pub mod init; +pub mod utils; #[cfg(test)] mod tests; diff --git a/src/main.rs b/src/main.rs index 31d8a05..b8cee3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,9 +90,6 @@ fn dispatch(cli: cli::Cli) -> anyhow::Result<()> { commands::cluster_setup::run(profile) } - cli::Commands::Init { - interactive, - blank - } => commands::init::run (interactive, blank) + cli::Commands::Init { interactive, blank } => commands::init::run(interactive, blank), } } From 0346bf30852a21b4c54e24ad3e633ed2e9d166c9 Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Tue, 8 Apr 2025 23:25:53 -0700 Subject: [PATCH 03/26] init command, lump external-dns configuration options into 'not our problem' style --- src/asset_files/rcds.yaml.j2 | 8 ++++---- src/commands/init.rs | 5 ++++- src/init/mod.rs | 19 +++---------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index df5176b..ee0bd2b 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -40,7 +40,7 @@ profiles: access_key: {{ prof.s3_accesskey }} secret_key: {{ prof.s3_secretaccesskey }} dns: - provider: {{ prof.dns_provider }} - {{ prof.dns_provider }}: - # TODO: provider-specific external-dns -{% endfor %} \ No newline at end of file + # 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 +{% endfor %} diff --git a/src/commands/init.rs b/src/commands/init.rs index 8e23963..f46b730 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -23,7 +23,6 @@ pub fn run(_interactive: &bool, _blank: &bool) { options = init::example_init(); } - // TODO write to disk let configuration = templatize_init(options); let mut f = match File::create("rcds.yaml") { Ok(t) => t, @@ -39,4 +38,8 @@ pub fn run(_interactive: &bool, _blank: &bool) { exit(1); } } + + // Note about external-dns + println!("Note: external-dns configuration settings will need to be provided in rcds.yaml after file creation, under the `profiles.name.dns` key."); + println!("Reference: https://github.com/bitnami/charts/tree/main/bitnami/external-dns"); } diff --git a/src/init/mod.rs b/src/init/mod.rs index a637767..8ad598e 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -50,10 +50,6 @@ pub struct profile { pub s3_region: String, pub s3_accesskey: String, pub s3_secretaccesskey: String, - // TODO external dns garbage - pub dns_provider: String, - // dns_provider_values: HashMap - // dns_txtOwnerId: Option< } pub fn interactive_init() -> inquire::error::InquireResult { @@ -256,13 +252,6 @@ pub fn interactive_init() -> inquire::error::InquireResult { .with_help_message("The secret acess key to the S3 bucket.") .prompt()? }, - dns_provider: { - // TODO : literally all of the external DNS settings - inquire::Text::new("DNS provider:") - .with_help_message("The name of the cloud DNS provider being used.") - .with_placeholder("route53") - .prompt()? - }, }; profiles.push(prof); @@ -303,7 +292,6 @@ pub fn blank_init() -> init_vars { s3_region: String::new(), s3_accesskey: String::new(), s3_secretaccesskey: String::new(), - dns_provider: String::from("aws"), }], }; } @@ -333,16 +321,15 @@ pub fn example_init() -> init_vars { ], profiles: vec![profile { profile_name: String::from("default"), - frontend_url: String::from("https://ctf.coolguy.xyz"), + frontend_url: String::from("https://ctf.coolguy.invalid"), frontend_token: String::from("secretsecretsecret"), - challenges_domain: String::from("chals.coolguy.xyz"), + challenges_domain: String::from("chals.coolguy.invalid"), kubecontext: String::from("ctf-cluster"), s3_bucket_name: String::from("ctf-bucket"), - s3_endpoint: String::from("s3.coolguy.xyz"), + s3_endpoint: String::from("s3.coolguy.invalid"), s3_region: String::from("us-west-2"), s3_accesskey: String::from("accesskey"), s3_secretaccesskey: String::from("secretkey"), - dns_provider: String::from("aws"), }], }; } From ce5811165427a731e9ac862846fe6bbc91c8c395 Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Wed, 9 Apr 2025 17:35:28 -0700 Subject: [PATCH 04/26] init cmd added inquire placeholders and cleaned up prompt and help messages --- src/asset_files/rcds.yaml.j2 | 3 +- src/init/example_values.rs | 24 ++++++ src/init/mod.rs | 140 +++++++++++++++++++---------------- 3 files changed, 103 insertions(+), 64 deletions(-) create mode 100644 src/init/example_values.rs diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index ee0bd2b..4d672b5 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -26,8 +26,7 @@ deploy: pwn/notsh: true web/bar: true -profiles: -{% for prof in options.profiles %} +profiles: {% for prof in options.profiles %} {{ prof.profile_name}}: frontend_url: {{ prof.frontend_url }} frontend_token: {{ prof.frontend_token }} diff --git a/src/init/example_values.rs b/src/init/example_values.rs new file mode 100644 index 0000000..a04deab --- /dev/null +++ b/src/init/example_values.rs @@ -0,0 +1,24 @@ +// 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 = "1"; +pub static DEFAULTS_RESOURCES_CPU: &str = "1"; +pub static DEFAULTS_RESOURCES_MEMORY: &str = "500M"; +pub static POINTS_DIFFICULTY: &str = "1"; +pub static POINTS_MIN: &str = "200"; +pub static POINTS_MAX: &str = "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"; diff --git a/src/init/mod.rs b/src/init/mod.rs index 8ad598e..9c2155a 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -5,6 +5,7 @@ use serde; use std::fmt; pub mod templates; +pub mod example_values; #[derive(serde::Serialize)] pub struct init_vars { @@ -14,18 +15,18 @@ pub struct init_vars { pub registry_build_pass: String, pub registry_cluster_user: String, pub registry_cluster_pass: String, - pub defaults_difficulty: String, //u64, - pub defaults_resources_cpu: String, //u64, - pub defaults_resources_memory: String, //(u64, Option(String)), + pub defaults_difficulty: String, + pub defaults_resources_cpu: String, + pub defaults_resources_memory: String, pub points: Vec, pub profiles: Vec, } #[derive(Clone, serde::Serialize)] pub struct points { - pub difficulty: String, //u64, - pub min: String, //u64, - pub max: String, //u64 + pub difficulty: String, + pub min: String, + pub max: String, } impl fmt::Display for points { @@ -63,43 +64,47 @@ pub fn interactive_init() -> inquire::error::InquireResult { //TODO: // - also provide regex examples in help // - is this even a good idea to have the user provide the regex - // - with placeholder? + // - what kind of regex is being validated and accepted inquire::Text::new("Flag regex:") .with_help_message("This regex will be used to validate the individual flags of your challenges later.") + .with_placeholder(example_values::FLAG_REGEX) .prompt()? }, registry_domain: { inquire::Text::new ("Container registry:") - .with_help_message("This is the domain of your remote container registry, which includes both the endpoint details and your repository name.") //where you will push images to and where your cluster will pull challenge images from.") // TODO + .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") + .with_placeholder(example_values::REGISTRY_DOMAIN) .prompt()? }, registry_build_user: { - inquire::Text::new ("Container registry user (YOURS):") - .with_help_message("Your username to the remote container registry, which you will use to push containers to.") + inquire::Text::new ("Container registry 'build' user:") + .with_help_message("The username that will be used to push built containers.") + .with_placeholder(example_values::REGISTRY_BUILD_USER) .prompt()? }, // TODO: do we actually want to be in charge of these credentials vs letting the container building utility take care of it? registry_build_pass: { - inquire::Password::new("Container registry password (YOURS):") - .with_help_message("Your password to the remote container registry, which you will use to push containers to.") // TODO: could this support username:pat too? + inquire::Password::new("Container registry 'build' password:") + .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? .with_display_mode(inquire::PasswordDisplayMode::Masked) .with_custom_confirmation_message("Enter again:") .prompt()? }, registry_cluster_user: { - inquire::Text::new ("Container registry user (CLUSTER'S):") - .with_help_message("The cluster's username to the remote container registry, which it will use to pull containers from.") + inquire::Text::new ("Container registry 'cluster' user:") + .with_help_message("The username that the cluster will use to pull locally-built containers.") + .with_placeholder(example_values::REGISTRY_CLUSTER_USER) .prompt()? }, // TODO: would the cluster not use a token of some sort? registry_cluster_pass: { - inquire::Password::new("Container registry password (CLUSTER'S):") - .with_help_message("The cluster's password to the remote container registry, which it will use to pull containers from.") + inquire::Password::new("Container registry 'cluster' password:") + .with_help_message("The password to the 'cluster' user account") .with_display_mode(inquire::PasswordDisplayMode::Masked) .with_custom_confirmation_message("Enter again:") .prompt()? @@ -119,23 +124,26 @@ pub fn interactive_init() -> inquire::error::InquireResult { // default parser calls std::u64::from_str .with_error_message("Please type a valid number.") .with_help_message("The rank of the difficulty class as an unsigned integer, with lower numbers being \"easier.\"") + .with_placeholder(example_values::POINTS_DIFFICULTY) .prompt()? .to_string() }, // TODO: support static-point challenges min: { - inquire::CustomType::::new("Minimum number of points:") + inquire::CustomType::::new("Minimum points:") // default parser calls std::u64::from_str .with_error_message("Please type a valid number.") - .with_help_message("Challenge points are dynamic: the minimum number of points that challenges within this difficulty class are worth.") + .with_help_message("Challenge points are dynamic. The minimum number of points that challenges within this difficulty class are worth.") + .with_placeholder(example_values::POINTS_MIN) .prompt()? .to_string() }, max: { - inquire::CustomType::::new("Maximum number of points:") + inquire::CustomType::::new("Maximum points:") // default parser calls std::u64::from_str .with_error_message("Please type a valid number.") - .with_help_message("Challenge points are dynamic: the maximum number of points that challenges within this difficulty class are worth.") + .with_help_message("The maximum number of points that challenges within this difficulty class are worth.") + .with_placeholder(example_values::POINTS_MAX) .prompt()? .to_string() }, @@ -150,7 +158,6 @@ pub fn interactive_init() -> inquire::error::InquireResult { points_ranks }, - // TODO: how much format validation should these two do now vs offloading to validate() later? current inquire replacement calls are temporary and do the zero checking, just grabbing a String defaults_difficulty: { if points_ranks_reference.is_empty() { String::new() @@ -165,26 +172,26 @@ pub fn interactive_init() -> inquire::error::InquireResult { }, defaults_resources_cpu: { - let resources = inquire::Text::new("Default limit of CPUs per challenge:") - .with_help_message("The maximum limit of CPU resources per instance of challenge deployment (\"pod\").") - .with_placeholder("1") + let resources = inquire::Text::new("Default CPU limit:") + .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") + .with_placeholder(example_values::DEFAULTS_RESOURCES_CPU) .prompt()?; if resources.is_empty() { - String::from("1") + String::from(example_values::DEFAULTS_RESOURCES_CPU) } else { resources } }, defaults_resources_memory: { - let resources = inquire::Text::new("Default limit of memory per challenge:") - .with_help_message("The maximum limit of memory resources per instance of challenge deployment (\"pod\").") - .with_placeholder("500M") + let resources = inquire::Text::new("Default memory limit:") + .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") + .with_placeholder(example_values::DEFAULTS_RESOURCES_MEMORY) .prompt()?; if resources.is_empty() { - String::from("500M") + String::from(example_values::DEFAULTS_RESOURCES_MEMORY) } else { resources } @@ -202,54 +209,63 @@ pub fn interactive_init() -> inquire::error::InquireResult { profile_name: { inquire::Text::new("Profile name:") .with_help_message("The name of the deployment profile. One profile named \"default\" is recommended. You can add additional profiles.") - .with_placeholder("default") + .with_placeholder(example_values::PROFILES_PROFILE_NAME) .prompt()? }, frontend_url: { inquire::Text::new("Frontend URL:") - .with_help_message("The URL of the RNG scoreboard.") // TODO: can definitely say more about why this is significant + .with_help_message("The URL of the RNG scoreboard.") + .with_placeholder(example_values::PROFILES_FRONTEND_URL) .prompt()? }, frontend_token: { inquire::Text::new("Frontend token:") .with_help_message( - "The token for RNG to authenticate itself into the scoreboard.", - ) // TODO: again, say more + "The token to authenticate into the RNG scoreboard.", + ) + .with_placeholder(example_values::PROFILES_FRONTEND_TOKEN) .prompt()? }, challenges_domain: { inquire::Text::new("Challenges domain:") .with_help_message("Domain that challenges are hosted under.") + .with_placeholder(example_values::PROFILES_CHALLENGES_DOMAIN) .prompt()? }, kubecontext: { - inquire::Text::new("Kube context:") - .with_help_message("The name of the context that kubectl looks for to interface with the cluster.") + inquire::Text::new("Kubecontext name:") + .with_help_message("The name of the context that kubectl uses to connect to the cluster.") + .with_placeholder(example_values::PROFILES_KUBECONTEXT) .prompt()? }, s3_bucket_name: { inquire::Text::new("S3 bucket name:") - .with_help_message("Challenge artifacts and static files will be hosted on and served from S3. The name of the S3 bucket.") + .with_help_message("Challenge artifacts and static files will be hosted on S3. The name of the S3 bucket.") + .with_placeholder(example_values::PROFILES_S3_BUCKET_NAME) .prompt()? }, s3_endpoint: { inquire::Text::new("S3 endpoint:") .with_help_message("The endpoint of the S3 bucket server.") + .with_placeholder(example_values::PROFILES_S3_ENDPOINT) .prompt()? }, s3_region: { inquire::Text::new("S3 region:") - .with_help_message("The region that the S3 bucket is hosted.") + .with_help_message("The region where the S3 bucket is hosted.") + .with_placeholder(example_values::PROFILES_S3_REGION) .prompt()? }, s3_accesskey: { inquire::Text::new("S3 access key:") .with_help_message("The public access key to the S3 bucket.") + .with_placeholder(example_values::PROFILES_S3_ACCESSKEY) .prompt()? }, s3_secretaccesskey: { inquire::Text::new("S3 secret key:") .with_help_message("The secret acess key to the S3 bucket.") + .with_placeholder(example_values::PROFILES_S3_SECRETACCESSKEY) .prompt()? }, }; @@ -282,7 +298,7 @@ pub fn blank_init() -> init_vars { max: String::new(), }], profiles: vec![profile { - profile_name: String::from("default"), + profile_name: String::from(example_values::PROFILES_PROFILE_NAME), frontend_url: String::new(), frontend_token: String::new(), challenges_domain: String::new(), @@ -298,38 +314,38 @@ pub fn blank_init() -> init_vars { pub fn example_init() -> init_vars { return init_vars { - flag_regex: String::from("ctf{.*}"), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles - registry_domain: String::from("ghcr.io/youraccount"), - registry_build_user: String::from("admin"), - registry_build_pass: String::from("notrealcreds"), - registry_cluster_user: String::from("cluster_user"), - registry_cluster_pass: String::from("alsofake"), - defaults_difficulty: String::from("1"), - defaults_resources_cpu: String::from("1"), - defaults_resources_memory: String::from("500M"), + flag_regex: String::from(example_values::FLAG_REGEX), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles + registry_domain: String::from(example_values::REGISTRY_DOMAIN), + registry_build_user: String::from(example_values::REGISTRY_BUILD_USER), + registry_build_pass: String::from(example_values::REGISTRY_BUILD_PASS), + registry_cluster_user: String::from(example_values::REGISTRY_CLUSTER_USER), + registry_cluster_pass: String::from(example_values::REGISTRY_CLUSTER_USER), + defaults_difficulty: String::from(example_values::DEFAULTS_DIFFICULTY), + defaults_resources_cpu: String::from(example_values::DEFAULTS_RESOURCES_CPU), + defaults_resources_memory: String::from(example_values::DEFAULTS_RESOURCES_MEMORY), points: vec![ points { - difficulty: String::from("1"), - min: String::from("1"), - max: String::from("1337"), + difficulty: String::from(example_values::POINTS_DIFFICULTY), + min: String::from(example_values::POINTS_MIN), + max: String::from(example_values::POINTS_MAX), }, points { difficulty: String::from("2"), - min: String::from("200"), - max: String::from("500"), + min: String::from("1"), + max: String::from("1337"), }, ], profiles: vec![profile { - profile_name: String::from("default"), - frontend_url: String::from("https://ctf.coolguy.invalid"), - frontend_token: String::from("secretsecretsecret"), - challenges_domain: String::from("chals.coolguy.invalid"), - kubecontext: String::from("ctf-cluster"), - s3_bucket_name: String::from("ctf-bucket"), - s3_endpoint: String::from("s3.coolguy.invalid"), - s3_region: String::from("us-west-2"), - s3_accesskey: String::from("accesskey"), - s3_secretaccesskey: String::from("secretkey"), + profile_name: String::from(example_values::PROFILES_PROFILE_NAME), + frontend_url: String::from(example_values::PROFILES_FRONTEND_URL), + frontend_token: String::from(example_values::PROFILES_FRONTEND_TOKEN), + challenges_domain: String::from(example_values::PROFILES_CHALLENGES_DOMAIN), + kubecontext: String::from(example_values::PROFILES_KUBECONTEXT), + s3_bucket_name: String::from(example_values::PROFILES_S3_BUCKET_NAME), + s3_endpoint: String::from(example_values::PROFILES_S3_ENDPOINT), + s3_region: String::from(example_values::PROFILES_S3_REGION), + s3_accesskey: String::from(example_values::PROFILES_S3_ACCESSKEY), + s3_secretaccesskey: String::from(example_values::PROFILES_S3_SECRETACCESSKEY), }], }; } From 5b9b11e608e23f4f93db2e35fbc7c34c1447867f Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Wed, 9 Apr 2025 18:30:48 -0700 Subject: [PATCH 05/26] init command - allowed difficulty class to be a string instead of a number identifier --- src/init/example_values.rs | 2 +- src/init/mod.rs | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/init/example_values.rs b/src/init/example_values.rs index a04deab..91309bb 100644 --- a/src/init/example_values.rs +++ b/src/init/example_values.rs @@ -9,7 +9,7 @@ pub static REGISTRY_CLUSTER_PASS: &str = "alsofake"; pub static DEFAULTS_DIFFICULTY: &str = "1"; pub static DEFAULTS_RESOURCES_CPU: &str = "1"; pub static DEFAULTS_RESOURCES_MEMORY: &str = "500M"; -pub static POINTS_DIFFICULTY: &str = "1"; +pub static POINTS_DIFFICULTY: &str = "easy"; pub static POINTS_MIN: &str = "200"; pub static POINTS_MAX: &str = "500"; pub static PROFILES_PROFILE_NAME: &str = "default"; diff --git a/src/init/mod.rs b/src/init/mod.rs index 9c2155a..fc875a2 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -85,7 +85,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { .prompt()? }, - // TODO: do we actually want to be in charge of these credentials vs letting the container building utility take care of it? + // TODO: do we actually want to be in charge of these credentials vs expecting the local building utility already be logged in? registry_build_pass: { inquire::Password::new("Container registry 'build' password:") .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? @@ -115,24 +115,21 @@ pub fn interactive_init() -> inquire::error::InquireResult { let mut again = inquire::Confirm::new("Do you want to provide a difficulty class?") .with_default(false) .prompt()?; + println!("Challenge points are dynamic. For a static challenge, simply set minimum and maximum points to the same value."); let mut points_ranks: Vec = Vec::new(); while again { let points_obj = points { - // TODO: theres no reason these need to be numbers instead of open strings, e.g. for "easy" difficulty: { - inquire::CustomType::::new("Difficulty rank:") - // default parser calls std::u64::from_str - .with_error_message("Please type a valid number.") - .with_help_message("The rank of the difficulty class as an unsigned integer, with lower numbers being \"easier.\"") + inquire::Text::new("Difficulty class:") + .with_validator(inquire::required!("Please provide a name.")) + .with_help_message("The name of the difficulty class.") .with_placeholder(example_values::POINTS_DIFFICULTY) .prompt()? - .to_string() }, // TODO: support static-point challenges min: { inquire::CustomType::::new("Minimum points:") - // default parser calls std::u64::from_str - .with_error_message("Please type a valid number.") + .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str .with_help_message("Challenge points are dynamic. The minimum number of points that challenges within this difficulty class are worth.") .with_placeholder(example_values::POINTS_MIN) .prompt()? @@ -140,8 +137,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { }, max: { inquire::CustomType::::new("Maximum points:") - // default parser calls std::u64::from_str - .with_error_message("Please type a valid number.") + .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str .with_help_message("The maximum number of points that challenges within this difficulty class are worth.") .with_placeholder(example_values::POINTS_MAX) .prompt()? From cbe9e946ab74b42f4f59d9531acedd24573587b5 Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Wed, 9 Apr 2025 18:46:53 -0700 Subject: [PATCH 06/26] changed structs to upper pascal case --- src/commands/init.rs | 2 +- src/init/mod.rs | 54 ++++++++++++++++++++++---------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index f46b730..f376042 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -7,7 +7,7 @@ use crate::init::{self as init, templatize_init}; use crate::{access_handlers::frontend, commands::deploy}; pub fn run(_interactive: &bool, _blank: &bool) { - let options: init::init_vars; + let options: init::InitVars; if *_interactive { options = match init::interactive_init() { diff --git a/src/init/mod.rs b/src/init/mod.rs index fc875a2..651a09f 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -8,7 +8,7 @@ pub mod templates; pub mod example_values; #[derive(serde::Serialize)] -pub struct init_vars { +pub struct InitVars { pub flag_regex: String, pub registry_domain: String, pub registry_build_user: String, @@ -18,18 +18,18 @@ pub struct init_vars { pub defaults_difficulty: String, pub defaults_resources_cpu: String, pub defaults_resources_memory: String, - pub points: Vec, - pub profiles: Vec, + pub points: Vec, + pub profiles: Vec, } #[derive(Clone, serde::Serialize)] -pub struct points { +pub struct Points { pub difficulty: String, pub min: String, pub max: String, } -impl fmt::Display for points { +impl fmt::Display for Points { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -40,7 +40,7 @@ impl fmt::Display for points { } #[derive(serde::Serialize)] -pub struct profile { +pub struct Profile { pub profile_name: String, pub frontend_url: String, pub frontend_token: String, @@ -53,13 +53,13 @@ pub struct profile { pub s3_secretaccesskey: String, } -pub fn interactive_init() -> inquire::error::InquireResult { +pub fn interactive_init() -> inquire::error::InquireResult { println!("For all prompts below, simply press Enter to leave blank."); println!("All fields that can be set in rcds.yaml can also be set via environment variables."); - let points_ranks_reference: Vec; + let points_ranks_reference: Vec; - let options = init_vars { + let options = InitVars { flag_regex: { //TODO: // - also provide regex examples in help @@ -116,9 +116,9 @@ pub fn interactive_init() -> inquire::error::InquireResult { .with_default(false) .prompt()?; println!("Challenge points are dynamic. For a static challenge, simply set minimum and maximum points to the same value."); - let mut points_ranks: Vec = Vec::new(); + let mut points_ranks: Vec = Vec::new(); while again { - let points_obj = points { + let points_obj = Points { difficulty: { inquire::Text::new("Difficulty class:") .with_validator(inquire::required!("Please provide a name.")) @@ -130,7 +130,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { min: { inquire::CustomType::::new("Minimum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str - .with_help_message("Challenge points are dynamic. The minimum number of points that challenges within this difficulty class are worth.") + .with_help_message("The minimum number of points that challenges within this difficulty class are worth.") .with_placeholder(example_values::POINTS_MIN) .prompt()? .to_string() @@ -196,15 +196,15 @@ pub fn interactive_init() -> inquire::error::InquireResult { profiles: { println!("You can define several environment profiles below."); - let mut again = inquire::Confirm::new("Do you want to provide a profile?") + let mut again = inquire::Confirm::new("Do you want to provide a Profile?") .with_default(false) .prompt()?; - let mut profiles: Vec = Vec::new(); + let mut profiles: Vec = Vec::new(); while again { - let prof = profile { + let prof = Profile { profile_name: { inquire::Text::new("Profile name:") - .with_help_message("The name of the deployment profile. One profile named \"default\" is recommended. You can add additional profiles.") + .with_help_message("The name of the deployment Profile. One Profile named \"default\" is recommended. You can add additional profiles.") .with_placeholder(example_values::PROFILES_PROFILE_NAME) .prompt()? }, @@ -267,7 +267,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { }; profiles.push(prof); - again = inquire::Confirm::new("Do you want to provide another profile?") + again = inquire::Confirm::new("Do you want to provide another Profile?") .with_default(false) .prompt()?; } @@ -277,8 +277,8 @@ pub fn interactive_init() -> inquire::error::InquireResult { return Ok(options); } -pub fn blank_init() -> init_vars { - return init_vars { +pub fn blank_init() -> InitVars { + return InitVars { flag_regex: String::new(), registry_domain: String::new(), registry_build_user: String::new(), @@ -288,12 +288,12 @@ pub fn blank_init() -> init_vars { defaults_difficulty: String::new(), defaults_resources_cpu: String::new(), defaults_resources_memory: String::new(), - points: vec![points { + points: vec![Points { difficulty: String::new(), min: String::new(), max: String::new(), }], - profiles: vec![profile { + profiles: vec![Profile { profile_name: String::from(example_values::PROFILES_PROFILE_NAME), frontend_url: String::new(), frontend_token: String::new(), @@ -308,8 +308,8 @@ pub fn blank_init() -> init_vars { }; } -pub fn example_init() -> init_vars { - return init_vars { +pub fn example_init() -> InitVars { + return InitVars { flag_regex: String::from(example_values::FLAG_REGEX), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles registry_domain: String::from(example_values::REGISTRY_DOMAIN), registry_build_user: String::from(example_values::REGISTRY_BUILD_USER), @@ -320,18 +320,18 @@ pub fn example_init() -> init_vars { defaults_resources_cpu: String::from(example_values::DEFAULTS_RESOURCES_CPU), defaults_resources_memory: String::from(example_values::DEFAULTS_RESOURCES_MEMORY), points: vec![ - points { + Points { difficulty: String::from(example_values::POINTS_DIFFICULTY), min: String::from(example_values::POINTS_MIN), max: String::from(example_values::POINTS_MAX), }, - points { + Points { difficulty: String::from("2"), min: String::from("1"), max: String::from("1337"), }, ], - profiles: vec![profile { + profiles: vec![Profile { profile_name: String::from(example_values::PROFILES_PROFILE_NAME), frontend_url: String::from(example_values::PROFILES_FRONTEND_URL), frontend_token: String::from(example_values::PROFILES_FRONTEND_TOKEN), @@ -346,7 +346,7 @@ pub fn example_init() -> init_vars { }; } -pub fn templatize_init(options: init_vars) -> String { +pub fn templatize_init(options: InitVars) -> String { let filled_template = minijinja::render!(templates::RCDS, options); return filled_template; } From cb3628792045f60609568c2fd993dc105ab236f1 Mon Sep 17 00:00:00 2001 From: Casey Colley Date: Wed, 9 Apr 2025 19:25:51 -0700 Subject: [PATCH 07/26] cleaning up & addressing robert's feedback in the old branch/PR --- src/asset_files/rcds.yaml.j2 | 3 ++- src/init/mod.rs | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index 4d672b5..1e012f8 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -18,7 +18,7 @@ points: {% for pts in options.points %} min: {{ pts.min }} max: {{ pts.max }} {% endfor %} -# TODO +# TODO: templatize the deploy section deploy: # control challenge deployment status explicitly per environment/profile testing: @@ -31,6 +31,7 @@ profiles: {% for prof in options.profiles %} frontend_url: {{ prof.frontend_url }} frontend_token: {{ prof.frontend_token }} challenges_domain: {{ prof.challenges_domain }} + # TODO: kubeconfig kubecontext: {{ prof.kubecontext }} s3: bucket_name: {{ prof.s3_bucket_name }} diff --git a/src/init/mod.rs b/src/init/mod.rs index 651a09f..f9fd7a9 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -61,10 +61,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { let options = InitVars { flag_regex: { - //TODO: - // - also provide regex examples in help - // - is this even a good idea to have the user provide the regex - // - what kind of regex is being validated and accepted + //TODO: what flavor of regex is being validated and accepted inquire::Text::new("Flag regex:") .with_help_message("This regex will be used to validate the individual flags of your challenges later.") .with_placeholder(example_values::FLAG_REGEX) @@ -101,7 +98,6 @@ pub fn interactive_init() -> inquire::error::InquireResult { .prompt()? }, - // TODO: would the cluster not use a token of some sort? registry_cluster_pass: { inquire::Password::new("Container registry 'cluster' password:") .with_help_message("The password to the 'cluster' user account") @@ -126,7 +122,6 @@ pub fn interactive_init() -> inquire::error::InquireResult { .with_placeholder(example_values::POINTS_DIFFICULTY) .prompt()? }, - // TODO: support static-point challenges min: { inquire::CustomType::::new("Minimum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str @@ -310,7 +305,7 @@ pub fn blank_init() -> InitVars { pub fn example_init() -> InitVars { return InitVars { - flag_regex: String::from(example_values::FLAG_REGEX), // TODO: do that wildcard in the most common regex flavor since Rust regex supports multiple styles + flag_regex: String::from(example_values::FLAG_REGEX), registry_domain: String::from(example_values::REGISTRY_DOMAIN), registry_build_user: String::from(example_values::REGISTRY_BUILD_USER), registry_build_pass: String::from(example_values::REGISTRY_BUILD_PASS), From b75b3d6d85b9bf770ab0cc361f942fa159001fab Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Sat, 27 Sep 2025 12:25:39 -0700 Subject: [PATCH 08/26] Fix result handling in init command runner Signed-off-by: Robert Detjens --- src/commands/init.rs | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index f376042..107de83 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,22 +1,17 @@ -use simplelog::error; +use anyhow::Result; use std::fs::File; use std::io::Write; use std::process::exit; +use tracing::error; use crate::init::{self as init, templatize_init}; use crate::{access_handlers::frontend, commands::deploy}; -pub fn run(_interactive: &bool, _blank: &bool) { +pub fn run(_interactive: &bool, _blank: &bool) -> Result<()> { let options: init::InitVars; if *_interactive { - options = match init::interactive_init() { - Ok(t) => t, - Err(e) => { - error!("Error in init: {e}"); - exit(1); - } - }; + options = init::interactive_init()?; } else if *_blank { options = init::blank_init(); } else { @@ -24,22 +19,13 @@ pub fn run(_interactive: &bool, _blank: &bool) { } let configuration = templatize_init(options); - let mut f = match File::create("rcds.yaml") { - Ok(t) => t, - Err(e) => { - error!("Error in init: {e}"); - exit(1); - } - }; - match f.write_all(configuration.as_bytes()) { - Ok(_) => (), - Err(e) => { - error!("Error in init: {e}"); - exit(1); - } - } + + let mut f = File::create("rcds.yaml")?; + f.write_all(configuration.as_bytes())?; // Note about external-dns println!("Note: external-dns configuration settings will need to be provided in rcds.yaml after file creation, under the `profiles.name.dns` key."); println!("Reference: https://github.com/bitnami/charts/tree/main/bitnami/external-dns"); + + Ok(()) } From 4b3723c0d40ac370e39597954ec45ef87c8ebf25 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Sat, 27 Sep 2025 12:36:42 -0700 Subject: [PATCH 09/26] Clippy fixes, use Default trait for blank Signed-off-by: Robert Detjens --- src/init/mod.rs | 95 ++++++++++++++++++------------------------------- 1 file changed, 34 insertions(+), 61 deletions(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index f9fd7a9..e698d10 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -4,10 +4,10 @@ use regex::Regex; use serde; use std::fmt; -pub mod templates; pub mod example_values; +pub mod templates; -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Default)] pub struct InitVars { pub flag_regex: String, pub registry_domain: String, @@ -22,7 +22,7 @@ pub struct InitVars { pub profiles: Vec, } -#[derive(Clone, serde::Serialize)] +#[derive(Clone, serde::Serialize, Default)] pub struct Points { pub difficulty: String, pub min: String, @@ -39,7 +39,7 @@ impl fmt::Display for Points { } } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, Default)] pub struct Profile { pub profile_name: String, pub frontend_url: String, @@ -70,40 +70,42 @@ pub fn interactive_init() -> inquire::error::InquireResult { registry_domain: { inquire::Text::new ("Container registry:") - .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") + .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") .with_placeholder(example_values::REGISTRY_DOMAIN) .prompt()? }, registry_build_user: { - inquire::Text::new ("Container registry 'build' user:") - .with_help_message("The username that will be used to push built containers.") - .with_placeholder(example_values::REGISTRY_BUILD_USER) - .prompt()? + inquire::Text::new("Container registry 'build' user:") + .with_help_message("The username that will be used to push built containers.") + .with_placeholder(example_values::REGISTRY_BUILD_USER) + .prompt()? }, // TODO: do we actually want to be in charge of these credentials vs expecting the local building utility already be logged in? registry_build_pass: { inquire::Password::new("Container registry 'build' password:") - .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? + .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? }, registry_cluster_user: { - inquire::Text::new ("Container registry 'cluster' user:") - .with_help_message("The username that the cluster will use to pull locally-built containers.") - .with_placeholder(example_values::REGISTRY_CLUSTER_USER) - .prompt()? + inquire::Text::new("Container registry 'cluster' user:") + .with_help_message( + "The username that the cluster will use to pull locally-built containers.", + ) + .with_placeholder(example_values::REGISTRY_CLUSTER_USER) + .prompt()? }, registry_cluster_pass: { inquire::Password::new("Container registry 'cluster' password:") - .with_help_message("The password to the 'cluster' user account") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? + .with_help_message("The password to the 'cluster' user account") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? }, points: { @@ -117,10 +119,10 @@ pub fn interactive_init() -> inquire::error::InquireResult { let points_obj = Points { difficulty: { inquire::Text::new("Difficulty class:") - .with_validator(inquire::required!("Please provide a name.")) - .with_help_message("The name of the difficulty class.") - .with_placeholder(example_values::POINTS_DIFFICULTY) - .prompt()? + .with_validator(inquire::required!("Please provide a name.")) + .with_help_message("The name of the difficulty class.") + .with_placeholder(example_values::POINTS_DIFFICULTY) + .prompt()? }, min: { inquire::CustomType::::new("Minimum points:") @@ -211,9 +213,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { }, frontend_token: { inquire::Text::new("Frontend token:") - .with_help_message( - "The token to authenticate into the RNG scoreboard.", - ) + .with_help_message("The token to authenticate into the RNG scoreboard.") .with_placeholder(example_values::PROFILES_FRONTEND_TOKEN) .prompt()? }, @@ -269,42 +269,16 @@ pub fn interactive_init() -> inquire::error::InquireResult { profiles }, }; - return Ok(options); + + Ok(options) } pub fn blank_init() -> InitVars { - return InitVars { - flag_regex: String::new(), - registry_domain: String::new(), - registry_build_user: String::new(), - registry_build_pass: String::new(), - registry_cluster_user: String::new(), - registry_cluster_pass: String::new(), - defaults_difficulty: String::new(), - defaults_resources_cpu: String::new(), - defaults_resources_memory: String::new(), - points: vec![Points { - difficulty: String::new(), - min: String::new(), - max: String::new(), - }], - profiles: vec![Profile { - profile_name: String::from(example_values::PROFILES_PROFILE_NAME), - frontend_url: String::new(), - frontend_token: String::new(), - challenges_domain: String::new(), - kubecontext: String::new(), - s3_bucket_name: String::new(), - s3_endpoint: String::new(), - s3_region: String::new(), - s3_accesskey: String::new(), - s3_secretaccesskey: String::new(), - }], - }; + InitVars::default() } pub fn example_init() -> InitVars { - return InitVars { + InitVars { flag_regex: String::from(example_values::FLAG_REGEX), registry_domain: String::from(example_values::REGISTRY_DOMAIN), registry_build_user: String::from(example_values::REGISTRY_BUILD_USER), @@ -338,10 +312,9 @@ pub fn example_init() -> InitVars { s3_accesskey: String::from(example_values::PROFILES_S3_ACCESSKEY), s3_secretaccesskey: String::from(example_values::PROFILES_S3_SECRETACCESSKEY), }], - }; + } } pub fn templatize_init(options: InitVars) -> String { - let filled_template = minijinja::render!(templates::RCDS, options); - return filled_template; + minijinja::render!(templates::RCDS, options) } From bdfd2bd9c52cf52afba75b23a0915ef15582db3a Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Sat, 27 Sep 2025 12:45:20 -0700 Subject: [PATCH 10/26] Tracing logs and use render_strict helper Signed-off-by: Robert Detjens --- src/commands/init.rs | 20 +++++++++----------- src/init/mod.rs | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index 107de83..b588ae8 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use std::fs::File; use std::io::Write; use std::process::exit; @@ -7,18 +7,16 @@ use tracing::error; use crate::init::{self as init, templatize_init}; use crate::{access_handlers::frontend, commands::deploy}; -pub fn run(_interactive: &bool, _blank: &bool) -> Result<()> { - let options: init::InitVars; - - if *_interactive { - options = init::interactive_init()?; - } else if *_blank { - options = init::blank_init(); +pub fn run(interactive: &bool, blank: &bool) -> Result<()> { + let options = if *interactive { + init::interactive_init()? + } else if *blank { + init::blank_init() } else { - options = init::example_init(); - } + init::example_init() + }; - let configuration = templatize_init(options); + let configuration = templatize_init(options).context("could not render template")?; let mut f = File::create("rcds.yaml")?; f.write_all(configuration.as_bytes())?; diff --git a/src/init/mod.rs b/src/init/mod.rs index e698d10..d853454 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -1,13 +1,17 @@ +use anyhow::Result; use inquire; use minijinja; use regex::Regex; use serde; use std::fmt; +use tracing::{debug, error, info, trace, warn}; + +use crate::utils::render_strict; pub mod example_values; pub mod templates; -#[derive(serde::Serialize, Default)] +#[derive(serde::Serialize, Default, Debug)] pub struct InitVars { pub flag_regex: String, pub registry_domain: String, @@ -22,7 +26,7 @@ pub struct InitVars { pub profiles: Vec, } -#[derive(Clone, serde::Serialize, Default)] +#[derive(Clone, serde::Serialize, Default, Debug)] pub struct Points { pub difficulty: String, pub min: String, @@ -39,7 +43,7 @@ impl fmt::Display for Points { } } -#[derive(serde::Serialize, Default)] +#[derive(serde::Serialize, Default, Debug)] pub struct Profile { pub profile_name: String, pub frontend_url: String, @@ -274,10 +278,12 @@ pub fn interactive_init() -> inquire::error::InquireResult { } pub fn blank_init() -> InitVars { + trace!("building blank config"); InitVars::default() } pub fn example_init() -> InitVars { + trace!("building example values config"); InitVars { flag_regex: String::from(example_values::FLAG_REGEX), registry_domain: String::from(example_values::REGISTRY_DOMAIN), @@ -315,6 +321,7 @@ pub fn example_init() -> InitVars { } } -pub fn templatize_init(options: InitVars) -> String { - minijinja::render!(templates::RCDS, options) +pub fn templatize_init(options: InitVars) -> Result { + debug!("rendering template with {options:?}"); + render_strict(templates::RCDS, minijinja::context! {options}) } From 77498dc53f7504ce2ff752c73f7e88d58401ebfe Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Sun, 28 Sep 2025 13:34:33 -0700 Subject: [PATCH 11/26] use to_string, inquire default values Signed-off-by: Robert Detjens --- src/init/mod.rs | 74 +++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 42 deletions(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index d853454..e41f55e 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -168,30 +168,20 @@ pub fn interactive_init() -> inquire::error::InquireResult { } }, - defaults_resources_cpu: { - let resources = inquire::Text::new("Default CPU limit:") + defaults_resources_cpu: inquire::Text::new("Default CPU limit:") .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") .with_placeholder(example_values::DEFAULTS_RESOURCES_CPU) - .prompt()?; - - if resources.is_empty() { - String::from(example_values::DEFAULTS_RESOURCES_CPU) - } else { - resources - } - }, + .with_default(example_values::DEFAULTS_RESOURCES_CPU) + .prompt()? + , defaults_resources_memory: { - let resources = inquire::Text::new("Default memory limit:") + inquire::Text::new("Default memory limit:") .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") .with_placeholder(example_values::DEFAULTS_RESOURCES_MEMORY) - .prompt()?; + .with_default(example_values::DEFAULTS_RESOURCES_MEMORY) + .prompt()? - if resources.is_empty() { - String::from(example_values::DEFAULTS_RESOURCES_MEMORY) - } else { - resources - } }, profiles: { @@ -285,38 +275,38 @@ pub fn blank_init() -> InitVars { pub fn example_init() -> InitVars { trace!("building example values config"); InitVars { - flag_regex: String::from(example_values::FLAG_REGEX), - registry_domain: String::from(example_values::REGISTRY_DOMAIN), - registry_build_user: String::from(example_values::REGISTRY_BUILD_USER), - registry_build_pass: String::from(example_values::REGISTRY_BUILD_PASS), - registry_cluster_user: String::from(example_values::REGISTRY_CLUSTER_USER), - registry_cluster_pass: String::from(example_values::REGISTRY_CLUSTER_USER), - defaults_difficulty: String::from(example_values::DEFAULTS_DIFFICULTY), - defaults_resources_cpu: String::from(example_values::DEFAULTS_RESOURCES_CPU), - defaults_resources_memory: String::from(example_values::DEFAULTS_RESOURCES_MEMORY), + flag_regex: example_values::FLAG_REGEX.to_string(), + registry_domain: example_values::REGISTRY_DOMAIN.to_string(), + registry_build_user: example_values::REGISTRY_BUILD_USER.to_string(), + registry_build_pass: example_values::REGISTRY_BUILD_PASS.to_string(), + registry_cluster_user: example_values::REGISTRY_CLUSTER_USER.to_string(), + registry_cluster_pass: example_values::REGISTRY_CLUSTER_USER.to_string(), + defaults_difficulty: example_values::DEFAULTS_DIFFICULTY.to_string(), + defaults_resources_cpu: example_values::DEFAULTS_RESOURCES_CPU.to_string(), + defaults_resources_memory: example_values::DEFAULTS_RESOURCES_MEMORY.to_string(), points: vec![ Points { - difficulty: String::from(example_values::POINTS_DIFFICULTY), - min: String::from(example_values::POINTS_MIN), - max: String::from(example_values::POINTS_MAX), + difficulty: example_values::POINTS_DIFFICULTY.to_string(), + min: example_values::POINTS_MIN.to_string(), + max: example_values::POINTS_MAX.to_string(), }, Points { - difficulty: String::from("2"), - min: String::from("1"), - max: String::from("1337"), + difficulty: "2".to_string(), + min: "1".to_string(), + max: "1337".to_string(), }, ], profiles: vec![Profile { - profile_name: String::from(example_values::PROFILES_PROFILE_NAME), - frontend_url: String::from(example_values::PROFILES_FRONTEND_URL), - frontend_token: String::from(example_values::PROFILES_FRONTEND_TOKEN), - challenges_domain: String::from(example_values::PROFILES_CHALLENGES_DOMAIN), - kubecontext: String::from(example_values::PROFILES_KUBECONTEXT), - s3_bucket_name: String::from(example_values::PROFILES_S3_BUCKET_NAME), - s3_endpoint: String::from(example_values::PROFILES_S3_ENDPOINT), - s3_region: String::from(example_values::PROFILES_S3_REGION), - s3_accesskey: String::from(example_values::PROFILES_S3_ACCESSKEY), - s3_secretaccesskey: String::from(example_values::PROFILES_S3_SECRETACCESSKEY), + profile_name: example_values::PROFILES_PROFILE_NAME.to_string(), + frontend_url: example_values::PROFILES_FRONTEND_URL.to_string(), + frontend_token: example_values::PROFILES_FRONTEND_TOKEN.to_string(), + challenges_domain: example_values::PROFILES_CHALLENGES_DOMAIN.to_string(), + kubecontext: example_values::PROFILES_KUBECONTEXT.to_string(), + s3_bucket_name: example_values::PROFILES_S3_BUCKET_NAME.to_string(), + s3_endpoint: example_values::PROFILES_S3_ENDPOINT.to_string(), + s3_region: example_values::PROFILES_S3_REGION.to_string(), + s3_accesskey: example_values::PROFILES_S3_ACCESSKEY.to_string(), + s3_secretaccesskey: example_values::PROFILES_S3_SECRETACCESSKEY.to_string(), }], } } From fe18a8e465766bd88830f736853332bf358efd5a Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 7 Oct 2025 19:52:00 -0700 Subject: [PATCH 12/26] simplify init imports Signed-off-by: Robert Detjens --- src/commands/init.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index b588ae8..8bb52f6 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -4,7 +4,7 @@ use std::io::Write; use std::process::exit; use tracing::error; -use crate::init::{self as init, templatize_init}; +use crate::init; use crate::{access_handlers::frontend, commands::deploy}; pub fn run(interactive: &bool, blank: &bool) -> Result<()> { @@ -16,7 +16,7 @@ pub fn run(interactive: &bool, blank: &bool) -> Result<()> { init::example_init() }; - let configuration = templatize_init(options).context("could not render template")?; + let configuration = init::templatize_init(options).context("could not render template")?; let mut f = File::create("rcds.yaml")?; f.write_all(configuration.as_bytes())?; From 72dcc8816deb078b61fcc17da6e139e4b616201b Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 14 Oct 2025 21:30:08 -0700 Subject: [PATCH 13/26] Show external-dns config notice as warning Signed-off-by: Robert Detjens --- src/commands/init.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/init.rs b/src/commands/init.rs index 8bb52f6..4f04225 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use std::fs::File; use std::io::Write; use std::process::exit; -use tracing::error; +use tracing::{error, warn}; use crate::init; use crate::{access_handlers::frontend, commands::deploy}; @@ -22,8 +22,8 @@ pub fn run(interactive: &bool, blank: &bool) -> Result<()> { f.write_all(configuration.as_bytes())?; // Note about external-dns - println!("Note: external-dns configuration settings will need to be provided in rcds.yaml after file creation, under the `profiles.name.dns` key."); - println!("Reference: https://github.com/bitnami/charts/tree/main/bitnami/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(()) } From 229342386b86796a23d71c698c65da40ea736ed8 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 14 Oct 2025 21:32:34 -0700 Subject: [PATCH 14/26] Use our internal config structs for init handling -- example init We already have an existing struct used to parse the rcds.yaml config, use that to build the example/interactive config instead of creating an almost-identical struct. So far this is only done for the example values / non-interactive init. Signed-off-by: Robert Detjens --- src/init/example_values.rs | 14 +++--- src/init/mod.rs | 92 ++++++++++++++++++++------------------ 2 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/init/example_values.rs b/src/init/example_values.rs index 91309bb..fe4d4d4 100644 --- a/src/init/example_values.rs +++ b/src/init/example_values.rs @@ -1,17 +1,21 @@ // 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 = "1"; -pub static DEFAULTS_RESOURCES_CPU: &str = "1"; + +pub static DEFAULTS_DIFFICULTY: i64 = 1; +pub static DEFAULTS_RESOURCES_CPU: i64 = 1; pub static DEFAULTS_RESOURCES_MEMORY: &str = "500M"; -pub static POINTS_DIFFICULTY: &str = "easy"; -pub static POINTS_MIN: &str = "200"; -pub static POINTS_MAX: &str = "500"; + +pub static POINTS_DIFFICULTY: i64 = 1; +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"; diff --git a/src/init/mod.rs b/src/init/mod.rs index e41f55e..5e36ed6 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -3,9 +3,11 @@ use inquire; use minijinja; use regex::Regex; use serde; +use std::collections::HashMap; use std::fmt; use tracing::{debug, error, info, trace, warn}; +use crate::configparser::config; use crate::utils::render_strict; pub mod example_values; @@ -22,18 +24,18 @@ pub struct InitVars { pub defaults_difficulty: String, pub defaults_resources_cpu: String, pub defaults_resources_memory: String, - pub points: Vec, - pub profiles: Vec, + pub points: Vec, + pub profiles: Vec, } #[derive(Clone, serde::Serialize, Default, Debug)] -pub struct Points { +pub struct InitPoints { pub difficulty: String, pub min: String, pub max: String, } -impl fmt::Display for Points { +impl fmt::Display for InitPoints { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -44,7 +46,7 @@ impl fmt::Display for Points { } #[derive(serde::Serialize, Default, Debug)] -pub struct Profile { +pub struct InitProfile { pub profile_name: String, pub frontend_url: String, pub frontend_token: String, @@ -61,7 +63,7 @@ pub fn interactive_init() -> inquire::error::InquireResult { println!("For all prompts below, simply press Enter to leave blank."); println!("All fields that can be set in rcds.yaml can also be set via environment variables."); - let points_ranks_reference: Vec; + let points_ranks_reference: Vec; let options = InitVars { flag_regex: { @@ -118,9 +120,9 @@ pub fn interactive_init() -> inquire::error::InquireResult { .with_default(false) .prompt()?; println!("Challenge points are dynamic. For a static challenge, simply set minimum and maximum points to the same value."); - let mut points_ranks: Vec = Vec::new(); + let mut points_ranks: Vec = Vec::new(); while again { - let points_obj = Points { + let points_obj = InitPoints { difficulty: { inquire::Text::new("Difficulty class:") .with_validator(inquire::required!("Please provide a name.")) @@ -190,9 +192,9 @@ pub fn interactive_init() -> inquire::error::InquireResult { let mut again = inquire::Confirm::new("Do you want to provide a Profile?") .with_default(false) .prompt()?; - let mut profiles: Vec = Vec::new(); + let mut profiles: Vec = Vec::new(); while again { - let prof = Profile { + let prof = InitProfile { profile_name: { inquire::Text::new("Profile name:") .with_help_message("The name of the deployment Profile. One Profile named \"default\" is recommended. You can add additional profiles.") @@ -267,47 +269,51 @@ pub fn interactive_init() -> inquire::error::InquireResult { Ok(options) } -pub fn blank_init() -> InitVars { +pub fn blank_init() -> config::RcdsConfig { trace!("building blank config"); - InitVars::default() + config::RcdsConfig { + flag_regex: "".to_string(), + ..Default::default() + } } -pub fn example_init() -> InitVars { +pub fn example_init() -> config::RcdsConfig { trace!("building example values config"); - InitVars { + + config::RcdsConfig { flag_regex: example_values::FLAG_REGEX.to_string(), - registry_domain: example_values::REGISTRY_DOMAIN.to_string(), - registry_build_user: example_values::REGISTRY_BUILD_USER.to_string(), - registry_build_pass: example_values::REGISTRY_BUILD_PASS.to_string(), - registry_cluster_user: example_values::REGISTRY_CLUSTER_USER.to_string(), - registry_cluster_pass: example_values::REGISTRY_CLUSTER_USER.to_string(), - defaults_difficulty: example_values::DEFAULTS_DIFFICULTY.to_string(), - defaults_resources_cpu: example_values::DEFAULTS_RESOURCES_CPU.to_string(), - defaults_resources_memory: example_values::DEFAULTS_RESOURCES_MEMORY.to_string(), - points: vec![ - Points { - difficulty: example_values::POINTS_DIFFICULTY.to_string(), - min: example_values::POINTS_MIN.to_string(), - max: example_values::POINTS_MAX.to_string(), + registry: config::Registry { + domain: example_values::REGISTRY_DOMAIN.to_string(), + tag_format: String::new(), + build: config::UserPass { + user: example_values::REGISTRY_BUILD_USER.to_string(), + pass: example_values::REGISTRY_BUILD_PASS.to_string(), }, - Points { - difficulty: "2".to_string(), - min: "1".to_string(), - max: "1337".to_string(), + cluster: config::UserPass { + user: example_values::REGISTRY_CLUSTER_USER.to_string(), + pass: example_values::REGISTRY_CLUSTER_PASS.to_string(), + }, + }, + defaults: config::Defaults { + difficulty: example_values::DEFAULTS_DIFFICULTY, + resources: config::Resource { + cpu: example_values::DEFAULTS_RESOURCES_CPU, + memory: example_values::DEFAULTS_RESOURCES_MEMORY.to_string(), }, - ], - profiles: vec![Profile { - profile_name: example_values::PROFILES_PROFILE_NAME.to_string(), - frontend_url: example_values::PROFILES_FRONTEND_URL.to_string(), - frontend_token: example_values::PROFILES_FRONTEND_TOKEN.to_string(), - challenges_domain: example_values::PROFILES_CHALLENGES_DOMAIN.to_string(), - kubecontext: example_values::PROFILES_KUBECONTEXT.to_string(), - s3_bucket_name: example_values::PROFILES_S3_BUCKET_NAME.to_string(), - s3_endpoint: example_values::PROFILES_S3_ENDPOINT.to_string(), - s3_region: example_values::PROFILES_S3_REGION.to_string(), - s3_accesskey: example_values::PROFILES_S3_ACCESSKEY.to_string(), - s3_secretaccesskey: example_values::PROFILES_S3_SECRETACCESSKEY.to_string(), + }, + points: vec![config::ChallengePoints { + difficulty: example_values::POINTS_DIFFICULTY, + min: example_values::POINTS_MIN, + max: example_values::POINTS_MAX, }], + + deploy: HashMap::from([( + example_values::PROFILES_PROFILE_NAME.to_string(), + config::ProfileDeploy { + challenges: HashMap::new(), + }, + )]), + profiles: HashMap::from([]), } } From e4368ccd664ceed1c2770fcd9de8159b42fed4e2 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 14 Oct 2025 22:39:13 -0700 Subject: [PATCH 15/26] Use our internal config structs for init handling -- blank init Signed-off-by: Robert Detjens --- src/init/mod.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index 5e36ed6..a613941 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -271,9 +271,32 @@ pub fn interactive_init() -> inquire::error::InquireResult { pub fn blank_init() -> config::RcdsConfig { trace!("building blank config"); + + // struct does not implement Default on purpose, manually fill out as blank config::RcdsConfig { flag_regex: "".to_string(), - ..Default::default() + registry: config::Registry { + domain: "".to_string(), + tag_format: String::new(), + build: config::UserPass { + user: "".to_string(), + pass: "".to_string(), + }, + cluster: config::UserPass { + user: "".to_string(), + pass: "".to_string(), + }, + }, + defaults: config::Defaults { + difficulty: 1, + resources: config::Resource { + cpu: 1, + memory: "".to_string(), + }, + }, + points: vec![], + deploy: HashMap::from([]), + profiles: HashMap::from([]), } } From bba1d48527daaa1e460bd025ef78b8a0471eb7af Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 14 Oct 2025 23:41:41 -0700 Subject: [PATCH 16/26] Convert points difficulty category name to a string Having this be a string is more user friendly than a number, and this branch was originally set up to do that. Signed-off-by: Robert Detjens --- src/configparser/config.rs | 7 +++--- src/init/example_values.rs | 4 +-- src/init/mod.rs | 6 ++--- src/tests/parsing/config.rs | 50 +++++++++++++------------------------ tests/repo/rcds.yaml | 6 ++--- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/configparser/config.rs b/src/configparser/config.rs index 6527213..3131078 100644 --- a/src/configparser/config.rs +++ b/src/configparser/config.rs @@ -121,7 +121,7 @@ struct Resource { #[derive(Debug, PartialEq, Serialize, Deserialize)] #[fully_pub] struct Defaults { - difficulty: i64, + difficulty: String, resources: Resource, } @@ -145,10 +145,11 @@ struct ProfileConfig { dns: serde_yml::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, } diff --git a/src/init/example_values.rs b/src/init/example_values.rs index fe4d4d4..3b9dfe7 100644 --- a/src/init/example_values.rs +++ b/src/init/example_values.rs @@ -8,11 +8,11 @@ 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: i64 = 1; +pub static DEFAULTS_DIFFICULTY: &str = "easy"; pub static DEFAULTS_RESOURCES_CPU: i64 = 1; pub static DEFAULTS_RESOURCES_MEMORY: &str = "500M"; -pub static POINTS_DIFFICULTY: i64 = 1; +pub static POINTS_DIFFICULTY: &str = "easy"; pub static POINTS_MIN: i64 = 200; pub static POINTS_MAX: i64 = 500; diff --git a/src/init/mod.rs b/src/init/mod.rs index a613941..3dc2de9 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -288,7 +288,7 @@ pub fn blank_init() -> config::RcdsConfig { }, }, defaults: config::Defaults { - difficulty: 1, + difficulty: "easy".to_string(), resources: config::Resource { cpu: 1, memory: "".to_string(), @@ -318,14 +318,14 @@ pub fn example_init() -> config::RcdsConfig { }, }, defaults: config::Defaults { - difficulty: example_values::DEFAULTS_DIFFICULTY, + difficulty: example_values::DEFAULTS_DIFFICULTY.to_string(), resources: config::Resource { cpu: example_values::DEFAULTS_RESOURCES_CPU, memory: example_values::DEFAULTS_RESOURCES_MEMORY.to_string(), }, }, points: vec![config::ChallengePoints { - difficulty: example_values::POINTS_DIFFICULTY, + difficulty: example_values::POINTS_DIFFICULTY.to_string(), min: example_values::POINTS_MIN, max: example_values::POINTS_MAX, }], diff --git a/src/tests/parsing/config.rs b/src/tests/parsing/config.rs index 9d42903..f092537 100644 --- a/src/tests/parsing/config.rs +++ b/src/tests/parsing/config.rs @@ -27,11 +27,11 @@ fn all_yaml() { pass: alsofake defaults: - difficulty: 1 + difficulty: "easy" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 + - difficulty: "easy" min: 0 max: 1337 @@ -58,11 +58,7 @@ fn all_yaml() { "#, )?; - let config = match parse() { - Ok(c) => Ok(c), - // figment::Error cannot coerce from anyhow::Error natively - Err(e) => Err(figment::Error::from(format!("{:?}", e))), - }?; + let config = parse().map_err(|e| figment::Error::from(format!("{:?}", e)))?; let expected = RcdsConfig { flag_regex: "test{[a-zA-Z_]+}".to_string(), @@ -79,14 +75,14 @@ fn all_yaml() { }, }, defaults: Defaults { - difficulty: 1, + difficulty: "easy".to_string(), resources: Resource { cpu: 1, memory: "500M".to_string(), }, }, points: vec![ChallengePoints { - difficulty: 1, + difficulty: "easy".to_string(), min: 0, max: 1337, }], @@ -151,11 +147,11 @@ fn registry_tag_format() { pass: alsofake defaults: - difficulty: 1 + difficulty: "easy" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 + - difficulty: "easy" min: 0 max: 1337 @@ -182,11 +178,7 @@ fn registry_tag_format() { "#, )?; - let config = match parse() { - Ok(c) => Ok(c), - // figment::Error cannot coerce from anyhow::Error natively - Err(e) => Err(figment::Error::from(format!("{:?}", e))), - }?; + let config = parse().map_err(|e| figment::Error::from(format!("{:?}", e)))?; let expected = RcdsConfig { flag_regex: "test{[a-zA-Z_]+}".to_string(), @@ -203,14 +195,14 @@ fn registry_tag_format() { }, }, defaults: Defaults { - difficulty: 1, + difficulty: "easy".to_string(), resources: Resource { cpu: 1, memory: "500M".to_string(), }, }, points: vec![ChallengePoints { - difficulty: 1, + difficulty: "easy".to_string(), min: 0, max: 1337, }], @@ -274,11 +266,11 @@ fn yaml_with_env_overrides() { pass: alsofake defaults: - difficulty: 1 + difficulty: "easy" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 + - difficulty: "easy" min: 0 max: 1337 @@ -315,10 +307,7 @@ fn yaml_with_env_overrides() { jail.set_env("BEAVERCDS_PROFILES_TESTING_S3_ACCESS_KEY", "envkey"); jail.set_env("BEAVERCDS_PROFILES_TESTING_S3_SECRET_KEY", "envsecret"); - let config = match parse() { - Err(e) => Err(figment::Error::from(format!("{:?}", e))), - Ok(config) => Ok(config), - }?; + let config = parse().map_err(|e| figment::Error::from(format!("{:?}", e)))?; // also check that the envvar overrides were applied assert_eq!(config.registry.build.user, "envbuilduser"); @@ -350,11 +339,11 @@ fn partial_yaml_with_env() { domain: registry.example/test defaults: - difficulty: 1 + difficulty: "easy" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 + - difficulty: "easy" min: 0 max: 1337 @@ -388,10 +377,7 @@ fn partial_yaml_with_env() { jail.set_env("BEAVERCDS_PROFILES_TESTING_S3_ACCESS_KEY", "envkey"); jail.set_env("BEAVERCDS_PROFILES_TESTING_S3_SECRET_KEY", "envsecret"); - let config = match parse() { - Err(e) => Err(figment::Error::from(format!("{:?}", e))), - Ok(config) => Ok(config), - }?; + let config = parse().map_err(|e| figment::Error::from(format!("{:?}", e)))?; // also check that the envvar overrides were applied assert_eq!(config.registry.build.user, "envbuilduser"); @@ -449,11 +435,11 @@ fn bad_yaml_missing_secrets() { domain: registry.example/test defaults: - difficulty: 1 + difficulty: "easy" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 + - difficulty: "easy" min: 0 max: 1337 diff --git a/tests/repo/rcds.yaml b/tests/repo/rcds.yaml index eff3c14..6a2f36c 100644 --- a/tests/repo/rcds.yaml +++ b/tests/repo/rcds.yaml @@ -12,12 +12,12 @@ registry: pass: alsofake defaults: - difficulty: 1 + difficulty: "standard" resources: { cpu: 1, memory: 500M } points: - - difficulty: 1 - min: 0 + - difficulty: "standard" + min: 100 max: 1337 deploy: From c368a4dde46516ce6b9088a56d44560d617f9d4c Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Tue, 14 Oct 2025 23:44:47 -0700 Subject: [PATCH 17/26] Use our internal config structs for init handling -- interactive prompts Signed-off-by: Robert Detjens --- src/init/mod.rs | 230 ++++++++++++++++++++++-------------------------- 1 file changed, 104 insertions(+), 126 deletions(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index 3dc2de9..df94ae3 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -1,5 +1,6 @@ use anyhow::Result; use inquire; +use itertools::Itertools; use minijinja; use regex::Regex; use serde; @@ -13,59 +14,13 @@ use crate::utils::render_strict; pub mod example_values; pub mod templates; -#[derive(serde::Serialize, Default, Debug)] -pub struct InitVars { - pub flag_regex: String, - pub registry_domain: String, - pub registry_build_user: String, - pub registry_build_pass: String, - pub registry_cluster_user: String, - pub registry_cluster_pass: String, - pub defaults_difficulty: String, - pub defaults_resources_cpu: String, - pub defaults_resources_memory: String, - pub points: Vec, - pub profiles: Vec, -} - -#[derive(Clone, serde::Serialize, Default, Debug)] -pub struct InitPoints { - pub difficulty: String, - pub min: String, - pub max: String, -} - -impl fmt::Display for InitPoints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "({} Points: {}-{})", - self.difficulty, self.min, self.max - ) - } -} - -#[derive(serde::Serialize, Default, Debug)] -pub struct InitProfile { - pub profile_name: String, - pub frontend_url: String, - pub frontend_token: String, - pub challenges_domain: String, - pub kubecontext: String, - pub s3_bucket_name: String, - pub s3_endpoint: String, - pub s3_region: String, - pub s3_accesskey: String, - pub s3_secretaccesskey: String, -} - -pub fn interactive_init() -> inquire::error::InquireResult { +pub fn interactive_init() -> inquire::error::InquireResult { println!("For all prompts below, simply press Enter to leave blank."); println!("All fields that can be set in rcds.yaml can also be set via environment variables."); - let points_ranks_reference: Vec; + let difficulty_names; // set during `points` prompt later - let options = InitVars { + let options = config::RcdsConfig { flag_regex: { //TODO: what flavor of regex is being validated and accepted inquire::Text::new("Flag regex:") @@ -74,44 +29,50 @@ pub fn interactive_init() -> inquire::error::InquireResult { .prompt()? }, - registry_domain: { - inquire::Text::new ("Container registry:") + registry: config::Registry { + domain: { + inquire::Text::new ("Container registry:") .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") .with_placeholder(example_values::REGISTRY_DOMAIN) .prompt()? - }, - - registry_build_user: { - inquire::Text::new("Container registry 'build' user:") - .with_help_message("The username that will be used to push built containers.") - .with_placeholder(example_values::REGISTRY_BUILD_USER) - .prompt()? - }, - - // TODO: do we actually want to be in charge of these credentials vs expecting the local building utility already be logged in? - registry_build_pass: { - inquire::Password::new("Container registry 'build' password:") - .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? - }, - - registry_cluster_user: { - inquire::Text::new("Container registry 'cluster' user:") - .with_help_message( - "The username that the cluster will use to pull locally-built containers.", - ) - .with_placeholder(example_values::REGISTRY_CLUSTER_USER) - .prompt()? - }, + }, + tag_format: "TODO".to_string(), + build: config::UserPass { + user: { + inquire::Text::new("Container registry 'build' user:") + .with_help_message( + "The username that will be used to push built containers.", + ) + .with_placeholder(example_values::REGISTRY_BUILD_USER) + .prompt()? + }, + // TODO: do we actually want to be in charge of these credentials vs expecting the local building utility already be logged in? + pass: { + inquire::Password::new("Container registry 'build' password:") + .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? + }, + }, + cluster: config::UserPass { + user: { + inquire::Text::new("Container registry 'cluster' user:") + .with_help_message( + "The username that the cluster will use to pull locally-built containers.", + ) + .with_placeholder(example_values::REGISTRY_CLUSTER_USER) + .prompt()? + }, - registry_cluster_pass: { - inquire::Password::new("Container registry 'cluster' password:") - .with_help_message("The password to the 'cluster' user account") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? + pass: { + inquire::Password::new("Container registry 'cluster' password:") + .with_help_message("The password to the 'cluster' user account") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()? + }, + }, }, points: { @@ -119,88 +80,97 @@ pub fn interactive_init() -> inquire::error::InquireResult { let mut again = inquire::Confirm::new("Do you want to provide a difficulty class?") .with_default(false) .prompt()?; + println!("Challenge points are dynamic. For a static challenge, simply set minimum and maximum points to the same value."); - let mut points_ranks: Vec = Vec::new(); + let mut points = vec![]; while again { - let points_obj = InitPoints { - difficulty: { + let points_obj = config::ChallengePoints { + difficulty: inquire::Text::new("Difficulty class:") .with_validator(inquire::required!("Please provide a name.")) .with_help_message("The name of the difficulty class.") .with_placeholder(example_values::POINTS_DIFFICULTY) .prompt()? - }, - min: { - inquire::CustomType::::new("Minimum points:") + , + min: + inquire::CustomType::::new("Minimum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str .with_help_message("The minimum number of points that challenges within this difficulty class are worth.") - .with_placeholder(example_values::POINTS_MIN) + .with_placeholder(&example_values::POINTS_MIN.to_string()) .prompt()? - .to_string() - }, + , max: { - inquire::CustomType::::new("Maximum points:") + inquire::CustomType::::new("Maximum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str .with_help_message("The maximum number of points that challenges within this difficulty class are worth.") - .with_placeholder(example_values::POINTS_MAX) + .with_placeholder(&example_values::POINTS_MAX.to_string()) .prompt()? - .to_string() }, }; - points_ranks.push(points_obj); + points.push(points_obj); again = inquire::Confirm::new("Do you want to provide another difficulty class?") .with_default(false) .prompt()?; } - points_ranks_reference = points_ranks.clone(); - points_ranks + // save owned copy of difficulty category names for use below + difficulty_names = points.iter().map(|p| p.difficulty.clone()).collect_vec(); + points }, + defaults: config::Defaults { + difficulty: { + if difficulty_names.is_empty() { + String::new() + } else { + inquire::Select::new( + "Please choose the default difficulty class:", + difficulty_names, + ) + .prompt()? + } + }, + + + resources: config::Resource { + cpu: - defaults_difficulty: { - if points_ranks_reference.is_empty() { - String::new() - } else { - inquire::Select::new( - "Please choose the default difficulty class:", - points_ranks_reference, - ) - .prompt()? - .difficulty - } - }, - defaults_resources_cpu: inquire::Text::new("Default CPU limit:") + inquire::CustomType::::new("Default CPU limit:") .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") - .with_placeholder(example_values::DEFAULTS_RESOURCES_CPU) + .with_placeholder(&example_values::DEFAULTS_RESOURCES_CPU.to_string()) .with_default(example_values::DEFAULTS_RESOURCES_CPU) - .prompt()? - , + .prompt()?, - defaults_resources_memory: { + memory: { inquire::Text::new("Default memory limit:") .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") .with_placeholder(example_values::DEFAULTS_RESOURCES_MEMORY) .with_default(example_values::DEFAULTS_RESOURCES_MEMORY) .prompt()? + }, + }, }, + + profiles: { println!("You can define several environment profiles below."); let mut again = inquire::Confirm::new("Do you want to provide a Profile?") .with_default(false) .prompt()?; - let mut profiles: Vec = Vec::new(); + let mut profiles = HashMap::new(); + while again { - let prof = InitProfile { - profile_name: { + let name = { inquire::Text::new("Profile name:") .with_help_message("The name of the deployment Profile. One Profile named \"default\" is recommended. You can add additional profiles.") .with_placeholder(example_values::PROFILES_PROFILE_NAME) .prompt()? - }, + }; + + let prof = config::ProfileConfig { frontend_url: { inquire::Text::new("Frontend URL:") .with_help_message("The URL of the RNG scoreboard.") @@ -225,45 +195,53 @@ pub fn interactive_init() -> inquire::error::InquireResult { .with_placeholder(example_values::PROFILES_KUBECONTEXT) .prompt()? }, - s3_bucket_name: { + s3: config::S3Config { + bucket_name: { inquire::Text::new("S3 bucket name:") .with_help_message("Challenge artifacts and static files will be hosted on S3. The name of the S3 bucket.") .with_placeholder(example_values::PROFILES_S3_BUCKET_NAME) .prompt()? }, - s3_endpoint: { + endpoint: { inquire::Text::new("S3 endpoint:") .with_help_message("The endpoint of the S3 bucket server.") .with_placeholder(example_values::PROFILES_S3_ENDPOINT) .prompt()? }, - s3_region: { + region: { inquire::Text::new("S3 region:") .with_help_message("The region where the S3 bucket is hosted.") .with_placeholder(example_values::PROFILES_S3_REGION) .prompt()? }, - s3_accesskey: { + access_key: { inquire::Text::new("S3 access key:") .with_help_message("The public access key to the S3 bucket.") .with_placeholder(example_values::PROFILES_S3_ACCESSKEY) .prompt()? }, - s3_secretaccesskey: { + secret_key: { inquire::Text::new("S3 secret key:") .with_help_message("The secret acess key to the S3 bucket.") .with_placeholder(example_values::PROFILES_S3_SECRETACCESSKEY) .prompt()? }, + }, + kubeconfig: None, + dns: Default::default(), // explicitly leave this blank, user needs to set it }; - profiles.push(prof); + + profiles.insert(name, prof); again = inquire::Confirm::new("Do you want to provide another Profile?") .with_default(false) .prompt()?; } profiles + }, + + deploy: HashMap::new() // user is init'ing a blank repo, no challenges yet! }; Ok(options) @@ -340,7 +318,7 @@ pub fn example_init() -> config::RcdsConfig { } } -pub fn templatize_init(options: InitVars) -> Result { +pub fn templatize_init(options: config::RcdsConfig) -> Result { debug!("rendering template with {options:?}"); render_strict(templates::RCDS, minijinja::context! {options}) } From 5c2316eeed4381605471df3d0864794d5ae713f2 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Wed, 22 Oct 2025 23:26:09 -0700 Subject: [PATCH 18/26] Update rcds config template with help comments and use new structs Signed-off-by: Robert Detjens --- src/asset_files/rcds.yaml.j2 | 86 +++++++++++++++++++++++------------- src/commands/init.rs | 2 +- src/init/mod.rs | 7 ++- 3 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index 1e012f8..3197d83 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -1,46 +1,72 @@ -flag_regex: {{ options.flag_regex }} +# 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: {{ options.registry_domain }} + 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: {{ options.registry_build_user }} - pass: {{ options.registry_build_pass }} + user: "{{ registry.build.user }}" + pass: "{{ registry.build.pass }}" + # Used by the cluster to pull the built images. cluster: - user: {{ options.registry_cluster_user }} - pass: {{ options.registry_cluster_pass }} + 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: {{ options.defaults_difficulty }} - resources: { cpu: {{ options.defaults_resources_cpu }}, memory: {{ options.defaults_resources_memory }} } + difficulty: "{{ defaults.difficulty }}" + resources: { cpu: {{ defaults.resources.cpu }}, memory: "{{ defaults.resources.memory }}" } -points: {% for pts in options.points %} - - difficulty: {{ pts.difficulty }} +# 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 }} -{% endfor %} -# TODO: templatize the deploy section + {% endfor %} + +# Control what challenges are deployed in each environment profile. deploy: - # control challenge deployment status explicitly per environment/profile - testing: - misc/garf: true - pwn/notsh: true - web/bar: true + {% for name, _conf in profiles | items -%} + {{ name }}: {} + {% endfor %} -profiles: {% for prof in options.profiles %} - {{ prof.profile_name}}: - frontend_url: {{ prof.frontend_url }} - frontend_token: {{ prof.frontend_token }} - challenges_domain: {{ prof.challenges_domain }} +# 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: {{ prof.kubecontext }} + kubecontext: {{ p.kubecontext }} + # Credentials for the public challenge file asset bucket. s3: - bucket_name: {{ prof.s3_bucket_name }} - endpoint: {{ prof.s3_endpoint }} - region: {{ prof.s3_region }} - access_key: {{ prof.s3_accesskey }} - secret_key: {{ prof.s3_secretaccesskey }} + 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 + # this yaml will be passed directly to external-dns without modification # Reference: https://github.com/bitnami/charts/tree/main/bitnami/external-dns -{% endfor %} + {% endfor %} diff --git a/src/commands/init.rs b/src/commands/init.rs index 4f04225..4007c56 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -16,7 +16,7 @@ pub fn run(interactive: &bool, blank: &bool) -> Result<()> { init::example_init() }; - let configuration = init::templatize_init(options).context("could not render template")?; + let configuration = init::templatize_init(&options).context("could not render template")?; let mut f = File::create("rcds.yaml")?; f.write_all(configuration.as_bytes())?; diff --git a/src/init/mod.rs b/src/init/mod.rs index df94ae3..ca7763c 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -318,7 +318,10 @@ pub fn example_init() -> config::RcdsConfig { } } -pub fn templatize_init(options: config::RcdsConfig) -> Result { +pub fn templatize_init(options: &config::RcdsConfig) -> Result { debug!("rendering template with {options:?}"); - render_strict(templates::RCDS, minijinja::context! {options}) + render_strict( + templates::RCDS, + minijinja::context! {.. minijinja::Value::from_serialize(options)}, + ) } From 6569a5e0786406d3b259b359ad23ef50090795ac Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Wed, 22 Oct 2025 23:30:09 -0700 Subject: [PATCH 19/26] Handle empty profile/points correctly Signed-off-by: Robert Detjens --- src/asset_files/rcds.yaml.j2 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index 3197d83..95ad41e 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -36,12 +36,16 @@ 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 @@ -69,4 +73,6 @@ profiles: # 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 %} From 2221222cab6bdf950eeb14430503da26c950ac24 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Thu, 23 Oct 2025 20:12:08 -0700 Subject: [PATCH 20/26] Add trailing template comment to preserve trailing newline when blank Signed-off-by: Robert Detjens --- src/asset_files/rcds.yaml.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/asset_files/rcds.yaml.j2 b/src/asset_files/rcds.yaml.j2 index 95ad41e..e85a51c 100644 --- a/src/asset_files/rcds.yaml.j2 +++ b/src/asset_files/rcds.yaml.j2 @@ -76,3 +76,5 @@ profiles: {%- else -%} {} {% endfor %} + +{# comment to preserve trailing newline -#} From 3cdf345557d4764496e9dd0317c2df6c32b85f63 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Thu, 23 Oct 2025 21:14:54 -0700 Subject: [PATCH 21/26] Make default `init` behaviour interactive Blank and placeholder template files should be set explicitly by the user. Signed-off-by: Robert Detjens --- src/cli.rs | 15 +++++++++++---- src/commands/init.rs | 11 ++++++----- src/main.rs | 6 +++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 5a6a164..3b6272e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -88,13 +88,20 @@ pub enum Commands { profile: String, }, - /// Create an initial rcds.yaml to the current working directory. + /// 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 { - /// Cannot be used with -b. If enabled, will prompt for each field of the config file. If disabled, behavior depends on --blank. + /// Prompt user for each field interactively. [Default if no flags specified] #[arg(short = 'i', long)] interactive: bool, - /// Cannot be used with -i. If enabled, will create the file without any fields set. If disabled, will create an example config file (fields set with fake data). - #[arg(short = 'b', long, conflicts_with = "interactive")] + /// 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, }, } diff --git a/src/commands/init.rs b/src/commands/init.rs index 4007c56..76a4d17 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -7,13 +7,14 @@ use tracing::{error, warn}; use crate::init; use crate::{access_handlers::frontend, commands::deploy}; -pub fn run(interactive: &bool, blank: &bool) -> Result<()> { - let options = if *interactive { - init::interactive_init()? - } else if *blank { +pub fn run(_interactive: &bool, placeholders: &bool, blank: &bool) -> Result<()> { + let options = if *blank { init::blank_init() - } else { + } 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")?; diff --git a/src/main.rs b/src/main.rs index b8cee3c..b0761e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,6 +90,10 @@ fn dispatch(cli: cli::Cli) -> anyhow::Result<()> { commands::cluster_setup::run(profile) } - cli::Commands::Init { interactive, blank } => commands::init::run(interactive, blank), + cli::Commands::Init { + interactive, + placeholders, + blank, + } => commands::init::run(interactive, placeholders, blank), } } From 0fbb63641c80438ab92185ee83dbca52fd5a07a8 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Thu, 23 Oct 2025 21:15:51 -0700 Subject: [PATCH 22/26] Fix blank points and reuse default tag format from parsing Signed-off-by: Robert Detjens --- src/configparser/config.rs | 2 +- src/init/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/configparser/config.rs b/src/configparser/config.rs index 3131078..d1f1b96 100644 --- a/src/configparser/config.rs +++ b/src/configparser/config.rs @@ -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() } diff --git a/src/init/mod.rs b/src/init/mod.rs index ca7763c..6dfd4b6 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -266,9 +266,9 @@ pub fn blank_init() -> config::RcdsConfig { }, }, defaults: config::Defaults { - difficulty: "easy".to_string(), + difficulty: "".to_string(), resources: config::Resource { - cpu: 1, + cpu: 0, memory: "".to_string(), }, }, @@ -285,7 +285,7 @@ pub fn example_init() -> config::RcdsConfig { flag_regex: example_values::FLAG_REGEX.to_string(), registry: config::Registry { domain: example_values::REGISTRY_DOMAIN.to_string(), - tag_format: String::new(), + tag_format: config::default_tag_format(), build: config::UserPass { user: example_values::REGISTRY_BUILD_USER.to_string(), pass: example_values::REGISTRY_BUILD_PASS.to_string(), From ef574479e98d3366a5fe9ddebfe697fa0492317c Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Thu, 23 Oct 2025 21:19:52 -0700 Subject: [PATCH 23/26] Switch to serde_yaml_ng serde_yml turns out to be maintained weirdly (issues disabled, weird refactors) and is now archived anyway. _ng is what the community is now recommending. Signed-off-by: Robert Detjens --- Cargo.lock | 22 +++++----------------- Cargo.toml | 2 +- src/clients.rs | 10 +++++----- src/cluster_setup/mod.rs | 4 ++-- src/configparser/config.rs | 2 +- src/tests/parsing/config.rs | 4 ++-- 6 files changed, 16 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ea334f..5aaa0a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,7 +296,7 @@ dependencies = [ "rust-s3", "serde", "serde_nested_with", - "serde_yml", + "serde_yaml_ng", "tar", "tempfile", "tokio", @@ -1748,16 +1748,6 @@ dependencies = [ "redox_syscall", ] -[[package]] -name = "libyml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" -dependencies = [ - "anyhow", - "version_check", -] - [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2660,18 +2650,16 @@ dependencies = [ ] [[package]] -name = "serde_yml" -version = "0.0.12" +name = "serde_yaml_ng" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" dependencies = [ "indexmap 2.6.0", "itoa", - "libyml", - "memchr", "ryu", "serde", - "version_check", + "unsafe-libyaml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 13f2c83..ea9bbf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ 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" diff --git a/src/clients.rs b/src/clients.rs index dd22694..aa6f889 100644 --- a/src/clients.rs +++ b/src/clients.rs @@ -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, @@ -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> { +fn multidoc_deserialize(data: &str) -> Result> { 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), }; } diff --git a/src/cluster_setup/mod.rs b/src/cluster_setup/mod.rs index c55ddb1..22a6c92 100644 --- a/src/cluster_setup/mod.rs +++ b/src/cluster_setup/mod.rs @@ -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}; @@ -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 }, )?; diff --git a/src/configparser/config.rs b/src/configparser/config.rs index d1f1b96..a91bc8e 100644 --- a/src/configparser/config.rs +++ b/src/configparser/config.rs @@ -142,7 +142,7 @@ struct ProfileConfig { kubeconfig: Option, kubecontext: String, s3: S3Config, - dns: serde_yml::Value, + dns: serde_yaml_ng::Value, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] diff --git a/src/tests/parsing/config.rs b/src/tests/parsing/config.rs index f092537..a1d374a 100644 --- a/src/tests/parsing/config.rs +++ b/src/tests/parsing/config.rs @@ -111,7 +111,7 @@ fn all_yaml() { access_key: "accesskey".to_string(), secret_key: "secretkey".to_string(), }, - dns: serde_yml::to_value(HashMap::from([ + dns: serde_yaml_ng::to_value(HashMap::from([ ("provider", "somebody"), ("thing", "whatever"), ])) @@ -231,7 +231,7 @@ fn registry_tag_format() { access_key: "accesskey".to_string(), secret_key: "secretkey".to_string(), }, - dns: serde_yml::to_value(HashMap::from([ + dns: serde_yaml_ng::to_value(HashMap::from([ ("provider", "somebody"), ("thing", "whatever"), ])) From a7842c9d5661391f150de9d6de8c7cb768196185 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Thu, 23 Oct 2025 22:05:57 -0700 Subject: [PATCH 24/26] format interactive prompt structure, document rustfmt failings Signed-off-by: Robert Detjens --- src/init/mod.rs | 223 ++++++++++++++++++-------------------------- temp-test/rcds.yaml | 50 ++++++++++ 2 files changed, 141 insertions(+), 132 deletions(-) create mode 100644 temp-test/rcds.yaml diff --git a/src/init/mod.rs b/src/init/mod.rs index 6dfd4b6..9a31cf0 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -20,58 +20,51 @@ pub fn interactive_init() -> inquire::error::InquireResult { let difficulty_names; // set during `points` prompt later + // FORMATTING NOTE: The with_help_message() calls cause rustfmt to silently + // fail to format this whole definition. Commenting out the marked + // help_message lines temporarily will let the formatting work. + // + // see issues: + // - https://github.com/rust-lang/rustfmt/issues/6687, + // - https://github.com/rust-lang/rustfmt/issues/3863 + let options = config::RcdsConfig { - flag_regex: { - //TODO: what flavor of regex is being validated and accepted - inquire::Text::new("Flag regex:") - .with_help_message("This regex will be used to validate the individual flags of your challenges later.") + //TODO: what flavor of regex is being validated and accepted + flag_regex: inquire::Text::new("Flag regex:") + .with_help_message("This regex will be used to validate the individual flags of your challenges later.") // too long to format .with_placeholder(example_values::FLAG_REGEX) - .prompt()? - }, + .prompt()?, registry: config::Registry { - domain: { - inquire::Text::new ("Container registry:") - .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") - .with_placeholder(example_values::REGISTRY_DOMAIN) - .prompt()? - }, + domain: inquire::Text::new("Container registry:") + .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") // too long to format + .with_placeholder(example_values::REGISTRY_DOMAIN) + .prompt()?, tag_format: "TODO".to_string(), build: config::UserPass { - user: { - inquire::Text::new("Container registry 'build' user:") - .with_help_message( - "The username that will be used to push built containers.", - ) - .with_placeholder(example_values::REGISTRY_BUILD_USER) - .prompt()? - }, + user: inquire::Text::new("Container registry 'build' user:") + .with_help_message("The username that will be used to push built containers.") + .with_placeholder(example_values::REGISTRY_BUILD_USER) + .prompt()?, // TODO: do we actually want to be in charge of these credentials vs expecting the local building utility already be logged in? - pass: { - inquire::Password::new("Container registry 'build' password:") - .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? - }, + pass: inquire::Password::new("Container registry 'build' password:") + .with_help_message("The password to the 'build' user account") // TODO: could this support username:pat too? + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()?, }, cluster: config::UserPass { - user: { - inquire::Text::new("Container registry 'cluster' user:") + user: inquire::Text::new("Container registry 'cluster' user:") .with_help_message( "The username that the cluster will use to pull locally-built containers.", ) .with_placeholder(example_values::REGISTRY_CLUSTER_USER) - .prompt()? - }, - - pass: { - inquire::Password::new("Container registry 'cluster' password:") - .with_help_message("The password to the 'cluster' user account") - .with_display_mode(inquire::PasswordDisplayMode::Masked) - .with_custom_confirmation_message("Enter again:") - .prompt()? - }, + .prompt()?, + pass: inquire::Password::new("Container registry 'cluster' password:") + .with_help_message("The password to the 'cluster' user account") + .with_display_mode(inquire::PasswordDisplayMode::Masked) + .with_custom_confirmation_message("Enter again:") + .prompt()?, }, }, @@ -80,32 +73,26 @@ pub fn interactive_init() -> inquire::error::InquireResult { let mut again = inquire::Confirm::new("Do you want to provide a difficulty class?") .with_default(false) .prompt()?; - + // println!("Challenge points are dynamic. For a static challenge, simply set minimum and maximum points to the same value."); let mut points = vec![]; while again { let points_obj = config::ChallengePoints { - difficulty: - inquire::Text::new("Difficulty class:") - .with_validator(inquire::required!("Please provide a name.")) - .with_help_message("The name of the difficulty class.") - .with_placeholder(example_values::POINTS_DIFFICULTY) - .prompt()? - , - min: - inquire::CustomType::::new("Minimum points:") + difficulty: inquire::Text::new("Difficulty class:") + .with_validator(inquire::required!("Please provide a name.")) + .with_help_message("The name of the difficulty class.") + .with_placeholder(example_values::POINTS_DIFFICULTY) + .prompt()?, + min: inquire::CustomType::::new("Minimum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str - .with_help_message("The minimum number of points that challenges within this difficulty class are worth.") - .with_placeholder(&example_values::POINTS_MIN.to_string()) - .prompt()? - , - max: { - inquire::CustomType::::new("Maximum points:") + .with_help_message("The minimum number of points that challenges within this difficulty class are worth.") // too long to format + .with_default(example_values::POINTS_MIN) + .prompt()?, + max: inquire::CustomType::::new("Maximum points:") .with_error_message("Please type a valid number.") // default parser calls std::u64::from_str - .with_help_message("The maximum number of points that challenges within this difficulty class are worth.") - .with_placeholder(&example_values::POINTS_MAX.to_string()) - .prompt()? - }, + .with_help_message("The maximum number of points that challenges within this difficulty class are worth.") // too long to format + .with_default(example_values::POINTS_MAX) + .prompt()?, }; points.push(points_obj); @@ -130,30 +117,21 @@ pub fn interactive_init() -> inquire::error::InquireResult { } }, - resources: config::Resource { - cpu: - - - inquire::CustomType::::new("Default CPU limit:") - .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") - .with_placeholder(&example_values::DEFAULTS_RESOURCES_CPU.to_string()) - .with_default(example_values::DEFAULTS_RESOURCES_CPU) - .prompt()?, - - memory: { - inquire::Text::new("Default memory limit:") - .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") - .with_placeholder(example_values::DEFAULTS_RESOURCES_MEMORY) - .with_default(example_values::DEFAULTS_RESOURCES_MEMORY) - .prompt()? - - }, + cpu: inquire::CustomType::::new("Default CPU limit:") + .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") // too long to format + .with_placeholder(&example_values::DEFAULTS_RESOURCES_CPU.to_string()) + .with_default(example_values::DEFAULTS_RESOURCES_CPU) + .prompt()?, + + memory: inquire::Text::new("Default memory limit:") + .with_help_message("The default limit of CPU resources per challenge pod.\nhttps://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#resource-units-in-kubernetes") // too long to format + .with_placeholder(example_values::DEFAULTS_RESOURCES_MEMORY) + .with_default(example_values::DEFAULTS_RESOURCES_MEMORY) + .prompt()?, }, }, - - profiles: { println!("You can define several environment profiles below."); @@ -163,69 +141,51 @@ pub fn interactive_init() -> inquire::error::InquireResult { let mut profiles = HashMap::new(); while again { - let name = { - inquire::Text::new("Profile name:") - .with_help_message("The name of the deployment Profile. One Profile named \"default\" is recommended. You can add additional profiles.") - .with_placeholder(example_values::PROFILES_PROFILE_NAME) - .prompt()? - }; + let name = inquire::Text::new("Profile name:") + .with_help_message("The name of the deployment Profile. One Profile named \"default\" is recommended. You can add additional profiles.") // too long to format + .with_placeholder(example_values::PROFILES_PROFILE_NAME) + .prompt()?; let prof = config::ProfileConfig { - frontend_url: { - inquire::Text::new("Frontend URL:") - .with_help_message("The URL of the RNG scoreboard.") - .with_placeholder(example_values::PROFILES_FRONTEND_URL) - .prompt()? - }, - frontend_token: { - inquire::Text::new("Frontend token:") - .with_help_message("The token to authenticate into the RNG scoreboard.") - .with_placeholder(example_values::PROFILES_FRONTEND_TOKEN) - .prompt()? - }, - challenges_domain: { - inquire::Text::new("Challenges domain:") - .with_help_message("Domain that challenges are hosted under.") - .with_placeholder(example_values::PROFILES_CHALLENGES_DOMAIN) - .prompt()? - }, - kubecontext: { - inquire::Text::new("Kubecontext name:") - .with_help_message("The name of the context that kubectl uses to connect to the cluster.") + frontend_url: inquire::Text::new("Frontend URL:") + .with_help_message("The URL of the RNG scoreboard.") + .with_placeholder(example_values::PROFILES_FRONTEND_URL) + .prompt()?, + frontend_token: inquire::Text::new("Frontend token:") + .with_help_message("The token to authenticate into the RNG scoreboard.") + .with_placeholder(example_values::PROFILES_FRONTEND_TOKEN) + .prompt()?, + challenges_domain: inquire::Text::new("Challenges domain:") + .with_help_message("Domain that challenges are hosted under.") + .with_placeholder(example_values::PROFILES_CHALLENGES_DOMAIN) + .prompt()?, + kubecontext: inquire::Text::new("Kubecontext name:") + .with_help_message( + "The name of the context that kubectl uses to connect to the cluster.", + ) .with_placeholder(example_values::PROFILES_KUBECONTEXT) - .prompt()? - }, + .prompt()?, s3: config::S3Config { - bucket_name: { - inquire::Text::new("S3 bucket name:") - .with_help_message("Challenge artifacts and static files will be hosted on S3. The name of the S3 bucket.") - .with_placeholder(example_values::PROFILES_S3_BUCKET_NAME) - .prompt()? - }, - endpoint: { - inquire::Text::new("S3 endpoint:") + bucket_name: inquire::Text::new("S3 bucket name:") + .with_help_message("Challenge artifacts and static files will be hosted on S3. The name of the S3 bucket.") // too long to format + .with_placeholder(example_values::PROFILES_S3_BUCKET_NAME) + .prompt()?, + endpoint: inquire::Text::new("S3 endpoint:") .with_help_message("The endpoint of the S3 bucket server.") .with_placeholder(example_values::PROFILES_S3_ENDPOINT) - .prompt()? - }, - region: { - inquire::Text::new("S3 region:") + .prompt()?, + region: inquire::Text::new("S3 region:") .with_help_message("The region where the S3 bucket is hosted.") .with_placeholder(example_values::PROFILES_S3_REGION) - .prompt()? - }, - access_key: { - inquire::Text::new("S3 access key:") + .prompt()?, + access_key: inquire::Text::new("S3 access key:") .with_help_message("The public access key to the S3 bucket.") .with_placeholder(example_values::PROFILES_S3_ACCESSKEY) - .prompt()? - }, - secret_key: { - inquire::Text::new("S3 secret key:") + .prompt()?, + secret_key: inquire::Text::new("S3 secret key:") .with_help_message("The secret acess key to the S3 bucket.") .with_placeholder(example_values::PROFILES_S3_SECRETACCESSKEY) - .prompt()? - }, + .prompt()?, }, kubeconfig: None, dns: Default::default(), // explicitly leave this blank, user needs to set it @@ -238,10 +198,9 @@ pub fn interactive_init() -> inquire::error::InquireResult { .prompt()?; } profiles - }, - deploy: HashMap::new() // user is init'ing a blank repo, no challenges yet! + deploy: HashMap::new(), // user is init'ing a blank repo, no challenges yet! }; Ok(options) diff --git a/temp-test/rcds.yaml b/temp-test/rcds.yaml new file mode 100644 index 0000000..99c4e47 --- /dev/null +++ b/temp-test/rcds.yaml @@ -0,0 +1,50 @@ +# 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: "ctf{.*}" + +# Registry configuration for challenge images. +registry: + domain: "ghcr.io/youraccount" + # 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: "{{domain}}/{{challenge}}-{{container}}:{{profile}}" + # Build-time credentials used to push images during `beavercds deploy`. + build: + user: "admin" + pass: "notrealcreds" + # Used by the cluster to pull the built images. + cluster: + user: "cluster_user" + pass: "alsofake" + +# Default difficulty class and resource requests used for challenges that did +# not set their own. +defaults: + difficulty: "easy" + resources: { cpu: 1, memory: "500M" } + +# 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: + - difficulty: "easy" + min: 200 + max: 500 + +# Control what challenges are deployed in each environment profile. +deploy: + {} + + +# 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: + {} + + From 2b6180028508a1e8311c6f0144650803adce8b8c Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Mon, 10 Nov 2025 19:55:41 -0800 Subject: [PATCH 25/26] Prompt user for tag format Signed-off-by: Robert Detjens --- src/init/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index 9a31cf0..83263a9 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -36,11 +36,14 @@ pub fn interactive_init() -> inquire::error::InquireResult { .prompt()?, registry: config::Registry { + tag_format: inquire::Text::new("Container image/tag format:") + .with_help_message("Template to use for built container images. This default works with most registries.") // too long to format + .with_placeholder(&config::default_tag_format()) + .prompt()?, domain: inquire::Text::new("Container registry:") - .with_help_message("Hosted challenges will be hosted in a container registry.The connection endpoint and the repository name.") // too long to format + .with_help_message("Registry domain and repository name of the container registry for hosted challenge images.") // too long to format .with_placeholder(example_values::REGISTRY_DOMAIN) .prompt()?, - tag_format: "TODO".to_string(), build: config::UserPass { user: inquire::Text::new("Container registry 'build' user:") .with_help_message("The username that will be used to push built containers.") From 67d9e89e93fb618670bcdae839e12fa9d19a11c2 Mon Sep 17 00:00:00 2001 From: Robert Detjens Date: Mon, 10 Nov 2025 21:26:59 -0800 Subject: [PATCH 26/26] reword formatting comment Signed-off-by: Robert Detjens --- src/init/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/init/mod.rs b/src/init/mod.rs index 83263a9..fab782d 100644 --- a/src/init/mod.rs +++ b/src/init/mod.rs @@ -20,12 +20,12 @@ pub fn interactive_init() -> inquire::error::InquireResult { let difficulty_names; // set during `points` prompt later - // FORMATTING NOTE: The with_help_message() calls cause rustfmt to silently - // fail to format this whole definition. Commenting out the marked + // FORMATTING NOTE: Some of these help messages cause rustfmt to silently + // fail to format this struct definition. Commenting out the marked // help_message lines temporarily will let the formatting work. // - // see issues: - // - https://github.com/rust-lang/rustfmt/issues/6687, + // Ref: + // - https://github.com/rust-lang/rustfmt/issues/6687 // - https://github.com/rust-lang/rustfmt/issues/3863 let options = config::RcdsConfig {