use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use fakecloud_core::auth::IamMode;
use fakecloud_persistence::{PersistenceConfig, StorageMode};
#[derive(Clone, Copy, Debug, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum IamModeArg {
Off,
Soft,
Strict,
}
impl From<IamModeArg> for IamMode {
fn from(value: IamModeArg) -> Self {
match value {
IamModeArg::Off => IamMode::Off,
IamModeArg::Soft => IamMode::Soft,
IamModeArg::Strict => IamMode::Strict,
}
}
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[clap(rename_all = "lowercase")]
pub(crate) enum StorageModeArg {
Memory,
Persistent,
}
impl From<StorageModeArg> for StorageMode {
fn from(value: StorageModeArg) -> Self {
match value {
StorageModeArg::Memory => StorageMode::Memory,
StorageModeArg::Persistent => StorageMode::Persistent,
}
}
}
const DEFAULT_S3_CACHE_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Parser)]
#[command(name = "fakecloud")]
#[command(about = "FakeCloud — local AWS cloud emulator")]
#[command(version)]
pub(crate) struct Cli {
#[arg(long, default_value = "0.0.0.0:4566", env = "FAKECLOUD_ADDR")]
pub addr: String,
#[arg(long, default_value = "us-east-1", env = "FAKECLOUD_REGION")]
pub region: String,
#[arg(long, default_value = "123456789012", env = "FAKECLOUD_ACCOUNT_ID")]
pub account_id: String,
#[arg(long, default_value = "info", env = "FAKECLOUD_LOG")]
pub log_level: String,
#[arg(
long,
value_enum,
default_value_t = StorageModeArg::Memory,
env = "FAKECLOUD_STORAGE_MODE",
)]
pub storage_mode: StorageModeArg,
#[arg(long, env = "FAKECLOUD_DATA_PATH")]
pub data_path: Option<PathBuf>,
#[arg(long, default_value_t = DEFAULT_S3_CACHE_BYTES, env = "FAKECLOUD_S3_CACHE_SIZE")]
pub s3_cache_size: u64,
#[arg(long, default_value_t = false, env = "FAKECLOUD_VERIFY_SIGV4")]
pub verify_sigv4: bool,
#[arg(
long = "iam",
value_enum,
default_value_t = IamModeArg::Off,
env = "FAKECLOUD_IAM",
)]
pub iam_mode: IamModeArg,
}
impl Cli {
pub fn endpoint_url(&self) -> String {
let addr = &self.addr;
let port = addr.rsplit(':').next().unwrap_or("4566");
let host = addr.rsplit_once(':').map(|(h, _)| h).unwrap_or("0.0.0.0");
let host = if host == "0.0.0.0" || host == "[::]" {
"localhost"
} else {
host
};
format!("http://{host}:{port}")
}
pub fn iam_mode(&self) -> IamMode {
self.iam_mode.into()
}
pub fn persistence_config(&self) -> Result<PersistenceConfig, String> {
let mode: StorageMode = self.storage_mode.into();
let config = PersistenceConfig {
mode,
data_path: self.data_path.clone(),
s3_cache_bytes: self.s3_cache_size,
};
config.validate()?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn defaults_leave_security_features_off() {
let cli = Cli::try_parse_from(["fakecloud"]).unwrap();
assert!(!cli.verify_sigv4);
assert_eq!(cli.iam_mode(), IamMode::Off);
}
#[test]
fn verify_sigv4_flag_parses() {
let cli = Cli::try_parse_from(["fakecloud", "--verify-sigv4"]).unwrap();
assert!(cli.verify_sigv4);
}
#[test]
fn iam_flag_parses_all_variants() {
let cli = Cli::try_parse_from(["fakecloud", "--iam", "off"]).unwrap();
assert_eq!(cli.iam_mode(), IamMode::Off);
let cli = Cli::try_parse_from(["fakecloud", "--iam", "soft"]).unwrap();
assert_eq!(cli.iam_mode(), IamMode::Soft);
let cli = Cli::try_parse_from(["fakecloud", "--iam", "strict"]).unwrap();
assert_eq!(cli.iam_mode(), IamMode::Strict);
}
#[test]
fn iam_flag_rejects_garbage() {
assert!(Cli::try_parse_from(["fakecloud", "--iam", "allow"]).is_err());
}
}