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,
})
}