bucketwarden-cli 0.1.0

BucketWarden CLI command parsing, demos, and listener runtime.
Documentation
use crate::operator_cli::s3_parser::parse_s3_serve_command;
use crate::operator_cli::types::{GlobalOptions, OperatorCommand};
use crate::operator_tooling::{parse_auth_command, parse_policy_command};
use std::str;

pub fn parse_operator_command(args: &[String]) -> Result<OperatorCommand, String> {
    let (global_config_path, global_options, command_args) = parse_global_options(args)?;
    let command = parse_operator_command_inner(&command_args, global_config_path.as_deref())?;
    if global_options.quiet || global_options.verbose > 0 {
        Ok(OperatorCommand::Global {
            options: global_options,
            command: Box::new(command),
        })
    } else {
        Ok(command)
    }
}

fn parse_global_options(
    args: &[String],
) -> Result<(Option<String>, GlobalOptions, Vec<String>), String> {
    let mut config_path = None;
    let mut global_options = GlobalOptions {
        quiet: false,
        verbose: 0,
    };
    let mut command_args = vec![args
        .first()
        .cloned()
        .unwrap_or_else(|| "bucketwarden".to_string())];

    let mut index = 1;
    while index < args.len() {
        match args[index].as_str() {
            "--config" => {
                index += 1;
                config_path = Some(
                    args.get(index)
                        .cloned()
                        .ok_or_else(|| "missing value for global `--config`".to_string())?,
                );
            }
            "--quiet" => {
                global_options.quiet = true;
            }
            "--verbose" => {
                global_options.verbose = global_options.verbose.saturating_add(1);
            }
            "--version" => {
                command_args.push("--version".to_string());
            }
            option if option.starts_with("--") && command_args.len() == 1 => {
                command_args.extend_from_slice(&args[index..]);
                break;
            }
            _ => {
                command_args.extend_from_slice(&args[index..]);
                break;
            }
        }
        index += 1;
    }

    Ok((config_path, global_options, command_args))
}

fn parse_operator_command_inner(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    let Some(command) = args.get(1).map(String::as_str) else {
        return Ok(OperatorCommand::DefaultStatus);
    };

    match command {
        "-h" | "--help" | "help" => Ok(OperatorCommand::Help),
        "--version" | "version" => Ok(OperatorCommand::Version),
        "--health" | "health" => Ok(OperatorCommand::Health {
            config_path: optional_config_path(&args[2..], global_config_path)?,
        }),
        "readiness" => Ok(OperatorCommand::Readiness {
            config_path: optional_config_path(&args[2..], global_config_path)?,
        }),
        "diagnostics" => Ok(OperatorCommand::Diagnostics {
            config_path: optional_config_path(&args[2..], global_config_path)?,
        }),
        "metrics" => Ok(OperatorCommand::Metrics {
            config_path: optional_config_path(&args[2..], global_config_path)?,
        }),
        "ops" => parse_ops_command(&args[2..], global_config_path),
        "ui" => parse_ui_command(&args[2..]),
        "config" => parse_config_command(&args[2..], global_config_path),
        "audit" => parse_audit_command(&args[2..], global_config_path),
        "replication" => parse_replication_command(&args[2..], global_config_path),
        "s3" => parse_s3_command(&args[2..], global_config_path),
        "policy" => parse_policy_command(&args[2..]),
        "auth" => parse_auth_command(&args[2..]),
        "conformance" => Ok(OperatorCommand::Conformance {
            target: args.get(2).cloned(),
        }),
        other if is_demo_command(other) => Ok(OperatorCommand::Demo(other.to_string())),
        other => Err(format!(
            "unknown command `{other}`; run `bucketwarden --help` for usage"
        )),
    }
}

fn parse_ui_command(args: &[String]) -> Result<OperatorCommand, String> {
    match args {
        [family, command] if family == "browser" && command == "manifest" => {
            Ok(OperatorCommand::UiBrowserManifest)
        }
        [family, command] if family == "browser" && command == "html" => {
            Ok(OperatorCommand::UiBrowserHtml)
        }
        [family, command] if family == "browser" && command == "css" => {
            Ok(OperatorCommand::UiBrowserCss)
        }
        [family, command] if family == "browser" && command == "js" => {
            Ok(OperatorCommand::UiBrowserJs)
        }
        [family, command, ..] => Err(format!(
            "unknown ui command `{family} {command}`; expected `ui browser manifest|html|css|js`"
        )),
        _ => Err("missing ui command; expected `ui browser manifest|html|css|js`".to_string()),
    }
}

fn parse_ops_command(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    match args {
        [family, kind, rest @ ..] if family == "report" && kind == "health" => {
            let options = parse_ops_report_options(rest, global_config_path)?;
            Ok(OperatorCommand::OpsReportHealth {
                config_path: options.config_path,
                bucket: options.bucket,
            })
        }
        [family, kind, rest @ ..] if family == "report" && kind == "config" => {
            let options = parse_ops_report_options(rest, global_config_path)?;
            Ok(OperatorCommand::OpsReportConfig {
                config_path: options.config_path,
                bucket: options.bucket,
            })
        }
        [family, kind, rest @ ..] if family == "report" && kind == "admin-surfaces" => {
            let options = parse_ops_report_options(rest, global_config_path)?;
            Ok(OperatorCommand::OpsReportAdminSurfaces {
                config_path: options.config_path,
                bucket: options.bucket,
            })
        }
        [family, kind, rest @ ..] if family == "report" && kind == "incident" => {
            let options = parse_ops_report_options(rest, global_config_path)?;
            let incident_type = options
                .incident_type
                .ok_or_else(|| "missing required `--type <incident>`".to_string())?;
            Ok(OperatorCommand::OpsReportIncident {
                config_path: options.config_path,
                bucket: options.bucket,
                incident_type,
            })
        }
        [family, kind, rest @ ..] if family == "report" && kind == "evidence-export" => {
            let options = parse_ops_report_options(rest, global_config_path)?;
            Ok(OperatorCommand::OpsReportEvidenceExport {
                config_path: options.config_path,
                bucket: options.bucket,
            })
        }
        [family, command, ..] => Err(format!(
            "unknown ops command `{family} {command}`; expected `ops report health|config|admin-surfaces|incident|evidence-export`"
        )),
        _ => Err("missing ops command; expected `ops report health|config|admin-surfaces|incident|evidence-export`".to_string()),
    }
}

