refactor: remove --env flag, use --context like kubectl

Context resolution: --context flag > current-context from config > "local".
No more production/local distinction in the CLI flags — the context
determines everything (domain, kube-context, ssh-host, infra-dir).

Remove Env enum entirely. Production detection is now "context has ssh-host".
This commit is contained in:
2026-03-20 15:23:54 +00:00
parent 88b02acdd1
commit ded0ab442e
2 changed files with 64 additions and 54 deletions

View File

@@ -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<String>,
/// 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<Verb>,
}
#[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<String, String> {
.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.",
);

View File

@@ -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");
}
}