flatten subcommands and add better doc comments

This commit is contained in:
Oystein Kristoffer Tveit 2024-07-09 19:54:27 +02:00
parent b0bffc45ee
commit 4dedde5edb
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
5 changed files with 107 additions and 94 deletions

View File

@ -32,10 +32,10 @@ You should now be able to connect to the mariadb instance, after building the pr
cargo run -- --config-file ./config.toml <args> cargo run -- --config-file ./config.toml <args>
# example usage # example usage
cargo run -- --config-file ./config.toml db create "${USER}_testdb" cargo run -- --config-file ./config.toml create-db "${USER}_testdb"
cargo run -- --config-file ./config.toml user create "${USER}_testuser" cargo run -- --config-file ./config.toml create-user "${USER}_testuser"
cargo run -- --config-file ./config.toml db edit-perm -p "${USER}_testdb:${USER}_testuser:A" cargo run -- --config-file ./config.toml edit-db-perm -p "${USER}_testdb:${USER}_testuser:A"
cargo run -- --config-file ./config.toml db show-perm cargo run -- --config-file ./config.toml show-db-perm
``` ```
To stop and remove the container, run the following command: To stop and remove the container, run the following command:

View File

@ -15,40 +15,38 @@ use crate::core::{
}, },
}; };
#[derive(Parser)]
pub struct DatabaseArgs {
#[clap(subcommand)]
subcmd: DatabaseCommand,
}
// TODO: Support batch creation/dropping,showing of databases, // TODO: Support batch creation/dropping,showing of databases,
// using a comma-separated list of database names. // using a comma-separated list of database names.
#[derive(Parser)] #[derive(Parser)]
enum DatabaseCommand { // #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
/// Create the DATABASE(S). pub enum DatabaseCommand {
#[command(alias = "add", alias = "c")] /// Create one or more databases
Create(DatabaseCreateArgs),
/// Delete the DATABASE(S).
#[command(alias = "remove", alias = "delete", alias = "rm", alias = "d")]
Drop(DatabaseDropArgs),
/// List the DATABASE(S) you own.
#[command()] #[command()]
List(DatabaseListArgs), CreateDb(DatabaseCreateArgs),
/// Give information about the DATABASE(S), or if none are given, all the ones you own. /// Delete one or more databases
#[command()]
DropDb(DatabaseDropArgs),
/// List all databases you have access to
#[command()]
ListDb(DatabaseListArgs),
/// List user permissions for one or more databases
/// ///
/// In particular, this will show the permissions for the database(s) owned by the current user. /// If no database names are provided, it will show permissions for all databases you have access to.
#[command(alias = "s")] #[command()]
ShowPerm(DatabaseShowPermArgs), ShowDbPerm(DatabaseShowPermArgs),
/// Change permissions for the DATABASE(S). Run `edit-perm --help` for more information. /// Change user permissions for one or more databases. See `edit-db-perm --help` for details.
/// ///
/// This command has two modes of operation: /// This command has two modes of operation:
///
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the permissions using a text editor. /// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the permissions using a text editor.
/// ///
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
///
/// Follow the instructions inside the editor for more information. /// Follow the instructions inside the editor for more information.
/// ///
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write permissions using arguments. /// 2. Non-interactive mode: If the `-p` flag is specified, the user can write permissions using arguments.
@ -57,7 +55,7 @@ enum DatabaseCommand {
/// where the privileges are a string of characters, each representing a single permissions. /// where the privileges are a string of characters, each representing a single permissions.
/// The character `A` is an exception, because it represents all permissions. /// The character `A` is an exception, because it represents all permissions.
/// ///
/// The permission to character mapping is as follows: /// The character to permission mapping is declared as follows:
/// ///
/// - `s` - SELECT /// - `s` - SELECT
/// - `i` - INSERT /// - `i` - INSERT
@ -72,33 +70,49 @@ enum DatabaseCommand {
/// - `r` - REFERENCES /// - `r` - REFERENCES
/// - `A` - ALL PRIVILEGES /// - `A` - ALL PRIVILEGES
/// ///
#[command(display_name = "edit-perm", alias = "e", verbatim_doc_comment)] /// If you provide a database name, you can omit it from the permission arguments.
EditPerm(DatabaseEditPermArgs), ///
/// Example usage of non-interactive mode:
///
/// Set permissions `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
///
/// mysqladm edit-db-perm -p my_db:my_user:siu
///
/// Set all permissions for user `my_other_user` on database `my_other_db`:
///
/// mysqladm edit-db-perm -p my_other_db:my_other_user:A
///
/// Set miscellaneous permissions for multiple users on database `my_db`:
///
/// mysqladm edit-db-perm my_db -p my_user:siu my_other_user:ct
///
#[command(verbatim_doc_comment)]
EditDbPerm(DatabaseEditPermArgs),
} }
#[derive(Parser)] #[derive(Parser)]
struct DatabaseCreateArgs { pub struct DatabaseCreateArgs {
/// The name of the database(s) to create. /// The name of the database(s) to create.
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
name: Vec<String>, name: Vec<String>,
} }
#[derive(Parser)] #[derive(Parser)]
struct DatabaseDropArgs { pub struct DatabaseDropArgs {
/// The name of the database(s) to drop. /// The name of the database(s) to drop.
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
name: Vec<String>, name: Vec<String>,
} }
#[derive(Parser)] #[derive(Parser)]
struct DatabaseListArgs { pub struct DatabaseListArgs {
/// Whether to output the information in JSON format. /// Whether to output the information in JSON format.
#[arg(short, long)] #[arg(short, long)]
json: bool, json: bool,
} }
#[derive(Parser)] #[derive(Parser)]
struct DatabaseShowPermArgs { pub struct DatabaseShowPermArgs {
/// The name of the database(s) to show. /// The name of the database(s) to show.
#[arg(num_args = 0..)] #[arg(num_args = 0..)]
name: Vec<String>, name: Vec<String>,
@ -109,7 +123,7 @@ struct DatabaseShowPermArgs {
} }
#[derive(Parser)] #[derive(Parser)]
struct DatabaseEditPermArgs { pub struct DatabaseEditPermArgs {
/// The name of the database to edit permissions for. /// The name of the database to edit permissions for.
name: Option<String>, name: Option<String>,
@ -124,18 +138,21 @@ struct DatabaseEditPermArgs {
#[arg(short, long)] #[arg(short, long)]
editor: Option<String>, editor: Option<String>,
/// Disable confirmation before saving changes. /// Disable interactive confirmation before saving changes.
#[arg(short, long)] #[arg(short, long)]
yes: bool, yes: bool,
} }
pub async fn handle_command(args: DatabaseArgs, mut conn: MySqlConnection) -> anyhow::Result<()> { pub async fn handle_command(
let result = match args.subcmd { command: DatabaseCommand,
DatabaseCommand::Create(args) => create_databases(args, &mut conn).await, mut conn: MySqlConnection,
DatabaseCommand::Drop(args) => drop_databases(args, &mut conn).await, ) -> anyhow::Result<()> {
DatabaseCommand::List(args) => list_databases(args, &mut conn).await, let result = match command {
DatabaseCommand::ShowPerm(args) => show_databases(args, &mut conn).await, DatabaseCommand::CreateDb(args) => create_databases(args, &mut conn).await,
DatabaseCommand::EditPerm(args) => edit_permissions(args, &mut conn).await, DatabaseCommand::DropDb(args) => drop_databases(args, &mut conn).await,
DatabaseCommand::ListDb(args) => list_databases(args, &mut conn).await,
DatabaseCommand::ShowDbPerm(args) => show_databases(args, &mut conn).await,
DatabaseCommand::EditDbPerm(args) => edit_permissions(args, &mut conn).await,
}; };
conn.close().await?; conn.close().await?;

View File

@ -13,38 +13,40 @@ pub struct UserArgs {
} }
#[derive(Parser)] #[derive(Parser)]
enum UserCommand { pub enum UserCommand {
/// Create the USER(s). /// Create one or more users
#[command(alias = "add", alias = "c")] #[command()]
Create(UserCreateArgs), CreateUser(UserCreateArgs),
/// Delete the USER(s). /// Delete one or more users
#[command(alias = "remove", alias = "delete", alias = "rm", alias = "d")] #[command()]
Drop(UserDeleteArgs), DropUser(UserDeleteArgs),
/// Change the MySQL password for the USER. /// Change the MySQL password for a user
#[command(alias = "password", alias = "p")] #[command()]
Passwd(UserPasswdArgs), PasswdUser(UserPasswdArgs),
/// Give information about the USER(s), or if no USER is given, all USERs you have access to. /// Give information about one or more users
#[command(alias = "list", alias = "ls", alias = "s")] ///
Show(UserShowArgs), /// If no username is provided, all users you have access will be shown.
#[command()]
ShowUser(UserShowArgs),
} }
#[derive(Parser)] #[derive(Parser)]
struct UserCreateArgs { pub struct UserCreateArgs {
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
username: Vec<String>, username: Vec<String>,
} }
#[derive(Parser)] #[derive(Parser)]
struct UserDeleteArgs { pub struct UserDeleteArgs {
#[arg(num_args = 1..)] #[arg(num_args = 1..)]
username: Vec<String>, username: Vec<String>,
} }
#[derive(Parser)] #[derive(Parser)]
struct UserPasswdArgs { pub struct UserPasswdArgs {
username: String, username: String,
#[clap(short, long)] #[clap(short, long)]
@ -52,17 +54,17 @@ struct UserPasswdArgs {
} }
#[derive(Parser)] #[derive(Parser)]
struct UserShowArgs { pub struct UserShowArgs {
#[arg(num_args = 0..)] #[arg(num_args = 0..)]
username: Vec<String>, username: Vec<String>,
} }
pub async fn handle_command(args: UserArgs, mut conn: MySqlConnection) -> anyhow::Result<()> { pub async fn handle_command(command: UserCommand, mut conn: MySqlConnection) -> anyhow::Result<()> {
let result = match args.subcmd { let result = match command {
UserCommand::Create(args) => create_users(args, &mut conn).await, UserCommand::CreateUser(args) => create_users(args, &mut conn).await,
UserCommand::Drop(args) => drop_users(args, &mut conn).await, UserCommand::DropUser(args) => drop_users(args, &mut conn).await,
UserCommand::Passwd(args) => change_password_for_user(args, &mut conn).await, UserCommand::PasswdUser(args) => change_password_for_user(args, &mut conn).await,
UserCommand::Show(args) => show_users(args, &mut conn).await, UserCommand::ShowUser(args) => show_users(args, &mut conn).await,
}; };
conn.close().await?; conn.close().await?;

View File

@ -20,60 +20,57 @@ pub struct MysqlConfig {
} }
#[derive(Parser)] #[derive(Parser)]
pub struct ConfigOverrideArgs { pub struct GlobalConfigArgs {
/// Path to the configuration file.
#[arg( #[arg(
short,
long, long,
value_name = "PATH", value_name = "PATH",
global = true, global = true,
help_heading = Some("Configuration overrides"),
hide_short_help = true, hide_short_help = true,
alias = "config", default_value = "/etc/mysqladm/config.toml",
alias = "conf",
)] )]
config_file: Option<String>, config_file: String,
/// Hostname of the MySQL server.
#[arg( #[arg(
long, long,
value_name = "HOST", value_name = "HOST",
global = true, global = true,
help_heading = Some("Configuration overrides"),
hide_short_help = true, hide_short_help = true,
)] )]
mysql_host: Option<String>, mysql_host: Option<String>,
/// Port of the MySQL server.
#[arg( #[arg(
long, long,
value_name = "PORT", value_name = "PORT",
global = true, global = true,
help_heading = Some("Configuration overrides"),
hide_short_help = true, hide_short_help = true,
)] )]
mysql_port: Option<u16>, mysql_port: Option<u16>,
/// Username to use for the MySQL connection.
#[arg( #[arg(
long, long,
value_name = "USER", value_name = "USER",
global = true, global = true,
help_heading = Some("Configuration overrides"),
hide_short_help = true, hide_short_help = true,
)] )]
mysql_user: Option<String>, mysql_user: Option<String>,
/// Path to a file containing the MySQL password.
#[arg( #[arg(
long, long,
value_name = "PATH", value_name = "PATH",
global = true, global = true,
help_heading = Some("Configuration overrides"),
hide_short_help = true, hide_short_help = true,
)] )]
mysql_password_file: Option<String>, mysql_password_file: Option<String>,
} }
pub fn get_config(args: ConfigOverrideArgs) -> anyhow::Result<Config> { pub fn get_config(args: GlobalConfigArgs) -> anyhow::Result<Config> {
let config_path = args let config_path = PathBuf::from(args.config_file);
.config_file
.unwrap_or("/etc/mysqladm/config.toml".to_string());
let config_path = PathBuf::from(config_path);
let config: Config = fs::read_to_string(&config_path) let config: Config = fs::read_to_string(&config_path)
.context(format!( .context(format!(
@ -108,7 +105,7 @@ pub fn get_config(args: ConfigOverrideArgs) -> anyhow::Result<Config> {
}) })
} }
/// TODO: Add timeout. /// TODO: Make timeout configurable
pub async fn mysql_connection_from_config(config: Config) -> anyhow::Result<MySqlConnection> { pub async fn mysql_connection_from_config(config: Config) -> anyhow::Result<MySqlConnection> {
match tokio::time::timeout( match tokio::time::timeout(
Duration::from_secs(2), Duration::from_secs(2),

View File

@ -15,28 +15,27 @@ struct Args {
command: Command, command: Command,
#[command(flatten)] #[command(flatten)]
config_overrides: core::config::ConfigOverrideArgs, config_overrides: core::config::GlobalConfigArgs,
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
#[arg(short, long, alias = "tui", global = true)] #[arg(short, long, alias = "tui", global = true)]
interactive: bool, interactive: bool,
} }
/// Database administration tool designed for non-admin users to manage their own MySQL databases and users. /// Database administration tool for non-admin users to manage their own MySQL databases and users.
/// Use `--help` for advanced usage.
/// ///
/// This tool allows you to manage users and databases in MySQL that are prefixed with your username. /// This tool allows you to manage users and databases in MySQL.
///
/// You are only allowed to manage databases and users that are prefixed with
/// either your username, or a group that you are a member of.
#[derive(Parser)] #[derive(Parser)]
#[command(version, about, disable_help_subcommand = true)] #[command(version, about, disable_help_subcommand = true)]
enum Command { enum Command {
/// Create, drop or show/edit permissions for DATABASE(s), #[command(flatten)]
#[command(alias = "database")] Db(cli::database_command::DatabaseCommand),
Db(cli::database_command::DatabaseArgs),
// Database(cli::database_command::DatabaseArgs), #[command(flatten)]
/// Create, drop, change password for, or show your USER(s), User(cli::user_command::UserCommand),
#[command(name = "user")]
User(cli::user_command::UserArgs),
} }
#[tokio::main] #[tokio::main]
@ -47,9 +46,7 @@ async fn main() -> anyhow::Result<()> {
let connection = core::config::mysql_connection_from_config(config).await?; let connection = core::config::mysql_connection_from_config(config).await?;
match args.command { match args.command {
Command::Db(database_args) => { Command::Db(command) => cli::database_command::handle_command(command, connection).await,
cli::database_command::handle_command(database_args, connection).await
}
Command::User(user_args) => cli::user_command::handle_command(user_args, connection).await, Command::User(user_args) => cli::user_command::handle_command(user_args, connection).await,
} }
} }