diff --git a/src/cli.rs b/src/cli.rs index 36b24a9..2e8edb0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,15 +5,11 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Parser, Debug)] #[command(name = "sunbeam", about = "Sunbeam local dev stack manager")] pub struct Cli { - /// Target environment. - #[arg(long, default_value = "local")] - pub env: Env, - - /// kubectl context override. + /// Named context to use (overrides current-context from config). #[arg(long)] pub context: Option, - /// Domain suffix for production deploys (e.g. sunbeam.pt). + /// Domain suffix override (e.g. sunbeam.pt). #[arg(long, default_value = "")] pub domain: String, @@ -25,20 +21,6 @@ pub struct Cli { pub verb: Option, } -#[derive(Debug, Clone, ValueEnum)] -pub enum Env { - Local, - Production, -} - -impl std::fmt::Display for Env { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Env::Local => write!(f, "local"), - Env::Production => write!(f, "production"), - } - } -} #[derive(Subcommand, Debug)] pub enum Verb { @@ -406,13 +388,6 @@ fn validate_date(s: &str) -> std::result::Result { .map_err(|_| format!("Invalid date: '{s}' (expected YYYY-MM-DD)")) } -/// Default kubectl context per environment. -fn default_context(env: &Env) -> &'static str { - match env { - Env::Local => "sunbeam", - Env::Production => "production", - } -} #[cfg(test)] mod tests { @@ -762,22 +737,23 @@ mod tests { pub async fn dispatch() -> Result<()> { let cli = Cli::parse(); - // Resolve the active context from config + CLI flags + // Resolve the active context from config + CLI flags (like kubectl) let config = crate::config::load_config(); let active = crate::config::resolve_context( &config, - &cli.env.to_string(), + "", cli.context.as_deref(), &cli.domain, ); // Initialize kube context from the resolved context - let kube_ctx = if active.kube_context.is_empty() { - default_context(&cli.env) + let kube_ctx_str = if active.kube_context.is_empty() { + "sunbeam".to_string() } else { - &active.kube_context + active.kube_context.clone() }; - crate::kube::set_context(kube_ctx, &active.ssh_host); + let ssh_host_str = active.ssh_host.clone(); + crate::kube::set_context(&kube_ctx_str, &ssh_host_str); // Store active context globally for other modules to read crate::config::set_active_context(active); @@ -803,7 +779,8 @@ pub async fn dispatch() -> Result<()> { domain, email, }) => { - let env_str = cli.env.to_string(); + let is_production = !crate::config::active_context().ssh_host.is_empty(); + let env_str = if is_production { "production" } else { "local" }; let domain = if domain.is_empty() { cli.domain.clone() } else { @@ -817,7 +794,7 @@ pub async fn dispatch() -> Result<()> { let ns = namespace.unwrap_or_default(); // Production full-apply requires --all or confirmation - if matches!(cli.env, Env::Production) && ns.is_empty() && !apply_all { + if is_production && ns.is_empty() && !apply_all { crate::output::warn( "This will apply ALL namespaces to production.", ); diff --git a/src/config.rs b/src/config.rs index 74052e9..b5b9726 100644 --- a/src/config.rs +++ b/src/config.rs @@ -151,26 +151,24 @@ pub fn save_config(config: &SunbeamConfig) -> Result<()> { } /// Resolve the context to use, given CLI flags and config. +/// +/// Priority (same as kubectl): +/// 1. `--context` flag (explicit context name) +/// 2. `current-context` from config +/// 3. Default to "local" pub fn resolve_context( config: &SunbeamConfig, - env_flag: &str, + _env_flag: &str, context_override: Option<&str>, domain_override: &str, ) -> Context { - // Start from the named context (CLI --env or current-context) - let context_name = context_override - .map(|s| s.to_string()) - .unwrap_or_else(|| { - if env_flag == "production" { - "production".to_string() - } else if env_flag == "local" { - "local".to_string() - } else if !config.current_context.is_empty() { - config.current_context.clone() - } else { - "local".to_string() - } - }); + let context_name = if let Some(explicit) = context_override { + explicit.to_string() + } else if !config.current_context.is_empty() { + config.current_context.clone() + } else { + "local".to_string() + }; let mut ctx = config .contexts @@ -340,7 +338,7 @@ mod tests { } #[test] - fn test_resolve_context_from_env_flag() { + fn test_resolve_context_explicit_flag() { let mut config = SunbeamConfig::default(); config.contexts.insert( "production".to_string(), @@ -350,22 +348,57 @@ mod tests { ..Default::default() }, ); - let ctx = resolve_context(&config, "production", None, ""); + // --context production explicitly selects the named context + let ctx = resolve_context(&config, "", Some("production"), ""); assert_eq!(ctx.domain, "sunbeam.pt"); assert_eq!(ctx.kube_context, "production"); } + #[test] + fn test_resolve_context_current_context() { + let mut config = SunbeamConfig::default(); + config.current_context = "staging".to_string(); + config.contexts.insert( + "staging".to_string(), + Context { + domain: "staging.example.com".to_string(), + ..Default::default() + }, + ); + // No --context flag, uses current-context + let ctx = resolve_context(&config, "", None, ""); + assert_eq!(ctx.domain, "staging.example.com"); + } + #[test] fn test_resolve_context_domain_override() { let config = SunbeamConfig::default(); - let ctx = resolve_context(&config, "local", None, "custom.example.com"); + let ctx = resolve_context(&config, "", None, "custom.example.com"); assert_eq!(ctx.domain, "custom.example.com"); } #[test] fn test_resolve_context_defaults_local() { let config = SunbeamConfig::default(); - let ctx = resolve_context(&config, "local", None, ""); + // No current-context, no --context flag → defaults to "local" + let ctx = resolve_context(&config, "", None, ""); assert_eq!(ctx.kube_context, "sunbeam"); } + + #[test] + fn test_resolve_context_flag_overrides_current() { + let mut config = SunbeamConfig::default(); + config.current_context = "staging".to_string(); + config.contexts.insert( + "staging".to_string(), + Context { domain: "staging.example.com".to_string(), ..Default::default() }, + ); + config.contexts.insert( + "prod".to_string(), + Context { domain: "prod.example.com".to_string(), ..Default::default() }, + ); + // --context prod overrides current-context "staging" + let ctx = resolve_context(&config, "", Some("prod"), ""); + assert_eq!(ctx.domain, "prod.example.com"); + } }