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)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "sunbeam", about = "Sunbeam local dev stack manager")]
|
#[command(name = "sunbeam", about = "Sunbeam local dev stack manager")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Target environment.
|
/// Named context to use (overrides current-context from config).
|
||||||
#[arg(long, default_value = "local")]
|
|
||||||
pub env: Env,
|
|
||||||
|
|
||||||
/// kubectl context override.
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub context: Option<String>,
|
pub context: Option<String>,
|
||||||
|
|
||||||
/// Domain suffix for production deploys (e.g. sunbeam.pt).
|
/// Domain suffix override (e.g. sunbeam.pt).
|
||||||
#[arg(long, default_value = "")]
|
#[arg(long, default_value = "")]
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
|
|
||||||
@@ -25,20 +21,6 @@ pub struct Cli {
|
|||||||
pub verb: Option<Verb>,
|
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)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Verb {
|
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)"))
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -762,22 +737,23 @@ mod tests {
|
|||||||
pub async fn dispatch() -> Result<()> {
|
pub async fn dispatch() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
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 config = crate::config::load_config();
|
||||||
let active = crate::config::resolve_context(
|
let active = crate::config::resolve_context(
|
||||||
&config,
|
&config,
|
||||||
&cli.env.to_string(),
|
"",
|
||||||
cli.context.as_deref(),
|
cli.context.as_deref(),
|
||||||
&cli.domain,
|
&cli.domain,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize kube context from the resolved context
|
// Initialize kube context from the resolved context
|
||||||
let kube_ctx = if active.kube_context.is_empty() {
|
let kube_ctx_str = if active.kube_context.is_empty() {
|
||||||
default_context(&cli.env)
|
"sunbeam".to_string()
|
||||||
} else {
|
} 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
|
// Store active context globally for other modules to read
|
||||||
crate::config::set_active_context(active);
|
crate::config::set_active_context(active);
|
||||||
@@ -803,7 +779,8 @@ pub async fn dispatch() -> Result<()> {
|
|||||||
domain,
|
domain,
|
||||||
email,
|
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() {
|
let domain = if domain.is_empty() {
|
||||||
cli.domain.clone()
|
cli.domain.clone()
|
||||||
} else {
|
} else {
|
||||||
@@ -817,7 +794,7 @@ pub async fn dispatch() -> Result<()> {
|
|||||||
let ns = namespace.unwrap_or_default();
|
let ns = namespace.unwrap_or_default();
|
||||||
|
|
||||||
// Production full-apply requires --all or confirmation
|
// 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(
|
crate::output::warn(
|
||||||
"This will apply ALL namespaces to production.",
|
"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.
|
/// 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(
|
pub fn resolve_context(
|
||||||
config: &SunbeamConfig,
|
config: &SunbeamConfig,
|
||||||
env_flag: &str,
|
_env_flag: &str,
|
||||||
context_override: Option<&str>,
|
context_override: Option<&str>,
|
||||||
domain_override: &str,
|
domain_override: &str,
|
||||||
) -> Context {
|
) -> Context {
|
||||||
// Start from the named context (CLI --env or current-context)
|
let context_name = if let Some(explicit) = context_override {
|
||||||
let context_name = context_override
|
explicit.to_string()
|
||||||
.map(|s| s.to_string())
|
} else if !config.current_context.is_empty() {
|
||||||
.unwrap_or_else(|| {
|
config.current_context.clone()
|
||||||
if env_flag == "production" {
|
} else {
|
||||||
"production".to_string()
|
"local".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 mut ctx = config
|
let mut ctx = config
|
||||||
.contexts
|
.contexts
|
||||||
@@ -340,7 +338,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_context_from_env_flag() {
|
fn test_resolve_context_explicit_flag() {
|
||||||
let mut config = SunbeamConfig::default();
|
let mut config = SunbeamConfig::default();
|
||||||
config.contexts.insert(
|
config.contexts.insert(
|
||||||
"production".to_string(),
|
"production".to_string(),
|
||||||
@@ -350,22 +348,57 @@ mod tests {
|
|||||||
..Default::default()
|
..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.domain, "sunbeam.pt");
|
||||||
assert_eq!(ctx.kube_context, "production");
|
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]
|
#[test]
|
||||||
fn test_resolve_context_domain_override() {
|
fn test_resolve_context_domain_override() {
|
||||||
let config = SunbeamConfig::default();
|
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");
|
assert_eq!(ctx.domain, "custom.example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_context_defaults_local() {
|
fn test_resolve_context_defaults_local() {
|
||||||
let config = SunbeamConfig::default();
|
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");
|
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