Author:
Hash:
Timestamp:
+348 -192 +/-19 browse
Kevin Schoon [me@kevinschoon.com]
72ba4353d1f6c846355f0a2883aedc587d80f102
Sat, 23 Aug 2025 16:00:46 +0000 (2 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index 912aec5..9c43931 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -322,12 +322,12 @@ dependencies = [ |
| 6 | "axum", |
| 7 | "axum-extra", |
| 8 | "ayllu_api", |
| 9 | + "ayllu_cmd", |
| 10 | "ayllu_config", |
| 11 | "ayllu_git", |
| 12 | "ayllu_identity", |
| 13 | "bytes", |
| 14 | "cc", |
| 15 | - "clap 4.5.41", |
| 16 | "comrak", |
| 17 | "file-mode", |
| 18 | "futures", |
| 19 | @@ -372,9 +372,9 @@ dependencies = [ |
| 20 | name = "ayllu-keys" |
| 21 | version = "0.5.1" |
| 22 | dependencies = [ |
| 23 | + "ayllu_cmd", |
| 24 | "ayllu_config", |
| 25 | "ayllu_identity", |
| 26 | - "clap 4.5.41", |
| 27 | "serde", |
| 28 | ] |
| 29 | |
| 30 | @@ -382,10 +382,10 @@ dependencies = [ |
| 31 | name = "ayllu-shell" |
| 32 | version = "0.5.1" |
| 33 | dependencies = [ |
| 34 | + "ayllu_cmd", |
| 35 | "ayllu_config", |
| 36 | "ayllu_git", |
| 37 | "ayllu_identity", |
| 38 | - "clap 4.5.41", |
| 39 | "dialoguer", |
| 40 | "nix", |
| 41 | "serde", |
| 42 | @@ -402,6 +402,16 @@ dependencies = [ |
| 43 | ] |
| 44 | |
| 45 | [[package]] |
| 46 | + name = "ayllu_cmd" |
| 47 | + version = "0.1.0" |
| 48 | + dependencies = [ |
| 49 | + "ayllu_config", |
| 50 | + "clap 4.5.41", |
| 51 | + "tracing", |
| 52 | + "url", |
| 53 | + ] |
| 54 | + |
| 55 | + [[package]] |
| 56 | name = "ayllu_config" |
| 57 | version = "0.2.1" |
| 58 | dependencies = [ |
| 59 | @@ -660,6 +670,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 60 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" |
| 61 | |
| 62 | [[package]] |
| 63 | + name = "clap_mangen" |
| 64 | + version = "0.2.29" |
| 65 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 66 | + checksum = "27b4c3c54b30f0d9adcb47f25f61fcce35c4dd8916638c6b82fbd5f4fb4179e2" |
| 67 | + dependencies = [ |
| 68 | + "clap 4.5.41", |
| 69 | + "roff", |
| 70 | + ] |
| 71 | + |
| 72 | + [[package]] |
| 73 | name = "colorchoice" |
| 74 | version = "1.0.4" |
| 75 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 76 | @@ -2342,6 +2362,7 @@ name = "quipu" |
| 77 | version = "0.5.1" |
| 78 | dependencies = [ |
| 79 | "ayllu_api", |
| 80 | + "ayllu_cmd", |
| 81 | "ayllu_config", |
| 82 | "ayllu_git", |
| 83 | "clap 4.5.41", |
| 84 | @@ -2598,6 +2619,12 @@ dependencies = [ |
| 85 | ] |
| 86 | |
| 87 | [[package]] |
| 88 | + name = "roff" |
| 89 | + version = "0.2.2" |
| 90 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
| 91 | + checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" |
| 92 | + |
| 93 | + [[package]] |
| 94 | name = "rss" |
| 95 | version = "2.0.12" |
| 96 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 97 | @@ -4138,6 +4165,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
| 98 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" |
| 99 | |
| 100 | [[package]] |
| 101 | + name = "xtask" |
| 102 | + version = "0.1.0" |
| 103 | + dependencies = [ |
| 104 | + "ayllu_cmd", |
| 105 | + "clap 4.5.41", |
| 106 | + "clap_mangen", |
| 107 | + ] |
| 108 | + |
| 109 | + [[package]] |
| 110 | name = "yoke" |
| 111 | version = "0.8.0" |
| 112 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 113 | diff --git a/Cargo.toml b/Cargo.toml |
| 114 | index 717542d..8f2214d 100644 |
| 115 | --- a/Cargo.toml |
| 116 | +++ b/Cargo.toml |
| 117 | @@ -16,7 +16,7 @@ members = [ |
| 118 | "ayllu-keys", |
| 119 | # "ayllu-jobs", |
| 120 | # "ayllu-xmpp", |
| 121 | - "quipu", "crates/identity", |
| 122 | + "quipu", "crates/identity", "crates/cmd", "xtask", |
| 123 | ] |
| 124 | |
| 125 | [workspace.dependencies] |
| 126 | @@ -31,8 +31,6 @@ thiserror = "2.0.12" |
| 127 | tracing = { version = "0.1.41", features=["log"] } |
| 128 | toml = "0.8.23" |
| 129 | futures = "0.3.31" |
| 130 | - futures-util = "0.3.31" |
| 131 | - sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", "macros", "time" ] } |
| 132 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } |
| 133 | openssh-keys = "0.6.4" |
| 134 | url = { version = "2.5.4", features = ["serde"]} |
| 135 | diff --git a/ayllu-keys/Cargo.toml b/ayllu-keys/Cargo.toml |
| 136 | index 3d7e822..fca312b 100644 |
| 137 | --- a/ayllu-keys/Cargo.toml |
| 138 | +++ b/ayllu-keys/Cargo.toml |
| 139 | @@ -5,8 +5,8 @@ edition = "2021" |
| 140 | |
| 141 | [dependencies] |
| 142 | |
| 143 | + ayllu_cmd = { path = "../crates/cmd"} |
| 144 | ayllu_config = { path = "../crates/config" } |
| 145 | ayllu_identity = { path = "../crates/identity" } |
| 146 | |
| 147 | - clap = { workspace = true } |
| 148 | serde = { workspace = true } |
| 149 | diff --git a/ayllu-keys/src/main.rs b/ayllu-keys/src/main.rs |
| 150 | index 281cbfb..c0afb0a 100644 |
| 151 | --- a/ayllu-keys/src/main.rs |
| 152 | +++ b/ayllu-keys/src/main.rs |
| 153 | @@ -1,18 +1,11 @@ |
| 154 | - /// ayllu-keys handles authentication for remote users in a shared environment. |
| 155 | - /// There are a few scenarios which it needs to account for: |
| 156 | - /// |
| 157 | - /// * A system user that is not defined in the global ayllu-config is |
| 158 | - /// requesting shell access via a regular ssh channel. |
| 159 | - /// * An Ayllu managed user is requesting shell access. |
| 160 | - /// * An Ayllu managed user is running git operations as the git service. |
| 161 | use std::{ |
| 162 | path::{Path, PathBuf}, |
| 163 | process::ExitCode, |
| 164 | }; |
| 165 | |
| 166 | + use ayllu_cmd::keys::Command; |
| 167 | use ayllu_config::Configurable; |
| 168 | use ayllu_identity::Identity; |
| 169 | - use clap::Parser; |
| 170 | |
| 171 | use serde::{Deserialize, Serialize}; |
| 172 | |
| 173 | @@ -26,19 +19,6 @@ pub struct Config { |
| 174 | |
| 175 | impl Configurable for Config {} |
| 176 | |
| 177 | - #[derive(Parser, Debug)] |
| 178 | - #[clap(version, about, long_about = "Ayllu Keys")] |
| 179 | - struct Arguments { |
| 180 | - username: String, |
| 181 | - home_dir: PathBuf, |
| 182 | - ca_key_type: String, |
| 183 | - certificate: String, |
| 184 | - #[clap(short, long, value_name = "FILE")] |
| 185 | - config: Option<PathBuf>, |
| 186 | - #[clap(long, value_name = "ayllu-shell")] |
| 187 | - ayllu_shell: Option<PathBuf>, |
| 188 | - } |
| 189 | - |
| 190 | fn authorized_keys(home_dir: &Path) -> ExitCode { |
| 191 | let authorized_keys_path = home_dir.join(".ssh/authorized_keys"); |
| 192 | if let Ok(mut fp) = std::fs::File::open(authorized_keys_path) { |
| 193 | @@ -56,14 +36,13 @@ fn authorized_keys(home_dir: &Path) -> ExitCode { |
| 194 | } |
| 195 | |
| 196 | fn identify<'a>(config: &'a Config, ca_key_type: &str, certificate: &str) -> Option<&'a Identity> { |
| 197 | - let user_key = |
| 198 | - match ayllu_identity::PublicKey::parse(&format!("{ca_key_type} {certificate}")) { |
| 199 | - Ok(key) => key, |
| 200 | - Err(e) => { |
| 201 | - eprintln!("Cannot parse SSH key from sshd: {e:?}"); |
| 202 | - return None; |
| 203 | - } |
| 204 | - }; |
| 205 | + let user_key = match ayllu_identity::PublicKey::parse(&format!("{ca_key_type} {certificate}")) { |
| 206 | + Ok(key) => key, |
| 207 | + Err(e) => { |
| 208 | + eprintln!("Cannot parse SSH key from sshd: {e:?}"); |
| 209 | + return None; |
| 210 | + } |
| 211 | + }; |
| 212 | |
| 213 | match config.identities.iter().find(|identity| { |
| 214 | identity |
| 215 | @@ -80,7 +59,7 @@ fn identify<'a>(config: &'a Config, ca_key_type: &str, certificate: &str) -> Opt |
| 216 | } |
| 217 | |
| 218 | fn main() -> ExitCode { |
| 219 | - let args = Arguments::parse(); |
| 220 | + let args = ayllu_cmd::parse::<Command>(); |
| 221 | |
| 222 | if args.username != "ayllu" { |
| 223 | return authorized_keys(&args.home_dir); |
| 224 | diff --git a/ayllu-shell/Cargo.toml b/ayllu-shell/Cargo.toml |
| 225 | index 5c8965f..97cf2cd 100644 |
| 226 | --- a/ayllu-shell/Cargo.toml |
| 227 | +++ b/ayllu-shell/Cargo.toml |
| 228 | @@ -5,11 +5,11 @@ edition = "2021" |
| 229 | |
| 230 | [dependencies] |
| 231 | |
| 232 | + ayllu_cmd = { path = "../crates/cmd" } |
| 233 | ayllu_config = { path = "../crates/config" } |
| 234 | ayllu_git = { path = "../crates/git"} |
| 235 | ayllu_identity = { path = "../crates/identity" } |
| 236 | |
| 237 | - clap = { workspace = true } |
| 238 | dialoguer = { version = "0.11.0", default-features = false } |
| 239 | nix = { version = "0.30.1", default-features = false, features = ["user"] } |
| 240 | serde = { workspace = true } |
| 241 | diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs |
| 242 | index 7a23919..11ad2e7 100644 |
| 243 | --- a/ayllu-shell/src/main.rs |
| 244 | +++ b/ayllu-shell/src/main.rs |
| 245 | @@ -1,54 +1,11 @@ |
| 246 | - use std::path::PathBuf; |
| 247 | - |
| 248 | + use ayllu_cmd::shell::{Command, Subcommand}; |
| 249 | use ayllu_git::Wrapper; |
| 250 | - use clap::{Parser, Subcommand}; |
| 251 | |
| 252 | mod config; |
| 253 | mod error; |
| 254 | mod ui; |
| 255 | |
| 256 | - #[derive(Subcommand, Debug)] |
| 257 | - enum Commands { |
| 258 | - /// Invokes git-receive-pack |
| 259 | - GitReceivePack { |
| 260 | - /// Path to the git repository |
| 261 | - path: PathBuf, |
| 262 | - }, |
| 263 | - /// Invokes git-upload-pack |
| 264 | - GitUploadPack { |
| 265 | - /// Path to the git repository |
| 266 | - path: PathBuf, |
| 267 | - }, |
| 268 | - } |
| 269 | - |
| 270 | - #[derive(Parser, Debug)] |
| 271 | - #[clap( |
| 272 | - version, |
| 273 | - about, |
| 274 | - long_about = r#" |
| 275 | - |
| 276 | - Ayllu Shell |
| 277 | - |
| 278 | - ayllu-shell provides a restricted shell for performing operations against |
| 279 | - an Ayllu server. Although it may be invoked directly, typically it is |
| 280 | - expected to be paired with ayllu-keys and an OpenSSH server. |
| 281 | - "# |
| 282 | - )] |
| 283 | - struct Arguments { |
| 284 | - /// sh compatibility |
| 285 | - #[clap(short, action)] |
| 286 | - c: Option<String>, |
| 287 | - /// Path to a configuration file |
| 288 | - #[clap(long)] |
| 289 | - config: Option<PathBuf>, |
| 290 | - /// Specify the user identity to assume |
| 291 | - #[clap(short, long)] |
| 292 | - username: Option<String>, |
| 293 | - #[command(subcommand)] |
| 294 | - command: Option<Commands>, |
| 295 | - } |
| 296 | - |
| 297 | - fn execute(args: Arguments) -> Result<(), Box<dyn std::error::Error>> { |
| 298 | + fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> { |
| 299 | let config: config::Config = ayllu_config::Reader::load(args.config.as_deref())?; |
| 300 | let username = args |
| 301 | .username |
| 302 | @@ -58,7 +15,7 @@ fn execute(args: Arguments) -> Result<(), Box<dyn std::error::Error>> { |
| 303 | .iter() |
| 304 | .find(|identity| identity.username == username); |
| 305 | match args.command { |
| 306 | - Some(Commands::GitReceivePack { path }) => { |
| 307 | + Some(Subcommand::GitReceivePack { path }) => { |
| 308 | let resolved = if let Some(base_dir) = config.base_dir { |
| 309 | let trimmed = if path.is_absolute() { |
| 310 | path.strip_prefix("/").unwrap() |
| 311 | @@ -88,7 +45,7 @@ fn execute(args: Arguments) -> Result<(), Box<dyn std::error::Error>> { |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | - Some(Commands::GitUploadPack { ref path }) => { |
| 316 | + Some(Subcommand::GitUploadPack { ref path }) => { |
| 317 | let resolved = if path.is_relative() { |
| 318 | if let Some(base_dir) = config.base_dir { |
| 319 | &base_dir.join(path) |
| 320 | @@ -129,17 +86,16 @@ fn execute(args: Arguments) -> Result<(), Box<dyn std::error::Error>> { |
| 321 | // TODO: Add RBAC style permissions per identity |
| 322 | fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 323 | eprintln!("Args: {:?}", std::env::args()); |
| 324 | - let args = Arguments::parse(); |
| 325 | + let args = ayllu_cmd::parse::<Command>(); |
| 326 | if let Some(cmd) = args.c { |
| 327 | let unescaped = cmd.replace("\'", "").replace("\"", ""); |
| 328 | - let parts = unescaped.split(" ").fold( |
| 329 | - vec!["ayllu-shell".to_string()], |
| 330 | - |mut accm, part| { |
| 331 | + let parts = unescaped |
| 332 | + .split(" ") |
| 333 | + .fold(vec!["ayllu-shell".to_string()], |mut accm, part| { |
| 334 | accm.push(part.to_string()); |
| 335 | accm |
| 336 | - }, |
| 337 | - ); |
| 338 | - return execute(Arguments::parse_from(parts)); |
| 339 | + }); |
| 340 | + return execute(ayllu_cmd::parse_from(parts.as_slice())); |
| 341 | }; |
| 342 | execute(args) |
| 343 | } |
| 344 | diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml |
| 345 | index 2cee0a7..770dcf8 100644 |
| 346 | --- a/ayllu/Cargo.toml |
| 347 | +++ b/ayllu/Cargo.toml |
| 348 | @@ -9,6 +9,7 @@ name = "ayllu" |
| 349 | |
| 350 | [dependencies] |
| 351 | ayllu_api = { path = "../crates/api" } |
| 352 | + ayllu_cmd = { path = "../crates/cmd" } |
| 353 | ayllu_git = { path = "../crates/git" } |
| 354 | ayllu_identity = {path = "../crates/identity"} |
| 355 | ayllu_config = { path = "../crates/config" } |
| 356 | @@ -16,7 +17,6 @@ timeutil = {path = "../crates/timeutil"} |
| 357 | |
| 358 | async-trait = { workspace = true } |
| 359 | bytes = { workspace = true } |
| 360 | - clap = { workspace = true } |
| 361 | futures = { workspace = true } |
| 362 | thiserror = { workspace = true } |
| 363 | tracing = { workspace = true } |
| 364 | diff --git a/ayllu/src/main.rs b/ayllu/src/main.rs |
| 365 | index 61aa9bd..7c3d758 100644 |
| 366 | --- a/ayllu/src/main.rs |
| 367 | +++ b/ayllu/src/main.rs |
| 368 | @@ -1,11 +1,10 @@ |
| 369 | use std::error::Error; |
| 370 | - use std::path::PathBuf; |
| 371 | use std::str::FromStr; |
| 372 | |
| 373 | - use clap::{Args, Parser, Subcommand}; |
| 374 | use config::Collection; |
| 375 | use tracing::Level; |
| 376 | |
| 377 | + use ayllu_cmd::ayllu::{Command, Subcommand}; |
| 378 | use ayllu_config::Reader; |
| 379 | |
| 380 | mod web2; |
| 381 | @@ -16,42 +15,6 @@ mod languages; |
| 382 | mod license; |
| 383 | mod readme; |
| 384 | |
| 385 | - #[derive(Parser, Debug)] |
| 386 | - #[clap(version, about, long_about = "hyper performant code forge")] |
| 387 | - struct Arguments { |
| 388 | - #[clap(short, long, value_name = "FILE")] |
| 389 | - config: Option<PathBuf>, |
| 390 | - // FIXME: Parse to Level enum directly |
| 391 | - /// logging level [ERROR,WARN,INFO,DEBUG,TRACE] |
| 392 | - #[clap(short, long)] |
| 393 | - level: Option<String>, |
| 394 | - #[clap(subcommand)] |
| 395 | - subcommand: Command, |
| 396 | - } |
| 397 | - |
| 398 | - #[derive(Args, Debug)] |
| 399 | - struct JobArguments { |
| 400 | - #[clap(short, long)] |
| 401 | - /// Maximum amount of commits to traverse |
| 402 | - depth: Option<i64>, |
| 403 | - /// Path to a git repository |
| 404 | - path: Option<PathBuf>, |
| 405 | - /// Job kind specifier. |
| 406 | - #[clap(short, long)] |
| 407 | - kind: Option<String>, |
| 408 | - } |
| 409 | - |
| 410 | - #[derive(Subcommand, Debug)] |
| 411 | - enum Command { |
| 412 | - /// Configuration options. |
| 413 | - #[clap(subcommand)] |
| 414 | - Config(ayllu_config::Command), |
| 415 | - /// Launch the main web server. If no configuration file is loaded or the configuration does |
| 416 | - /// not contain any collections Ayllu will attempt to serve a repository at the current working |
| 417 | - /// path. If none are found Ayllu will serve an empty Git interface. |
| 418 | - Serve {}, |
| 419 | - } |
| 420 | - |
| 421 | fn init_logger(level: Level) { |
| 422 | tracing_subscriber::fmt() |
| 423 | .compact() |
| 424 | @@ -64,14 +27,14 @@ fn init_logger(level: Level) { |
| 425 | |
| 426 | #[tokio::main] |
| 427 | async fn main() -> Result<(), Box<dyn Error>> { |
| 428 | - let args: Arguments = Arguments::parse(); |
| 429 | - match args.subcommand { |
| 430 | - Command::Config(subcommand) => { |
| 431 | - subcommand.execute::<config::Config>(config::EXAMPLE_CONFIG, args.config)?; |
| 432 | + let cli = ayllu_cmd::parse::<Command>(); |
| 433 | + match cli.subcommand { |
| 434 | + Subcommand::Config(subcommand) => { |
| 435 | + subcommand.execute::<config::Config>(config::EXAMPLE_CONFIG, cli.config)?; |
| 436 | Ok(()) |
| 437 | } |
| 438 | - Command::Serve {} => { |
| 439 | - let mut cfg: config::Config = Reader::load(args.config.as_deref())?; |
| 440 | + Subcommand::Serve {} => { |
| 441 | + let mut cfg: config::Config = Reader::load(cli.config.as_deref())?; |
| 442 | if cfg.collections.is_empty() { |
| 443 | if let Ok(path) = std::env::current_dir() { |
| 444 | if ayllu_git::git_dir(path.as_path()).is_ok_and(|is_git_dir| is_git_dir) { |
| 445 | @@ -85,7 +48,7 @@ async fn main() -> Result<(), Box<dyn Error>> { |
| 446 | } |
| 447 | } |
| 448 | init_logger( |
| 449 | - args.level |
| 450 | + cli.level |
| 451 | .as_ref() |
| 452 | .map(|level| Level::from_str(level)) |
| 453 | .unwrap_or(Level::from_str(&cfg.log_level))?, |
| 454 | diff --git a/crates/cmd/Cargo.toml b/crates/cmd/Cargo.toml |
| 455 | new file mode 100644 |
| 456 | index 0000000..6a94e68 |
| 457 | --- /dev/null |
| 458 | +++ b/crates/cmd/Cargo.toml |
| 459 | @@ -0,0 +1,11 @@ |
| 460 | + [package] |
| 461 | + name = "ayllu_cmd" |
| 462 | + version = "0.1.0" |
| 463 | + edition = "2024" |
| 464 | + |
| 465 | + [dependencies] |
| 466 | + ayllu_config = {path = "../config"} |
| 467 | + |
| 468 | + tracing = {workspace = true} |
| 469 | + url = {workspace = true} |
| 470 | + clap = {workspace = true} |
| 471 | diff --git a/crates/cmd/src/ayllu.rs b/crates/cmd/src/ayllu.rs |
| 472 | new file mode 100644 |
| 473 | index 0000000..4ce3eb0 |
| 474 | --- /dev/null |
| 475 | +++ b/crates/cmd/src/ayllu.rs |
| 476 | @@ -0,0 +1,42 @@ |
| 477 | + use std::path::PathBuf; |
| 478 | + |
| 479 | + use clap::Parser; |
| 480 | + |
| 481 | + const LONG_ABOUT_DESCRIPTION: &str = r#" |
| 482 | + |
| 483 | + ayllu is the main binary interface for running the Ayllu web server. |
| 484 | + |
| 485 | + When *ayllu serve* is run and no global configuration file is loaded or no |
| 486 | + collections are configured Ayllu will attempt to serve a repository at the |
| 487 | + current working path. If no repositories are found an empty git interface will |
| 488 | + be served. |
| 489 | + "#; |
| 490 | + |
| 491 | + /// Hyper Performant & Hackable Code Forge |
| 492 | + #[derive(Parser, Debug)] |
| 493 | + #[clap( |
| 494 | + version, |
| 495 | + about, |
| 496 | + name = "ayllu", |
| 497 | + long_about = LONG_ABOUT_DESCRIPTION, |
| 498 | + |
| 499 | + )] |
| 500 | + pub struct Command { |
| 501 | + #[clap(short, long, value_name = "FILE")] |
| 502 | + pub config: Option<PathBuf>, |
| 503 | + // FIXME: Parse to Level enum directly |
| 504 | + /// logging level [ERROR,WARN,INFO,DEBUG,TRACE] |
| 505 | + #[clap(short, long)] |
| 506 | + pub level: Option<String>, |
| 507 | + #[clap(subcommand)] |
| 508 | + pub subcommand: Subcommand, |
| 509 | + } |
| 510 | + |
| 511 | + #[derive(clap::Subcommand, Debug)] |
| 512 | + pub enum Subcommand { |
| 513 | + /// Configuration options. |
| 514 | + #[clap(subcommand)] |
| 515 | + Config(ayllu_config::Command), |
| 516 | + /// Launch the web interface |
| 517 | + Serve {}, |
| 518 | + } |
| 519 | diff --git a/crates/cmd/src/keys.rs b/crates/cmd/src/keys.rs |
| 520 | new file mode 100644 |
| 521 | index 0000000..95b5ca2 |
| 522 | --- /dev/null |
| 523 | +++ b/crates/cmd/src/keys.rs |
| 524 | @@ -0,0 +1,40 @@ |
| 525 | + use std::path::PathBuf; |
| 526 | + |
| 527 | + use clap::Parser; |
| 528 | + |
| 529 | + const LONG_ABOUT_DESCRIPTION: &str = r#" |
| 530 | + |
| 531 | + ayllu-keys implements a shim for authorizing users via OpenSSH's AuthorizedKeysCommand |
| 532 | + option. When using this in a shared environment external users can be authorized by |
| 533 | + their public keys defined in the global Ayllu configuration file. This command is |
| 534 | + expected to be paired with the ayllu-shell. |
| 535 | + |
| 536 | + This command handles several authentication scenarios: |
| 537 | + |
| 538 | + * A system user that is not defined in the global ayllu-config is |
| 539 | + requesting shell access via a regular ssh channel. |
| 540 | + |
| 541 | + * An Ayllu managed user is requesting shell access. |
| 542 | + |
| 543 | + * An Ayllu managed user is running git operations as the git service. |
| 544 | + "#; |
| 545 | + |
| 546 | + /// Ayllu shim for OpenSSH's AuthorizedKeysCommand |
| 547 | + #[derive(Parser, Debug)] |
| 548 | + #[clap(version, about, name = "ayllu-keys", long_about = LONG_ABOUT_DESCRIPTION)] |
| 549 | + pub struct Command { |
| 550 | + /// The username the caller is attempting to impersonate |
| 551 | + pub username: String, |
| 552 | + /// The home directory of the caller specified user |
| 553 | + pub home_dir: PathBuf, |
| 554 | + /// Type of SSH key |
| 555 | + pub ca_key_type: String, |
| 556 | + /// Public key body content. |
| 557 | + pub certificate: String, |
| 558 | + /// Path to the Ayllu configuration file |
| 559 | + #[clap(short, long, value_name = "FILE")] |
| 560 | + pub config: Option<PathBuf>, |
| 561 | + /// Path to the ayllu-shell binary |
| 562 | + #[clap(long, value_name = "ayllu-shell")] |
| 563 | + pub ayllu_shell: Option<PathBuf>, |
| 564 | + } |
| 565 | diff --git a/crates/cmd/src/lib.rs b/crates/cmd/src/lib.rs |
| 566 | new file mode 100644 |
| 567 | index 0000000..473f986 |
| 568 | --- /dev/null |
| 569 | +++ b/crates/cmd/src/lib.rs |
| 570 | @@ -0,0 +1,18 @@ |
| 571 | + pub mod ayllu; |
| 572 | + pub mod keys; |
| 573 | + pub mod quipu; |
| 574 | + pub mod shell; |
| 575 | + |
| 576 | + pub fn parse<T>() -> T |
| 577 | + where |
| 578 | + T: clap::Parser, |
| 579 | + { |
| 580 | + T::parse() |
| 581 | + } |
| 582 | + |
| 583 | + pub fn parse_from<T>(parts: &[String]) -> T |
| 584 | + where |
| 585 | + T: clap::Parser, |
| 586 | + { |
| 587 | + T::parse_from(parts) |
| 588 | + } |
| 589 | diff --git a/crates/cmd/src/quipu.rs b/crates/cmd/src/quipu.rs |
| 590 | new file mode 100644 |
| 591 | index 0000000..891b352 |
| 592 | --- /dev/null |
| 593 | +++ b/crates/cmd/src/quipu.rs |
| 594 | @@ -0,0 +1,47 @@ |
| 595 | + use std::path::PathBuf; |
| 596 | + |
| 597 | + use clap::Parser; |
| 598 | + // use clap_complete::Shell; |
| 599 | + use tracing::Level; |
| 600 | + use url::Url; |
| 601 | + |
| 602 | + const LONG_ABOUT_DESCRIPTION: &str = r#" |
| 603 | + |
| 604 | + quipu is a client interface for inspecting and interacting with remote Ayllu instances. |
| 605 | + "#; |
| 606 | + |
| 607 | + /// Quipu - Ayllu Client |
| 608 | + #[derive(Parser)] |
| 609 | + #[command(author, version, about, name = "quipu", long_about = LONG_ABOUT_DESCRIPTION)] |
| 610 | + pub struct Command { |
| 611 | + /// Path to your configuration file |
| 612 | + #[arg(short, long, value_name = "FILE")] |
| 613 | + pub config: Option<PathBuf>, |
| 614 | + |
| 615 | + /// Sets the logging level |
| 616 | + #[arg(short, long, value_name = "LEVEL")] |
| 617 | + pub level: Option<Level>, |
| 618 | + |
| 619 | + #[arg(short, long)] |
| 620 | + /// instance name from your quipu configuration |
| 621 | + pub instance: Option<String>, |
| 622 | + #[arg(short, long)] |
| 623 | + /// alternative url to contact |
| 624 | + pub url: Option<Url>, |
| 625 | + |
| 626 | + #[command(subcommand)] |
| 627 | + pub command: Subcommand, |
| 628 | + } |
| 629 | + |
| 630 | + #[derive(clap::Subcommand, Debug, PartialEq)] |
| 631 | + pub enum Subcommand { |
| 632 | + /// Generate autocomplete commands for common shells |
| 633 | + // Complete { |
| 634 | + // #[arg(long)] |
| 635 | + // shell: Shell, |
| 636 | + // }, |
| 637 | + /// Verify the remote server is functional |
| 638 | + Ping, |
| 639 | + /// Perform a webfinger query against the Ayllu instance |
| 640 | + Finger { resource: String }, |
| 641 | + } |
| 642 | diff --git a/crates/cmd/src/shell.rs b/crates/cmd/src/shell.rs |
| 643 | new file mode 100644 |
| 644 | index 0000000..1006100 |
| 645 | --- /dev/null |
| 646 | +++ b/crates/cmd/src/shell.rs |
| 647 | @@ -0,0 +1,46 @@ |
| 648 | + use std::path::PathBuf; |
| 649 | + |
| 650 | + use clap::Parser; |
| 651 | + |
| 652 | + const LONG_ABOUT_DESCRIPTION: &str = r#" |
| 653 | + |
| 654 | + ayllu-shell provides a restricted shell for performing operations against |
| 655 | + an Ayllu server. Although it may be invoked directly, typically it is |
| 656 | + expected to be paired with ayllu-keys and an OpenSSH server. |
| 657 | + "#; |
| 658 | + |
| 659 | + #[derive(clap::Subcommand, Debug)] |
| 660 | + pub enum Subcommand { |
| 661 | + /// Invokes git-receive-pack |
| 662 | + GitReceivePack { |
| 663 | + /// Path to the git repository |
| 664 | + path: PathBuf, |
| 665 | + }, |
| 666 | + /// Invokes git-upload-pack |
| 667 | + GitUploadPack { |
| 668 | + /// Path to the git repository |
| 669 | + path: PathBuf, |
| 670 | + }, |
| 671 | + } |
| 672 | + |
| 673 | + /// Interactive & restricted shell for remote Ayllu users |
| 674 | + #[derive(Parser, Debug)] |
| 675 | + #[clap( |
| 676 | + version, |
| 677 | + about, |
| 678 | + name = "ayllu-shell", |
| 679 | + long_about=LONG_ABOUT_DESCRIPTION, |
| 680 | + )] |
| 681 | + pub struct Command { |
| 682 | + /// sh compatibility |
| 683 | + #[clap(short, action)] |
| 684 | + pub c: Option<String>, |
| 685 | + /// Path to a configuration file |
| 686 | + #[clap(long)] |
| 687 | + pub config: Option<PathBuf>, |
| 688 | + /// Specify the user identity to assume |
| 689 | + #[clap(short, long)] |
| 690 | + pub username: Option<String>, |
| 691 | + #[command(subcommand)] |
| 692 | + pub command: Option<Subcommand>, |
| 693 | + } |
| 694 | diff --git a/quipu/Cargo.toml b/quipu/Cargo.toml |
| 695 | index d1c5b8a..2a29996 100644 |
| 696 | --- a/quipu/Cargo.toml |
| 697 | +++ b/quipu/Cargo.toml |
| 698 | @@ -10,6 +10,7 @@ name = "quipu" |
| 699 | ayllu_api = { path = "../crates/api" } |
| 700 | ayllu_config = { path = "../crates/config" } |
| 701 | ayllu_git = { path = "../crates/git" } |
| 702 | + ayllu_cmd = {path = "../crates/cmd"} |
| 703 | |
| 704 | clap = { workspace = true } |
| 705 | tokio = { workspace = true } |
| 706 | @@ -19,5 +20,5 @@ tracing-subscriber = { workspace = true } |
| 707 | clap_complete = { workspace = true } |
| 708 | thiserror = { workspace = true } |
| 709 | serde = { workspace = true } |
| 710 | - url = { version = "2.5.4", features = ["serde"] } |
| 711 | + url = { workspace = true } |
| 712 | webfinger-rs = { version = "0.0.18", features = ["reqwest"] } |
| 713 | diff --git a/quipu/src/main.rs b/quipu/src/main.rs |
| 714 | index e2d6b2a..1b293e2 100644 |
| 715 | --- a/quipu/src/main.rs |
| 716 | +++ b/quipu/src/main.rs |
| 717 | @@ -1,12 +1,9 @@ |
| 718 | - use std::io::stdout; |
| 719 | - use std::path::PathBuf; |
| 720 | use std::str::FromStr; |
| 721 | |
| 722 | - use clap::{arg, Command, CommandFactory, Parser, Subcommand}; |
| 723 | - use clap_complete::{generate, Generator, Shell}; |
| 724 | use tracing::Level; |
| 725 | use url::Url; |
| 726 | |
| 727 | + use ayllu_cmd::quipu::{Command, Subcommand}; |
| 728 | use ayllu_config::Reader; |
| 729 | |
| 730 | mod client; |
| 731 | @@ -14,47 +11,6 @@ mod config; |
| 732 | mod error; |
| 733 | mod output; |
| 734 | |
| 735 | - #[derive(Parser)] |
| 736 | - #[command(author, version, about, long_about = None)] |
| 737 | - #[command(name = "quipu")] |
| 738 | - #[command(about = "Ayllu Client")] |
| 739 | - struct Cli { |
| 740 | - /// Path to your configuration file |
| 741 | - #[arg(short, long, value_name = "FILE")] |
| 742 | - config: Option<PathBuf>, |
| 743 | - |
| 744 | - /// Sets the logging level |
| 745 | - #[arg(short, long, value_name = "LEVEL")] |
| 746 | - level: Option<Level>, |
| 747 | - |
| 748 | - #[arg(short, long)] |
| 749 | - /// instance name from your quipu configuration |
| 750 | - instance: Option<String>, |
| 751 | - #[arg(short, long)] |
| 752 | - /// alternative url to contact |
| 753 | - url: Option<Url>, |
| 754 | - |
| 755 | - #[command(subcommand)] |
| 756 | - command: Commands, |
| 757 | - } |
| 758 | - |
| 759 | - #[derive(Subcommand, Debug, PartialEq)] |
| 760 | - enum Commands { |
| 761 | - /// Generate autocomplete commands for common shells |
| 762 | - Complete { |
| 763 | - #[arg(long)] |
| 764 | - shell: Shell, |
| 765 | - }, |
| 766 | - /// Verify the remote server is functional |
| 767 | - Ping, |
| 768 | - /// Perform a webfinger query against the Ayllu instance |
| 769 | - Finger { resource: String }, |
| 770 | - } |
| 771 | - |
| 772 | - fn print_completions<G: Generator>(gen: G, cmd: &mut Command) { |
| 773 | - generate(gen, cmd, cmd.get_name().to_string(), &mut stdout()); |
| 774 | - } |
| 775 | - |
| 776 | fn get_instance( |
| 777 | cfg: &config::Config, |
| 778 | url: Option<Url>, |
| 779 | @@ -84,7 +40,7 @@ fn get_instance( |
| 780 | |
| 781 | #[tokio::main(flavor = "current_thread")] |
| 782 | async fn main() -> Result<(), error::QuipuError> { |
| 783 | - let cli = Cli::parse(); |
| 784 | + let cli = ayllu_cmd::parse::<Command>(); |
| 785 | let cfg: config::Config = Reader::load(cli.config.as_deref())?; |
| 786 | let log_level = Level::from_str(&cfg.log_level)?; |
| 787 | tracing_subscriber::fmt() |
| 788 | @@ -95,18 +51,18 @@ async fn main() -> Result<(), error::QuipuError> { |
| 789 | .init(); |
| 790 | tracing::info!("logger initialized"); |
| 791 | match cli.command { |
| 792 | - Commands::Complete { shell } => { |
| 793 | - let mut cmd = Cli::command(); |
| 794 | - print_completions(shell, &mut cmd); |
| 795 | - Ok(()) |
| 796 | - } |
| 797 | - Commands::Finger { resource } => { |
| 798 | + // Commands::Complete { shell } => { |
| 799 | + // let mut cmd = Cli::command(); |
| 800 | + // print_completions(shell, &mut cmd); |
| 801 | + // Ok(()) |
| 802 | + // } |
| 803 | + Subcommand::Finger { resource } => { |
| 804 | let instance = get_instance(&cfg, cli.url, cli.instance)?; |
| 805 | let client = client::Quipu::new(instance.url); |
| 806 | client.finger(&resource).await?; |
| 807 | Ok(()) |
| 808 | } |
| 809 | - Commands::Ping => { |
| 810 | + Subcommand::Ping => { |
| 811 | let instance = get_instance(&cfg, cli.url, cli.instance)?; |
| 812 | let client = client::Quipu::new(instance.url); |
| 813 | let status = client.ping().await?; |
| 814 | diff --git a/scripts/task.sh b/scripts/task.sh |
| 815 | new file mode 100755 |
| 816 | index 0000000..92f935b |
| 817 | --- /dev/null |
| 818 | +++ b/scripts/task.sh |
| 819 | @@ -0,0 +1,3 @@ |
| 820 | + #!/bin/sh |
| 821 | + |
| 822 | + cargo run --package xtask -- "$1" |
| 823 | diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml |
| 824 | new file mode 100644 |
| 825 | index 0000000..4f3a76a |
| 826 | --- /dev/null |
| 827 | +++ b/xtask/Cargo.toml |
| 828 | @@ -0,0 +1,9 @@ |
| 829 | + [package] |
| 830 | + name = "xtask" |
| 831 | + version = "0.1.0" |
| 832 | + edition = "2024" |
| 833 | + |
| 834 | + [dependencies] |
| 835 | + ayllu_cmd = { path = "../crates/cmd" } |
| 836 | + clap = {workspace = true} |
| 837 | + clap_mangen = "0.2.29" |
| 838 | diff --git a/xtask/src/main.rs b/xtask/src/main.rs |
| 839 | new file mode 100644 |
| 840 | index 0000000..2fe548e |
| 841 | --- /dev/null |
| 842 | +++ b/xtask/src/main.rs |
| 843 | @@ -0,0 +1,51 @@ |
| 844 | + use std::{ |
| 845 | + env, |
| 846 | + fs::OpenOptions, |
| 847 | + io::BufWriter, |
| 848 | + path::{Path, PathBuf}, |
| 849 | + }; |
| 850 | + |
| 851 | + const ARTIFACT_DIR: &str = "target/dist"; |
| 852 | + |
| 853 | + fn target_dir() -> PathBuf { |
| 854 | + Path::new(&env!("CARGO_MANIFEST_DIR")) |
| 855 | + .ancestors() |
| 856 | + .nth(1) |
| 857 | + .unwrap() |
| 858 | + .join(ARTIFACT_DIR) |
| 859 | + .to_path_buf() |
| 860 | + } |
| 861 | + |
| 862 | + fn man_gen<T>(target: &Path) -> std::io::Result<()> |
| 863 | + where |
| 864 | + T: clap::Parser, |
| 865 | + { |
| 866 | + let as_cmd = T::command(); |
| 867 | + let man = clap_mangen::Man::new(as_cmd); |
| 868 | + if let Some(parent) = target.parent() { |
| 869 | + std::fs::create_dir_all(parent)?; |
| 870 | + }; |
| 871 | + let fp = OpenOptions::new() |
| 872 | + .create(true) |
| 873 | + .write(true) |
| 874 | + .truncate(true) |
| 875 | + .open(target)?; |
| 876 | + let mut buf = BufWriter::new(&fp); |
| 877 | + man.render(&mut buf)?; |
| 878 | + fp.sync_data()?; |
| 879 | + |
| 880 | + Ok(()) |
| 881 | + } |
| 882 | + |
| 883 | + fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 884 | + match env::args().nth(1).as_deref() { |
| 885 | + Some("man_gen") => { |
| 886 | + man_gen::<ayllu_cmd::ayllu::Command>(&target_dir().join("ayllu.1"))?; |
| 887 | + man_gen::<ayllu_cmd::quipu::Command>(&target_dir().join("quipu.1"))?; |
| 888 | + man_gen::<ayllu_cmd::shell::Command>(&target_dir().join("ayllu-shell.1"))?; |
| 889 | + man_gen::<ayllu_cmd::keys::Command>(&target_dir().join("ayllu-keys.1"))?; |
| 890 | + Ok(()) |
| 891 | + } |
| 892 | + _ => unimplemented!(), |
| 893 | + } |
| 894 | + } |