summaryrefslogtreecommitdiff
path: root/ci-ctl
diff options
context:
space:
mode:
Diffstat (limited to 'ci-ctl')
-rw-r--r--ci-ctl/Cargo.toml15
-rw-r--r--ci-ctl/src/main.rs229
2 files changed, 244 insertions, 0 deletions
diff --git a/ci-ctl/Cargo.toml b/ci-ctl/Cargo.toml
new file mode 100644
index 0000000..e2662ad
--- /dev/null
+++ b/ci-ctl/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "ci-ctl"
+version = "0.0.1"
+authors = [ "iximeow <me@iximeow.net>" ]
+license = "0BSD"
+edition = "2021"
+
+[[bin]]
+name = "ci_ctl"
+path = "src/main.rs"
+
+[dependencies]
+ci-lib-core = { path = "../ci-lib-core" }
+ci-lib-native = { path = "../ci-lib-native" }
+clap = { version = "4", features = ["derive"] }
diff --git a/ci-ctl/src/main.rs b/ci-ctl/src/main.rs
new file mode 100644
index 0000000..bd2f733
--- /dev/null
+++ b/ci-ctl/src/main.rs
@@ -0,0 +1,229 @@
+use clap::{Parser, Subcommand};
+
+use ci_lib_core::dbctx::DbCtx;
+use ci_lib_native::notifier::NotifierConfig;
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)]
+struct Args {
+ /// path to a database to manage (defaults to "./state.db")
+ db_path: Option<String>,
+
+ /// path to where configs should be found (defaults to "./config")
+ config_path: Option<String>,
+
+ #[command(subcommand)]
+ command: Command,
+}
+
+#[derive(Subcommand)]
+enum Command {
+ /// add _something_ to the db
+ Add {
+ #[command(subcommand)]
+ what: AddItem,
+ },
+
+ /// make sure that the state looks reasonable.
+ ///
+ /// currently, ensure that all notifiers have a config, that config references an existing
+ /// file, and that the referenced file is valid.
+ Validate,
+
+ /// do something with jobs
+ Job {
+ #[command(subcommand)]
+ what: JobAction,
+ },
+}
+
+#[derive(Subcommand)]
+enum JobAction {
+ List,
+ Rerun {
+ which: u32
+ },
+ RerunCommit {
+ commit: String
+ },
+ Create {
+ repo: String,
+ commit: String,
+ pusher_email: String,
+ }
+}
+
+#[derive(Subcommand)]
+enum AddItem {
+ Repo {
+ name: String,
+ remote: Option<String>,
+ remote_kind: Option<String>,
+ config: Option<String>,
+ },
+ Remote {
+ repo_name: String,
+ remote: String,
+ remote_kind: String,
+ config: String,
+ },
+}
+
+fn main() {
+ let args = Args::parse();
+
+ let db_path = args.db_path.unwrap_or_else(|| "./state.db".to_owned());
+ let config_path = args.config_path.unwrap_or_else(|| "./config".to_owned());
+
+ match args.command {
+ Command::Job { what } => {
+ match what {
+ JobAction::List => {
+ let db = DbCtx::new(&config_path, &db_path);
+ let mut conn = db.conn.lock().unwrap();
+ let mut query = conn.prepare(ci_lib_core::sql::SELECT_ALL_RUNS_WITH_JOB_INFO).unwrap();
+ let mut jobs = query.query([]).unwrap();
+ while let Some(row) = jobs.next().unwrap() {
+ let (job_id, run_id, state, created_time, commit_id, run_preferences): (u64, u64, u64, u64, u64, Option<String>) = row.try_into().unwrap();
+
+ eprint!("[+] {:04} ({:04}) | {: >8?} | {} | {}", run_id, job_id, state, created_time, commit_id);
+ if let Some(run_preferences) = run_preferences {
+ eprintln!(" | run preference: {}", run_preferences);
+ } else {
+ eprintln!("");
+ }
+ }
+ eprintln!("jobs");
+ },
+ JobAction::Rerun { which } => {
+ let db = DbCtx::new(&config_path, &db_path);
+ let task_id = db.new_run(which as u64, None).expect("db can be queried").id;
+ eprintln!("[+] rerunning job {} as task {}", which, task_id);
+ }
+ JobAction::RerunCommit { commit } => {
+ let db = DbCtx::new(&config_path, &db_path);
+ let job_id = db.job_for_commit(&commit).unwrap();
+ if let Some(job_id) = job_id {
+ let task_id = db.new_run(job_id, None).expect("db can be queried").id;
+ eprintln!("[+] rerunning job {} (commit {}) as task {}", job_id, commit, task_id);
+ } else {
+ eprintln!("[-] no job for commit {}", commit);
+ }
+ }
+ JobAction::Create { repo, commit, pusher_email } => {
+ let db = DbCtx::new(&config_path, &db_path);
+ let parts = repo.split(":").collect::<Vec<&str>>();
+ let (remote_kind, repo_path) = (parts[0], parts[1]);
+ let remote = match db.remote_by_path_and_api(&remote_kind, &repo_path).expect("can query") {
+ Some(remote) => remote,
+ None => {
+ eprintln!("[-] no remote registered as {}:{}", remote_kind, repo_path);
+ return;
+ }
+ };
+
+ let repo_default_run_pref: Option<String> = db.conn.lock().unwrap()
+ .query_row("select default_run_preference from repos where id=?1;", [remote.repo_id], |row| {
+ Ok((row.get(0)).unwrap())
+ })
+ .expect("can query");
+
+ let job_id = db.new_job(remote.id, &commit, Some(&pusher_email), repo_default_run_pref).expect("can create");
+ let _ = db.new_run(job_id, None).unwrap();
+ }
+ }
+ },
+ Command::Add { what } => {
+ match what {
+ AddItem::Repo { name, remote, remote_kind, config } => {
+ let remote_config = match (remote, remote_kind, config) {
+ (Some(remote), Some(remote_kind), Some(config_path)) => {
+ // do something
+ if remote_kind != "github" {
+ eprintln!("unknown remote kind: {}", remote);
+ return;
+ }
+ Some((remote, remote_kind, config_path))
+ },
+ (None, None, None) => {
+ None
+ },
+ _ => {
+ eprintln!("when specifying a remote, `remote`, `remote_kind`, and `config_path` must either all be provided together or not at all");
+ return;
+ }
+ };
+
+ let db = DbCtx::new(&config_path, &db_path);
+ let repo_id = match db.new_repo(&name) {
+ Ok(repo_id) => repo_id,
+ Err(e) => {
+ if e.contains("UNIQUE constraint failed") {
+ eprintln!("[!] repo '{}' already exists", name);
+ return;
+ } else {
+ eprintln!("[!] failed to create repo entry: {}", e);
+ return;
+ }
+ }
+ };
+ println!("[+] new repo created: '{}' id {}", &name, repo_id);
+ if let Some((remote, remote_kind, config_path)) = remote_config {
+ let full_config_file_path = format!("{}/{}", &db.config_path.display(), config_path);
+ let config = match remote_kind.as_ref() {
+ "github" => {
+ assert!(NotifierConfig::github_from_file(&full_config_file_path).is_ok());
+ }
+ "github-email" => {
+ assert!(NotifierConfig::email_from_file(&full_config_file_path).is_ok());
+ }
+ other => {
+ panic!("[-] notifiers for '{}' remotes are not supported", other);
+ }
+ };
+ db.new_remote(repo_id, remote.as_str(), remote_kind.as_str(), config_path.as_str()).unwrap();
+ println!("[+] new remote created: repo '{}', {} remote at {}", &name, remote_kind, remote);
+ match remote_kind.as_str() {
+ "github" => {
+ println!("[!] now go make sure your github repo has a webhook set for `https://ci.butactuallyin.space/{}` to receive at least the `push` event.", remote.as_str());
+ println!(" the secret sent with calls to this webhook should be the same preshared secret as the CI server is configured to know.");
+ }
+ _ => { }
+ }
+ }
+ },
+ AddItem::Remote { repo_name, remote, remote_kind, config } => {
+ let db = DbCtx::new(&config_path, &db_path);
+ let repo_id = match db.repo_id_by_name(&repo_name) {
+ Ok(Some(id)) => id,
+ Ok(None) => {
+ eprintln!("[-] repo '{}' does not exist", repo_name);
+ return;
+ },
+ Err(e) => {
+ eprintln!("[!] couldn't look up repo '{}': {:?}", repo_name, e);
+ return;
+ }
+ };
+ let config_file = format!("{}/{}", config_path, config);
+ match remote_kind.as_ref() {
+ "github" => {
+ NotifierConfig::github_from_file(&config_file).unwrap();
+ }
+ "github-email" => {
+ NotifierConfig::email_from_file(&config_file).unwrap();
+ }
+ other => {
+ panic!("notifiers for '{}' remotes are not supported", other);
+ }
+ };
+ db.new_remote(repo_id, remote.as_str(), remote_kind.as_str(), config.as_str()).unwrap();
+ println!("[+] new remote created: repo '{}', {} remote at {}", &repo_name, remote_kind, remote);
+ },
+ }
+ },
+ Command::Validate => {
+ println!("ok");
+ }
+ }
+}