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:
47
src/cli.rs
47
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<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.",
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user