fabrica

hanna/fabrica

6154 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The command-line interface for fabrica.
6//!
7//! Every subcommand operates directly on the store and filesystem; there is no
8//! IPC with a running `serve`. [`run`] is the single entry point invoked by the
9//! `fabrica` binary. The command tree is built out over the implementation
10//! phases; today it covers only `config check` and `config show`.
11
12mod admin_cmd;
13mod config_cmd;
14mod context;
15mod group_cmd;
16mod hook_cmd;
17mod key_cmd;
18mod prompt;
19mod repo_cmd;
20#[cfg(test)]
21mod testutil;
22mod token_cmd;
23mod user_cmd;
24
25use std::path::PathBuf;
26use std::process::ExitCode;
27
28use clap::{Parser, Subcommand};
29
30/// Top-level argument parser for the `fabrica` binary.
31#[derive(Debug, Parser)]
32#[command(name = "fabrica", version, about = "A self-hosted git server.")]
33struct Cli {
34 /// Path to a configuration file, merged last after the standard locations.
35 #[arg(long, global = true, value_name = "PATH")]
36 config: Option<PathBuf>,
37
38 /// Override the configured log level (`trace|debug|info|warn|error`).
39 #[arg(long, global = true, value_name = "LEVEL")]
40 log_level: Option<String>,
41
42 /// Emit machine-readable JSON instead of human-formatted output.
43 #[arg(long, global = true)]
44 json: bool,
45
46 /// The subcommand to run.
47 #[command(subcommand)]
48 command: Command,
49}
50
51/// The fabrica subcommand tree.
52#[derive(Debug, Subcommand)]
53enum Command {
54 /// Inspect and validate configuration.
55 Config {
56 /// The configuration action to perform.
57 #[command(subcommand)]
58 command: config_cmd::ConfigCommand,
59 },
60 /// Manage user accounts.
61 User {
62 /// The user action to perform.
63 #[command(subcommand)]
64 command: user_cmd::UserCommand,
65 },
66 /// Manage a user's SSH and GPG keys.
67 Key {
68 /// The key action to perform.
69 #[command(subcommand)]
70 command: key_cmd::KeyCommand,
71 },
72 /// Manage repositories.
73 Repo {
74 /// The repository action to perform.
75 #[command(subcommand)]
76 command: repo_cmd::RepoCommand,
77 },
78 /// Manage the group hierarchy.
79 Group {
80 /// The group action to perform.
81 #[command(subcommand)]
82 command: group_cmd::GroupCommand,
83 },
84 /// Manage a user's API tokens.
85 Token {
86 /// The token action to perform.
87 #[command(subcommand)]
88 command: token_cmd::TokenCommand,
89 },
90 /// Run the HTTPS and SSH servers in the foreground.
91 Serve {
92 /// Double-fork into the background and write a pid file. (Not yet
93 /// implemented — systemd/Docker manage the foreground process.)
94 #[arg(long)]
95 detach: bool,
96 /// Skip applying pending migrations on startup.
97 #[arg(long)]
98 no_migrate: bool,
99 },
100 /// Apply pending database migrations.
101 Migrate,
102 /// Check that the environment fabrica needs is present and healthy.
103 Doctor {
104 /// Only set the exit code; suppress the per-check report. For health checks.
105 #[arg(long)]
106 quiet: bool,
107 },
108 /// Internal git-hook entry points (invoked by installed hooks).
109 #[command(hide = true)]
110 Hook {
111 /// The hook to run.
112 #[command(subcommand)]
113 command: hook_cmd::HookCommand,
114 },
115}
116
117/// The parsed request to run `fabrica serve`, handed to the serve callback the
118/// root binary supplies. This keeps `cli` free of any dependency on `web`/`ssh`
119/// (no crate may depend on `web`); the binary wires the servers together.
120#[derive(Debug, Clone)]
121pub struct ServeRequest {
122 /// The `--config` path.
123 pub config_path: Option<PathBuf>,
124 /// Double-fork into the background.
125 pub detach: bool,
126 /// Skip startup migrations.
127 pub no_migrate: bool,
128}
129
130/// Resolve the instance signing secret (generating and persisting it on first
131/// use), for the binary's serve wiring.
132///
133/// # Errors
134///
135/// Returns an error if the secret file cannot be read or written.
136pub fn resolve_secret(config: &config::Config) -> anyhow::Result<String> {
137 context::ensure_secret(config)
138}
139
140/// Parse arguments and dispatch to the requested subcommand.
141///
142/// `serve` handles the `fabrica serve` command; the root binary supplies it so
143/// the servers (which live in `web`/`ssh`) are wired without `cli` depending on
144/// them. Returns the process exit code. Argument-parse failures are handled by
145/// clap; any other error is printed to stderr and mapped to
146/// [`ExitCode::FAILURE`].
147pub fn run<F>(serve: F) -> ExitCode
148where
149 F: FnOnce(ServeRequest) -> anyhow::Result<ExitCode>,
150{
151 let cli = Cli::parse();
152 match dispatch(&cli, serve) {
153 Ok(code) => code,
154 Err(err) => {
155 eprintln!("error: {err:#}");
156 ExitCode::FAILURE
157 }
158 }
159}
160
161/// Route a parsed [`Cli`] to its handler.
162fn dispatch<F>(cli: &Cli, serve: F) -> anyhow::Result<ExitCode>
163where
164 F: FnOnce(ServeRequest) -> anyhow::Result<ExitCode>,
165{
166 match &cli.command {
167 Command::Serve { detach, no_migrate } => serve(ServeRequest {
168 config_path: cli.config.clone(),
169 detach: *detach,
170 no_migrate: *no_migrate,
171 }),
172 Command::Config { command } => config_cmd::run(command, cli.config.as_deref(), cli.json),
173 Command::User { command } => user_cmd::run(command, cli.config.as_deref(), cli.json),
174 Command::Key { command } => key_cmd::run(command, cli.config.as_deref(), cli.json),
175 Command::Repo { command } => repo_cmd::run(command, cli.config.as_deref(), cli.json),
176 Command::Group { command } => group_cmd::run(command, cli.config.as_deref(), cli.json),
177 Command::Token { command } => token_cmd::run(command, cli.config.as_deref(), cli.json),
178 Command::Migrate => admin_cmd::migrate(cli.config.as_deref()),
179 Command::Doctor { quiet } => admin_cmd::doctor(cli.config.as_deref(), *quiet),
180 Command::Hook { command } => Ok(hook_cmd::run(command)),
181 }
182}