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 anyhow::{Context, Result};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
static KUSTOMIZE_BIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/kustomize"));
|
|
|
|
|
static HELM_BIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/helm"));
|
|
|
|
|
|
|
|
|
|
fn cache_dir() -> PathBuf {
|
|
|
|
|
dirs::data_dir()
|
|
|
|
|
.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")))
|
|
|
|
|
.join("sunbeam")
|
|
|
|
|
.join("bin")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extract an embedded binary to the cache directory if not already present.
|
|
|
|
|
fn extract_embedded(data: &[u8], name: &str) -> Result<PathBuf> {
|
|
|
|
|
let dir = cache_dir();
|
|
|
|
|
std::fs::create_dir_all(&dir)
|
|
|
|
|
.with_context(|| format!("Failed to create cache dir: {}", dir.display()))?;
|
|
|
|
|
|
|
|
|
|
let dest = dir.join(name);
|
|
|
|
|
|
|
|
|
|
// Skip if already extracted and same size
|
|
|
|
|
if dest.exists() {
|
|
|
|
|
if let Ok(meta) = std::fs::metadata(&dest) {
|
|
|
|
|
if meta.len() == data.len() as u64 {
|
|
|
|
|
return Ok(dest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::fs::write(&dest, data)
|
|
|
|
|
.with_context(|| format!("Failed to write {}", dest.display()))?;
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(dest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ensure kustomize is extracted and return its path.
|
|
|
|
|
pub fn ensure_kustomize() -> Result<PathBuf> {
|
|
|
|
|
extract_embedded(KUSTOMIZE_BIN, "kustomize")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ensure helm is extracted and return its path.
|
|
|
|
|
pub fn ensure_helm() -> Result<PathBuf> {
|
|
|
|
|
extract_embedded(HELM_BIN, "helm")
|
|
|
|
|
}
|
feat: Phase 2 feature modules + comprehensive test suite (142 tests)
services.rs:
- Pod status with unicode icons, grouped by namespace
- VSO sync status (VaultStaticSecret/VaultDynamicSecret via kube-rs DynamicObject)
- Log streaming via kube-rs log_stream + futures::AsyncBufReadExt
- Pod get in YAML/JSON format
- Rollout restart with namespace/service filtering
checks.rs:
- 11 health check functions (gitea, postgres, valkey, openbao, seaweedfs, kratos, hydra, people, livekit)
- AWS4-HMAC-SHA256 S3 auth header generation using sha2 + hmac
- Concurrent execution via tokio JoinSet
- mkcert root CA trust for local TLS
secrets.rs:
- Stub with cmd_seed/cmd_verify (requires live cluster for full impl)
users.rs:
- All 10 Kratos identity operations via reqwest + kubectl port-forward
- Welcome email via lettre SMTP through port-forwarded postfix
- Employee onboarding with auto-assigned ID, HR metadata
- Offboarding with Kratos + Hydra session revocation
gitea.rs:
- Bootstrap without Lima VM: admin password, org creation, OIDC auth source
- Gitea API via kubectl exec curl
images.rs:
- BuildEnv detection, buildctl build + push via port-forward
- Per-service builders for all 17 build targets
- Deploy rollout, node image pull, uv Dockerfile patching
- Mirror scaffolding (containerd operations marked TODO)
cluster.rs:
- Pure K8s cmd_up: cert-manager, linkerd, rcgen TLS certs, core service wait
- No Lima VM operations
manifests.rs:
- Full cmd_apply: kustomize build, two-pass convergence, ConfigMap restart detection
- Pre-apply cleanup, webhook wait, mkcert CA, tuwunel OAuth2 redirect patch
Test coverage: 142 tests across 14 modules (44 in checks, 27 in cli, 13 in images, 12 in tools, 12 in services, 11 in users, 10 in manifests, 9 in kube, 9 in cluster, 7 in update, 6 in gitea, 4 in openbao, 3 in output, 2 in config).
2026-03-20 12:45:07 +00:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn kustomize_bin_is_non_empty() {
|
|
|
|
|
assert!(
|
|
|
|
|
KUSTOMIZE_BIN.len() > 0,
|
|
|
|
|
"Embedded kustomize binary should not be empty"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn helm_bin_is_non_empty() {
|
|
|
|
|
assert!(
|
|
|
|
|
HELM_BIN.len() > 0,
|
|
|
|
|
"Embedded helm binary should not be empty"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn kustomize_bin_has_reasonable_size() {
|
|
|
|
|
// kustomize binary should be at least 1 MB
|
|
|
|
|
assert!(
|
|
|
|
|
KUSTOMIZE_BIN.len() > 1_000_000,
|
|
|
|
|
"Embedded kustomize binary seems too small: {} bytes",
|
|
|
|
|
KUSTOMIZE_BIN.len()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn helm_bin_has_reasonable_size() {
|
|
|
|
|
// helm binary should be at least 1 MB
|
|
|
|
|
assert!(
|
|
|
|
|
HELM_BIN.len() > 1_000_000,
|
|
|
|
|
"Embedded helm binary seems too small: {} bytes",
|
|
|
|
|
HELM_BIN.len()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cache_dir_ends_with_sunbeam_bin() {
|
|
|
|
|
let dir = cache_dir();
|
|
|
|
|
assert!(
|
|
|
|
|
dir.ends_with("sunbeam/bin"),
|
|
|
|
|
"cache_dir() should end with sunbeam/bin, got: {}",
|
|
|
|
|
dir.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn cache_dir_is_absolute() {
|
|
|
|
|
let dir = cache_dir();
|
|
|
|
|
assert!(
|
|
|
|
|
dir.is_absolute(),
|
|
|
|
|
"cache_dir() should return an absolute path, got: {}",
|
|
|
|
|
dir.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ensure_kustomize_returns_valid_path() {
|
|
|
|
|
let path = ensure_kustomize().expect("ensure_kustomize should succeed");
|
|
|
|
|
assert!(
|
|
|
|
|
path.ends_with("kustomize"),
|
|
|
|
|
"ensure_kustomize path should end with 'kustomize', got: {}",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
assert!(path.exists(), "kustomize binary should exist at: {}", path.display());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ensure_helm_returns_valid_path() {
|
|
|
|
|
let path = ensure_helm().expect("ensure_helm should succeed");
|
|
|
|
|
assert!(
|
|
|
|
|
path.ends_with("helm"),
|
|
|
|
|
"ensure_helm path should end with 'helm', got: {}",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
assert!(path.exists(), "helm binary should exist at: {}", path.display());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ensure_kustomize_is_idempotent() {
|
|
|
|
|
let path1 = ensure_kustomize().expect("first call should succeed");
|
|
|
|
|
let path2 = ensure_kustomize().expect("second call should succeed");
|
|
|
|
|
assert_eq!(path1, path2, "ensure_kustomize should return the same path on repeated calls");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ensure_helm_is_idempotent() {
|
|
|
|
|
let path1 = ensure_helm().expect("first call should succeed");
|
|
|
|
|
let path2 = ensure_helm().expect("second call should succeed");
|
|
|
|
|
assert_eq!(path1, path2, "ensure_helm should return the same path on repeated calls");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extracted_kustomize_is_executable() {
|
|
|
|
|
let path = ensure_kustomize().expect("ensure_kustomize should succeed");
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
let perms = std::fs::metadata(&path)
|
|
|
|
|
.expect("should read metadata")
|
|
|
|
|
.permissions();
|
|
|
|
|
assert!(
|
|
|
|
|
perms.mode() & 0o111 != 0,
|
|
|
|
|
"kustomize binary should be executable"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn extracted_helm_is_executable() {
|
|
|
|
|
let path = ensure_helm().expect("ensure_helm should succeed");
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
let perms = std::fs::metadata(&path)
|
|
|
|
|
.expect("should read metadata")
|
|
|
|
|
.permissions();
|
|
|
|
|
assert!(
|
|
|
|
|
perms.mode() & 0o111 != 0,
|
|
|
|
|
"helm binary should be executable"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|