Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

af/init command #30

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 219 additions & 21 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ bollard = "0.16.1"
tar = "0.4.42"
tempfile = "3.13.0"
figment = { version = "0.10.19", features = ["env", "yaml"] }
inquire = "0.7.5"
regex = "1.11.1"
13 changes: 13 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,17 @@ pub enum Commands {
#[arg(short, long)]
registry: bool,
},

/// Copy an initial rcds.yaml to the current working directory.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this command can do interactive prompts aside from non-interactive copy/write-out, this description should a bit more generic, something like

Suggested change
/// Copy an initial rcds.yaml to the current working directory.
/// Set up a new rcds.yaml config in the current working directory

///
/// If interactive is enabled, then it will prompt for the various fields of the config file. If left disabled, then it will copy it out with fake data of the expected format.
///
/// If blank is enabled, then it will copy out the file without any fields set. If left disabled, it will write the default non-interactive example config to file.
Comment on lines +75 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be on the arguments since they are describing what they do.

Init {
/// Guided filling out of the config
#[arg(short = 'i', long)]
interactive: bool,
#[arg(short = 'b', long)]
blank: bool
}
}
337 changes: 337 additions & 0 deletions src/commands/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,337 @@
use inquire;
use tera;
use std::fmt;
use regex::Regex;

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

struct init_vars
{
flag_regex: String, // TODO: make all of these `str`s if it compiles
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structs need to own the data they contain, so these do all have to be Strings.

registry_domain: String,
registry_build_user: String,
registry_build_pass: String,
registry_cluster_user: String,
registry_cluster_pass: String,
defaults_difficulty: String, //u64,
defaults_resources_cpu: String, //u64,
defaults_resources_memory: String, //(u64, Option(String)),
points: Vec<points>,
profiles: Vec<profile>
}

#[derive(Clone)]
struct points {
difficulty: String, //u64,
min: String, //u64,
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)
}
}

struct profile {
profile_name: String,
frontend_url: String,
frontend_token: String,
challenges_domain: String,
kubecontext: String,
s3_endpoint: String,
s3_region: String,
s3_accesskey: String,
s3_secretaccesskey: String
}

pub fn run(_interactive: &bool, _blank: &bool) // TODO: is there a way to set options at mutually exclusive?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is, conflicts_with in clap-derive (docs)

{
let options: init_vars;

if *_interactive {
options = match interactive_init()
{
Ok(t) => t,
Err(e) =>
{
println!("Error in init: {e}");
return;
}
};
}
else if *_blank {
options = blank_init();
}
else {
options = noninteractive_init();
}

// TODO -- does not compile because options does not yet implement serialize
// comment out if wanting to test the rest of the code
Comment on lines +70 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding #[derive(Serialize)] to the other structs fixes this

let mut t = tera::Tera::new("src/templates/rcds.yaml.j2").unwrap();
let ctx = tera::Context::from_serialize(options).unwrap();
let rendered = t.render("rcds.yaml.j2", &ctx).unwrap();
println!("{rendered}");
}