fn parse_config_command(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    match args.first().map(String::as_str) {
        Some("validate") => {
            let path = required_config_path(&args[1..], global_config_path)?;
            Ok(OperatorCommand::ConfigValidate { config_path: path })
        }
        Some(other) => Err(format!(
            "unknown config command `{other}`; expected `config validate --config <path>`"
        )),
        None => {
            Err("missing config command; expected `config validate --config <path>`".to_string())
        }
    }
}

fn parse_audit_command(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    match args.first().map(String::as_str) {
        Some("export") => Ok(OperatorCommand::AuditExport {
            config_path: optional_config_path(&args[1..], global_config_path)?,
        }),
        Some(other) => Err(format!(
            "unknown audit command `{other}`; expected `audit export [--config <path>]`"
        )),
        None => Err("missing audit command; expected `audit export [--config <path>]`".to_string()),
    }
}

fn parse_replication_command(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    match args.first().map(String::as_str) {
        Some("status") => Ok(OperatorCommand::ReplicationStatus {
            config_path: optional_config_path(&args[1..], global_config_path)?,
        }),
        Some(other) => Err(format!(
            "unknown replication command `{other}`; expected `replication status [--config <path>]`"
        )),
        None => Err(
            "missing replication command; expected `replication status [--config <path>]`"
                .to_string(),
        ),
    }
}

fn parse_s3_command(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OperatorCommand, String> {
    match args.first().map(String::as_str) {
        Some("serve") => parse_s3_serve_command(&args[1..], global_config_path),
        Some(other) => Err(format!(
            "unknown s3 command `{other}`; expected `s3 serve [--bind ADDR] [--console-bind ADDR] [--config <path>] [--principal P] [--access-key KEY] [--secret-key SECRET] [--storage filesystem|in-memory] [--data-dir PATH] [--in-memory]`"
        )),
        None => Err(
            "missing s3 command; expected `s3 serve [--bind ADDR] [--console-bind ADDR] [--config <path>] [--principal P] [--access-key KEY] [--secret-key SECRET] [--storage filesystem|in-memory] [--data-dir PATH] [--in-memory]`"
                .to_string(),
        ),
    }
}

fn optional_config_path(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<Option<String>, String> {
    if args.is_empty() {
        return Ok(global_config_path.map(str::to_string));
    }
    required_config_path(args, global_config_path).map(Some)
}

fn required_config_path(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<String, String> {
    match args {
        [flag, path] if flag == "--config" => Ok(path.clone()),
        [] => global_config_path
            .map(str::to_string)
            .ok_or_else(|| "missing required `--config <path>`".to_string()),
        _ => Err("expected `--config <path>`".to_string()),
    }
}

fn is_demo_command(command: &str) -> bool {
    matches!(
        command,
        "protocol-surface-demo"
            | "client-compatibility-demo"
            | "api-operations-demo"
            | "bucket-control-demo"
            | "object-data-demo"
            | "demo"
            | "sigv4-demo"
            | "presign-runtime-demo"
            | "error-corpus-demo"
            | "header-contract-demo"
            | "addressing-demo"
            | "region-routing-demo"
            | "s3-http-demo"
            | "object-operations-demo"
            | "bucket-region-demo"
            | "bucket-operations-demo"
            | "snapshot-demo"
            | "multipart-demo"
            | "multipart-copy-demo"
            | "multipart-abort-demo"
            | "range-conditional-demo"
            | "list-v2-demo"
            | "versioning-demo"
            | "integrity-demo"
            | "cors-demo"
            | "cors-tagging-demo"
            | "tag-semantics-demo"
            | "website-demo"
            | "bucket-policy-tagging-demo"
            | "object-lock-http-demo"
            | "encryption-demo"
            | "lifecycle-demo"
            | "notification-demo"
            | "replication-demo"
            | "acl-ownership-demo"
    )
}

struct OpsReportOptions {
    config_path: Option<String>,
    bucket: Option<String>,
    incident_type: Option<String>,
}

fn parse_ops_report_options(
    args: &[String],
    global_config_path: Option<&str>,
) -> Result<OpsReportOptions, String> {
    let mut config_path = global_config_path.map(str::to_string);
    let mut bucket = None;
    let mut incident_type = None;
    let mut index = 0;

    while index < args.len() {
        match args[index].as_str() {
            "--config" => {
                index += 1;
                config_path = Some(
                    args.get(index)
                        .cloned()
                        .ok_or_else(|| "missing value for `--config`".to_string())?,
                );
            }
            "--bucket" => {
                index += 1;
                bucket = Some(
                    args.get(index)
                        .cloned()
                        .ok_or_else(|| "missing value for `--bucket`".to_string())?,
                );
            }
            "--type" => {
                index += 1;
                incident_type = Some(
                    args.get(index)
                        .cloned()
                        .ok_or_else(|| "missing value for `--type`".to_string())?,
                );
            }
            other => return Err(format!("unknown ops report flag `{other}`")),
        }
        index += 1;
    }

    Ok(OpsReportOptions {
        config_path,
        bucket,
        incident_type,
    })
}