2026-03-20 13:15:45 +00:00
|
|
|
use crate::error::{Result, ResultExt};
|
feat: Rust rewrite scaffolding with embedded kustomize+helm
Phase 0 of Python-to-Rust CLI rewrite:
- Cargo.toml with all dependencies (kube-rs, reqwest, russh, rcgen, lettre, etc.)
- build.rs: downloads kustomize v5.8.1 + helm v4.1.0 at compile time, embeds as bytes, sets SUNBEAM_COMMIT from git
- src/main.rs: tokio main with anyhow error formatting
- src/cli.rs: full clap derive struct tree matching all Python argparse subcommands
- src/config.rs: SunbeamConfig serde struct, load/save ~/.sunbeam.json
- src/output.rs: step/ok/warn/table with exact Python format strings
- src/tools.rs: embedded kustomize+helm extraction to cache dir
- src/kube.rs: parse_target, domain_replace, context management
- src/manifests.rs: filter_by_namespace with full test coverage
- Stub modules for all remaining features (cluster, secrets, images, services, checks, gitea, users, update)
23 tests pass, cargo check clean.
2026-03-20 12:24:21 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
/// Sunbeam configuration stored at ~/.sunbeam.json.
|
|
|
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
|
|
|
pub struct SunbeamConfig {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub production_host: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub infra_directory: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub acme_email: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn config_path() -> PathBuf {
|
|
|
|
|
dirs::home_dir()
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
|
|
|
.join(".sunbeam.json")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load configuration from ~/.sunbeam.json, return default if not found.
|
|
|
|
|
pub fn load_config() -> SunbeamConfig {
|
|
|
|
|
let path = config_path();
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
return SunbeamConfig::default();
|
|
|
|
|
}
|
|
|
|
|
match std::fs::read_to_string(&path) {
|
|
|
|
|
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
|
|
|
|
|
crate::output::warn(&format!(
|
|
|
|
|
"Failed to parse config from {}: {e}",
|
|
|
|
|
path.display()
|
|
|
|
|
));
|
|
|
|
|
SunbeamConfig::default()
|
|
|
|
|
}),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::output::warn(&format!(
|
|
|
|
|
"Failed to read config from {}: {e}",
|
|
|
|
|
path.display()
|
|
|
|
|
));
|
|
|
|
|
SunbeamConfig::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save configuration to ~/.sunbeam.json.
|
|
|
|
|
pub fn save_config(config: &SunbeamConfig) -> Result<()> {
|
|
|
|
|
let path = config_path();
|
|
|
|
|
if let Some(parent) = path.parent() {
|
2026-03-20 13:15:45 +00:00
|
|
|
std::fs::create_dir_all(parent).with_ctx(|| {
|
feat: Rust rewrite scaffolding with embedded kustomize+helm
Phase 0 of Python-to-Rust CLI rewrite:
- Cargo.toml with all dependencies (kube-rs, reqwest, russh, rcgen, lettre, etc.)
- build.rs: downloads kustomize v5.8.1 + helm v4.1.0 at compile time, embeds as bytes, sets SUNBEAM_COMMIT from git
- src/main.rs: tokio main with anyhow error formatting
- src/cli.rs: full clap derive struct tree matching all Python argparse subcommands
- src/config.rs: SunbeamConfig serde struct, load/save ~/.sunbeam.json
- src/output.rs: step/ok/warn/table with exact Python format strings
- src/tools.rs: embedded kustomize+helm extraction to cache dir
- src/kube.rs: parse_target, domain_replace, context management
- src/manifests.rs: filter_by_namespace with full test coverage
- Stub modules for all remaining features (cluster, secrets, images, services, checks, gitea, users, update)
23 tests pass, cargo check clean.
2026-03-20 12:24:21 +00:00
|
|
|
format!(
|
|
|
|
|
"Failed to create config directory: {}",
|
|
|
|
|
parent.display()
|
|
|
|
|
)
|
|
|
|
|
})?;
|
|
|
|
|
}
|
|
|
|
|
let content = serde_json::to_string_pretty(config)?;
|
|
|
|
|
std::fs::write(&path, content)
|
2026-03-20 13:15:45 +00:00
|
|
|
.with_ctx(|| format!("Failed to save config to {}", path.display()))?;
|
feat: Rust rewrite scaffolding with embedded kustomize+helm
Phase 0 of Python-to-Rust CLI rewrite:
- Cargo.toml with all dependencies (kube-rs, reqwest, russh, rcgen, lettre, etc.)
- build.rs: downloads kustomize v5.8.1 + helm v4.1.0 at compile time, embeds as bytes, sets SUNBEAM_COMMIT from git
- src/main.rs: tokio main with anyhow error formatting
- src/cli.rs: full clap derive struct tree matching all Python argparse subcommands
- src/config.rs: SunbeamConfig serde struct, load/save ~/.sunbeam.json
- src/output.rs: step/ok/warn/table with exact Python format strings
- src/tools.rs: embedded kustomize+helm extraction to cache dir
- src/kube.rs: parse_target, domain_replace, context management
- src/manifests.rs: filter_by_namespace with full test coverage
- Stub modules for all remaining features (cluster, secrets, images, services, checks, gitea, users, update)
23 tests pass, cargo check clean.
2026-03-20 12:24:21 +00:00
|
|
|
crate::output::ok(&format!("Configuration saved to {}", path.display()));
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get production host from config or SUNBEAM_SSH_HOST environment variable.
|
|
|
|
|
pub fn get_production_host() -> String {
|
|
|
|
|
let config = load_config();
|
|
|
|
|
if !config.production_host.is_empty() {
|
|
|
|
|
return config.production_host;
|
|
|
|
|
}
|
|
|
|
|
std::env::var("SUNBEAM_SSH_HOST").unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get infrastructure directory from config.
|
|
|
|
|
pub fn get_infra_directory() -> String {
|
|
|
|
|
load_config().infra_directory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Infrastructure manifests directory as a Path.
|
|
|
|
|
///
|
|
|
|
|
/// Prefers the configured infra_directory; falls back to a path relative to
|
|
|
|
|
/// the current executable (works when running from the development checkout).
|
|
|
|
|
pub fn get_infra_dir() -> PathBuf {
|
|
|
|
|
let configured = load_config().infra_directory;
|
|
|
|
|
if !configured.is_empty() {
|
|
|
|
|
return PathBuf::from(configured);
|
|
|
|
|
}
|
|
|
|
|
// Dev fallback: walk up from the executable to find monorepo root
|
|
|
|
|
std::env::current_exe()
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|p| p.canonicalize().ok())
|
|
|
|
|
.and_then(|p| {
|
|
|
|
|
let mut dir = p.as_path();
|
|
|
|
|
for _ in 0..10 {
|
|
|
|
|
dir = dir.parent()?;
|
|
|
|
|
if dir.join("infrastructure").is_dir() {
|
|
|
|
|
return Some(dir.join("infrastructure"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("infrastructure"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Monorepo root directory (parent of the infrastructure directory).
|
|
|
|
|
pub fn get_repo_root() -> PathBuf {
|
|
|
|
|
get_infra_dir()
|
|
|
|
|
.parent()
|
|
|
|
|
.map(|p| p.to_path_buf())
|
|
|
|
|
.unwrap_or_else(|| PathBuf::from("."))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Clear configuration file.
|
|
|
|
|
pub fn clear_config() -> Result<()> {
|
|
|
|
|
let path = config_path();
|
|
|
|
|
if path.exists() {
|
|
|
|
|
std::fs::remove_file(&path)
|
2026-03-20 13:15:45 +00:00
|
|
|
.with_ctx(|| format!("Failed to remove {}", path.display()))?;
|
feat: Rust rewrite scaffolding with embedded kustomize+helm
Phase 0 of Python-to-Rust CLI rewrite:
- Cargo.toml with all dependencies (kube-rs, reqwest, russh, rcgen, lettre, etc.)
- build.rs: downloads kustomize v5.8.1 + helm v4.1.0 at compile time, embeds as bytes, sets SUNBEAM_COMMIT from git
- src/main.rs: tokio main with anyhow error formatting
- src/cli.rs: full clap derive struct tree matching all Python argparse subcommands
- src/config.rs: SunbeamConfig serde struct, load/save ~/.sunbeam.json
- src/output.rs: step/ok/warn/table with exact Python format strings
- src/tools.rs: embedded kustomize+helm extraction to cache dir
- src/kube.rs: parse_target, domain_replace, context management
- src/manifests.rs: filter_by_namespace with full test coverage
- Stub modules for all remaining features (cluster, secrets, images, services, checks, gitea, users, update)
23 tests pass, cargo check clean.
2026-03-20 12:24:21 +00:00
|
|
|
crate::output::ok(&format!(
|
|
|
|
|
"Configuration cleared from {}",
|
|
|
|
|
path.display()
|
|
|
|
|
));
|
|
|
|
|
} else {
|
|
|
|
|
crate::output::warn("No configuration file found to clear");
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_default_config() {
|
|
|
|
|
let config = SunbeamConfig::default();
|
|
|
|
|
assert!(config.production_host.is_empty());
|
|
|
|
|
assert!(config.infra_directory.is_empty());
|
|
|
|
|
assert!(config.acme_email.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_config_roundtrip() {
|
|
|
|
|
let config = SunbeamConfig {
|
|
|
|
|
production_host: "user@example.com".to_string(),
|
|
|
|
|
infra_directory: "/path/to/infra".to_string(),
|
|
|
|
|
acme_email: "ops@example.com".to_string(),
|
|
|
|
|
};
|
|
|
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
|
|
|
let loaded: SunbeamConfig = serde_json::from_str(&json).unwrap();
|
|
|
|
|
assert_eq!(loaded.production_host, "user@example.com");
|
|
|
|
|
assert_eq!(loaded.infra_directory, "/path/to/infra");
|
|
|
|
|
assert_eq!(loaded.acme_email, "ops@example.com");
|
|
|
|
|
}
|
|
|
|
|
}
|