fn interactive_init() -> inquire::error::InquireResult<init_vars>
{
let flag_regex;
let registry_domain;
let registry_build_user;
let registry_build_pass;
let registry_cluster_user;
let registry_cluster_pass;
let defaults_difficulty;
let defaults_resources_cpu;
let defaults_resources_memory;
let mut points_difficulty = Vec::new();
let mut deploy_profiles = Vec::new();
Comment on lines +81 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be an mut init_vars struct instead of a bunch of individual variables, since these are all getting set into one after the prompts.

(ditto for points and profile sections)


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.");

flag_regex = inquire::Text::new ("Flag regex:")
.with_help_message("This regex will be used to validate the individual flags of your challenges later.") // TODO: also provide regex examples for help
.prompt()?; // yo is this even a good idea to have the user provide the regex
// TODO: with placeholder?

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.")
.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?
Comment on lines +105 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, "your" is a bit confusing since the config calls this build credentials -- "local/build" might work better?

.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.")
.prompt()?;
Comment on lines +120 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kubernetes uses the same base64("username:password") credential format as Docker does, so we're matching that here.


println!("You can define several challenge difficulty classes below:");
loop
{
// TODO: theres no reason these need to be numbers instead of open strings, e.g. for "easy"
let difficulty_class_rank = inquire::CustomType::<u64>::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()?;

// TODO: support static-point challenges
let difficulty_class_min = inquire::CustomType::<u64>::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 maximum number of points that challenges within this difficulty class are worth.")
.prompt()?;

let difficulty_class_max = inquire::CustomType::<u64>::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 minimum number of points that challenges within this difficulty class are worth.")
.prompt()?;

let points_object = points {
difficulty: difficulty_class_rank.to_string(),
min: difficulty_class_min.to_string(),
max: difficulty_class_max.to_string()
};
points_difficulty.push(points_object);

let again = inquire::Confirm::new("Do you want to provide another difficulty class?")
.with_default(false)
.prompt()?;
if !again
{
break;
}
}

defaults_difficulty = inquire::Select::new("Please choose the default difficulty class:", points_difficulty.clone())
.prompt()?;

// 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_resources_cpu = inquire::CustomType::<u64>::new("Default CPUs per challenge:")
// // default parser calls std::u64::from_str
// .with_error_message("Please type a valid number.")
// .with_help_message("The maximum limit of CPU resources per instance of challenge deployment (\"pod\").")
// .prompt()?;
defaults_resources_cpu = inquire::Text::new("Default limit of CPUs per challenge")
.with_help_message("The maximum limit of CPU resources per instance of challenge deployment (\"pod\").")
.prompt()?;

// defaults_resources_memory = inquire::CustomType::<String>::new("")
// .with_parser(&|i|
// {
// let re = Regex::new(r"^[0-9]+$") // TODO
// })
defaults_resources_memory = inquire::Text::new("Default limit of memory per challenge")
.with_help_message("The maximum limit of memory resources per instance of challenge deployment (\"pod\").")
.prompt()?;
Comment on lines +164 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if these need to be prompted for. Most challenges are going to be fine with a reasonable default resource limit, and I think we should figure out what that default is and always include it. I think users will very rarely need to change these here for all challenges, and that can be left to manual edits.


println!("You can define several challenge difficulty classes below.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong header message?

loop {
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.")
.prompt()?;
let 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()?;

let 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()?;

let challenges_domain = inquire::Text::new("Challenges domain:")
.with_help_message("Domain that challenges are hosted under.")
.prompt()?;

let kubecontext = inquire::Text::new("Kube context:")
.with_help_message("The name of the context that kubectl looks for to interface with the cluster.")
.prompt()?;

let s3_endpoint = inquire::Text::new("S3 endpoint:")
.with_help_message("Challenge artifacts and static files will be hosted on and served from S3. The endpoint of the S3 bucket server.")
.prompt()?;

let s3_region = inquire::Text::new("S3 region:")
.with_help_message("The region that the S3 bucket is hosted.")
.prompt()?;

let s3_accesskey = inquire::Text::new("S3 access key:")
.with_help_message("The public access key to the S3 bucket.")
.prompt()?;

let s3_secretkey = inquire::Text::new("S3 secret key:")
.with_help_message("The secret acess key to the S3 bucket.")
.prompt()?;

let profile_object = profile {
profile_name: name,
frontend_url: frontend_url,
frontend_token: frontend_token,
challenges_domain: challenges_domain,
kubecontext: kubecontext,
s3_endpoint: s3_endpoint,
s3_region: s3_region,
s3_accesskey: s3_accesskey,
s3_secretaccesskey: s3_secretkey
};
deploy_profiles.push(profile_object);

let again = inquire::Confirm::new("Do you want to provide another deployment profile?")
.with_default(false)
.prompt()?;
if !again
{
break;
}
}

// Put everything into the struct and return it
let options = init_vars {
flag_regex: flag_regex,
registry_domain: registry_domain,
registry_build_user: registry_build_user,
registry_build_pass: registry_build_pass,
registry_cluster_user: registry_cluster_user,
registry_cluster_pass: registry_cluster_pass,
defaults_difficulty: defaults_difficulty.difficulty,
defaults_resources_cpu: defaults_resources_cpu,
defaults_resources_memory: defaults_resources_memory,
points: points_difficulty,
profiles: deploy_profiles
};

return Ok(options);
}


fn noninteractive_init() -> init_vars
{
return init_vars {
flag_regex: String::from("ctf{.*}"), // TODO: do that wildcard in 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"), //(500, Some(String::from("M"))),
points: vec![
points {
difficulty: String::from("1"),
min: String::from("69"),
max: String::from("420")
},
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_endpoint: String::from("s3.coolguy.xyz"),
s3_region: String::from("us-west-2"),
s3_accesskey: String::from("accesskey"),
s3_secretaccesskey: String::from("secretkey")
}
]
};
}


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::new(),
frontend_url: String::new(),
frontend_token: String::new(),
challenges_domain: String::new(),
kubecontext: String::new(),
s3_endpoint: String::new(),
s3_region: String::new(),
s3_accesskey: String::new(),
s3_secretaccesskey: String::new(),
}
]
};
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod build;
pub mod check_access;
pub mod deploy;
pub mod validate;
pub mod init;
5 changes: 5 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,10 @@ fn main() {
commands::validate::run();
commands::deploy::run(profile, no_build, dry_run)
}

cli::Commands::Init {
interactive,
blank
} => commands::init::run(interactive, blank)
}
}
Loading
Loading