| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | #![recursion_limit = "256"] |
| 8 | |
| 9 | mod access; |
| 10 | mod admin_cli; |
| 11 | mod admin_client; |
| 12 | mod advisories; |
| 13 | mod api; |
| 14 | mod audit; |
| 15 | mod auth; |
| 16 | mod backup; |
| 17 | mod blobs; |
| 18 | mod boot; |
| 19 | mod ci; |
| 20 | mod client_ip; |
| 21 | mod compression; |
| 22 | mod git_auth; |
| 23 | mod git_http; |
| 24 | mod hook_cmd; |
| 25 | mod i18n; |
| 26 | mod internal; |
| 27 | mod lfs; |
| 28 | mod lfs_pointer; |
| 29 | mod mail; |
| 30 | mod mirrors; |
| 31 | mod notify; |
| 32 | mod pages; |
| 33 | mod post_update; |
| 34 | mod pulls_sync; |
| 35 | mod raw; |
| 36 | mod render; |
| 37 | mod replication; |
| 38 | mod signatures; |
| 39 | mod ssh_backend; |
| 40 | mod state; |
| 41 | mod templates; |
| 42 | #[cfg(test)] |
| 43 | mod test_support; |
| 44 | mod theme; |
| 45 | mod throttle; |
| 46 | mod views; |
| 47 | mod webhooks; |
| 48 | |
| 49 | use std::path::PathBuf; |
| 50 | use std::process::ExitCode; |
| 51 | |
| 52 | use forge_core::Config; |
| 53 | use tracing_subscriber::EnvFilter; |
| 54 | |
| 55 | use topcoat::router::route; |
| 56 | |
| 57 | type BoxError = Box<dyn std::error::Error + Send + Sync>; |
| 58 | |
| 59 | const USAGE: &str = "usage: forge [--config <path>] [serve | hook <pre-receive|post-receive> | runner <register|run|check> ... | admin create-user <username> [--admin] | admin set-username <current> <NewCase> | admin reset-password <username> | admin trash-list | admin trash-purge --older-than-days <N> | admin audit-purge --older-than-days <N> | admin delete-user <username> [--yes] | admin backup --out <DIR> [--include-key] [--repos-only | --db-only] | admin backup-verify <PATH> | admin fsck [--full] | admin lfs-gc | admin restore --from <SET> --data-dir <NEW_DIR>]"; |
| 60 | const DEFAULT_CONFIG: &str = "config/forge.toml"; |
| 61 | |
| 62 | #[route(GET "/healthz")] |
| 63 | async fn healthz() -> Result<String, topcoat::Error> { |
| 64 | Ok(format!("ok {}", env!("FORGE_BUILD_SHA"))) |
| 65 | } |
| 66 | |
| 67 | async fn serve(config_path: &std::path::Path) -> Result<(), BoxError> { |
| 68 | let config = Config::load(config_path)?; |
| 69 | boot::serve(config).await?; |
| 70 | Ok(()) |
| 71 | } |
| 72 | |
| 73 | |
| 74 | fn parse_args( |
| 75 | args: impl Iterator<Item = String>, |
| 76 | ) -> Result<(Option<PathBuf>, bool, Vec<String>), String> { |
| 77 | let mut config = None; |
| 78 | let mut admin = false; |
| 79 | let mut positional = Vec::new(); |
| 80 | let mut args = args; |
| 81 | while let Some(arg) = args.next() { |
| 82 | match arg.as_str() { |
| 83 | "--config" => { |
| 84 | let path = args.next().ok_or("--config needs a path")?; |
| 85 | config = Some(PathBuf::from(path)); |
| 86 | } |
| 87 | "--admin" => admin = true, |
| 88 | _ => positional.push(arg), |
| 89 | } |
| 90 | } |
| 91 | Ok((config, admin, positional)) |
| 92 | } |
| 93 | |
| 94 | fn main() -> ExitCode { |
| 95 | |
| 96 | if forge_runner::cli::is_sandbox_invocation() { |
| 97 | std::process::exit(forge_runner::cli::hidden_main()); |
| 98 | } |
| 99 | |
| 100 | |
| 101 | let raw: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect(); |
| 102 | if raw.first().is_some_and(|arg| arg == "runner") { |
| 103 | return forge_runner::cli::main_with_args("forge runner", raw.into_iter().skip(1)); |
| 104 | } |
| 105 | let runtime = match tokio::runtime::Builder::new_multi_thread() |
| 106 | .enable_all() |
| 107 | .build() |
| 108 | { |
| 109 | Ok(runtime) => runtime, |
| 110 | Err(e) => { |
| 111 | eprintln!("forge: cannot start the runtime: {e}"); |
| 112 | return ExitCode::FAILURE; |
| 113 | } |
| 114 | }; |
| 115 | runtime.block_on(real_main()) |
| 116 | } |
| 117 | |
| 118 | async fn real_main() -> ExitCode { |
| 119 | |
| 120 | let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); |
| 121 | tracing_subscriber::fmt() |
| 122 | .with_env_filter(filter) |
| 123 | .with_writer(std::io::stderr) |
| 124 | .init(); |
| 125 | |
| 126 | let (flag_config, admin, positional) = match parse_args(std::env::args().skip(1)) { |
| 127 | Ok(parsed) => parsed, |
| 128 | Err(e) => { |
| 129 | eprintln!("forge: {e}\n{USAGE}"); |
| 130 | return ExitCode::from(2); |
| 131 | } |
| 132 | }; |
| 133 | let config_path = flag_config |
| 134 | .or_else(|| std::env::var_os("FORGE_CONFIG").map(PathBuf::from)) |
| 135 | .unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG)); |
| 136 | let parts: Vec<&str> = positional.iter().map(String::as_str).collect(); |
| 137 | |
| 138 | let outcome: Result<(), BoxError> = match parts.as_slice() { |
| 139 | [] | ["serve"] => serve(&config_path).await, |
| 140 | ["hook", stage] => hook_cmd::run(stage).await.map_err(Into::into), |
| 141 | ["admin", "create-user", username] => admin_cli::create_user(&config_path, username, admin) |
| 142 | .await |
| 143 | .map_err(Into::into), |
| 144 | ["admin", "reset-password", username] => admin_cli::reset_password(&config_path, username) |
| 145 | .await |
| 146 | .map_err(Into::into), |
| 147 | ["admin", "set-username", current, new_display] => { |
| 148 | admin_cli::set_username(&config_path, current, new_display) |
| 149 | .await |
| 150 | .map_err(Into::into) |
| 151 | } |
| 152 | ["admin", "backup", rest @ ..] => match admin_cli::parse_backup_args(rest) { |
| 153 | Ok(opts) => admin_cli::backup(&config_path, opts) |
| 154 | .await |
| 155 | .map_err(Into::into), |
| 156 | Err(e) => { |
| 157 | eprintln!("forge: {e}\n{USAGE}"); |
| 158 | return ExitCode::from(2); |
| 159 | } |
| 160 | }, |
| 161 | ["admin", "restore", rest @ ..] => match admin_cli::parse_restore_args(rest) { |
| 162 | Ok((from, data_dir)) => admin_cli::restore(&from, &data_dir) |
| 163 | .await |
| 164 | .map_err(Into::into), |
| 165 | Err(e) => { |
| 166 | eprintln!("forge: {e}\n{USAGE}"); |
| 167 | return ExitCode::from(2); |
| 168 | } |
| 169 | }, |
| 170 | ["admin", "fsck"] => admin_cli::fsck(&config_path, false) |
| 171 | .await |
| 172 | .map_err(Into::into), |
| 173 | ["admin", "fsck", "--full"] => admin_cli::fsck(&config_path, true) |
| 174 | .await |
| 175 | .map_err(Into::into), |
| 176 | ["admin", "lfs-gc"] => admin_cli::lfs_gc(&config_path).await.map_err(Into::into), |
| 177 | ["admin", "backup-verify", path] => admin_cli::backup_verify(std::path::Path::new(path)) |
| 178 | .await |
| 179 | .map_err(Into::into), |
| 180 | ["admin", "trash-list"] => admin_cli::trash_list(&config_path).map_err(Into::into), |
| 181 | ["admin", "delete-user", username] => admin_cli::delete_user(&config_path, username, false) |
| 182 | .await |
| 183 | .map_err(Into::into), |
| 184 | ["admin", "delete-user", username, "--yes"] => { |
| 185 | admin_cli::delete_user(&config_path, username, true) |
| 186 | .await |
| 187 | .map_err(Into::into) |
| 188 | } |
| 189 | ["admin", "audit-purge", "--older-than-days", days] => match days.parse::<u64>() { |
| 190 | Ok(days) => admin_cli::audit_purge(&config_path, days) |
| 191 | .await |
| 192 | .map_err(Into::into), |
| 193 | Err(_) => { |
| 194 | eprintln!("forge: --older-than-days needs a whole number of days\n{USAGE}"); |
| 195 | return ExitCode::from(2); |
| 196 | } |
| 197 | }, |
| 198 | ["admin", "trash-purge", "--older-than-days", days] => match days.parse::<u64>() { |
| 199 | Ok(days) => admin_cli::trash_purge(&config_path, days).map_err(Into::into), |
| 200 | Err(_) => { |
| 201 | eprintln!("forge: --older-than-days needs a whole number of days\n{USAGE}"); |
| 202 | return ExitCode::from(2); |
| 203 | } |
| 204 | }, |
| 205 | _ => { |
| 206 | eprintln!("{USAGE}"); |
| 207 | return ExitCode::from(2); |
| 208 | } |
| 209 | }; |
| 210 | |
| 211 | match outcome { |
| 212 | Ok(()) => ExitCode::SUCCESS, |
| 213 | Err(e) => { |
| 214 | eprintln!("forge: {e}"); |
| 215 | ExitCode::FAILURE |
| 216 | } |
| 217 | } |
| 218 | } |