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 flate2::read::GzDecoder;
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::io::Read;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::process::Command;
|
|
|
|
|
use tar::Archive;
|
|
|
|
|
|
|
|
|
|
const KUSTOMIZE_VERSION: &str = "v5.8.1";
|
|
|
|
|
const HELM_VERSION: &str = "v4.1.0";
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
|
|
|
|
let target = env::var("TARGET").unwrap_or_default();
|
|
|
|
|
let (os, arch) = parse_target(&target);
|
|
|
|
|
|
|
|
|
|
download_and_embed("kustomize", KUSTOMIZE_VERSION, &os, &arch, &out_dir);
|
|
|
|
|
download_and_embed("helm", HELM_VERSION, &os, &arch, &out_dir);
|
|
|
|
|
|
|
|
|
|
// Set version info from git
|
|
|
|
|
let commit = git_commit_sha();
|
|
|
|
|
println!("cargo:rustc-env=SUNBEAM_COMMIT={commit}");
|
|
|
|
|
|
feat: Phase 1 foundations — kube-rs client, OpenBao HTTP client, self-update
kube.rs:
- KubeClient with lazy init from kubeconfig + context selection
- SSH tunnel via subprocess (port 2222, forward 16443->6443)
- Server-side apply for multi-document YAML via kube-rs discovery
- Secret get/create, namespace ensure, exec in pod, rollout restart
- Domain discovery from gitea-inline-config secret
- kustomize_build with embedded binary, domain/email/registry substitution
- kubectl and bao CLI passthrough commands
openbao.rs:
- Lightweight Vault/OpenBao HTTP API client using reqwest
- System ops: seal-status, init, unseal
- KV v2: get, put, patch, delete with proper response parsing
- Auth: enable method, write policy, write roles
- Database secrets engine: config, static roles
- Replaces all kubectl exec bao shell commands from Python version
update.rs:
- Self-update from latest mainline commit via Gitea API
- CI artifact download with SHA256 checksum verification
- Atomic self-replace (temp file + rename)
- Background update check with hourly cache (~/.local/share/sunbeam/)
- Enhanced version command with target triple and build date
build.rs:
- Added SUNBEAM_TARGET and SUNBEAM_BUILD_DATE env vars
35 tests pass.
2026-03-20 12:37:02 +00:00
|
|
|
// Build target triple and build date
|
|
|
|
|
println!("cargo:rustc-env=SUNBEAM_TARGET={target}");
|
|
|
|
|
let date = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
|
|
|
|
println!("cargo:rustc-env=SUNBEAM_BUILD_DATE={date}");
|
|
|
|
|
|
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
|
|
|
// Rebuild if git HEAD changes
|
|
|
|
|
println!("cargo:rerun-if-changed=.git/HEAD");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_target(target: &str) -> (String, String) {
|
|
|
|
|
let os = if target.contains("darwin") {
|
|
|
|
|
"darwin"
|
|
|
|
|
} else if target.contains("linux") {
|
|
|
|
|
"linux"
|
|
|
|
|
} else if cfg!(target_os = "macos") {
|
|
|
|
|
"darwin"
|
|
|
|
|
} else {
|
|
|
|
|
"linux"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let arch = if target.contains("aarch64") || target.contains("arm64") {
|
|
|
|
|
"arm64"
|
|
|
|
|
} else if target.contains("x86_64") || target.contains("amd64") {
|
|
|
|
|
"amd64"
|
|
|
|
|
} else if cfg!(target_arch = "aarch64") {
|
|
|
|
|
"arm64"
|
|
|
|
|
} else {
|
|
|
|
|
"amd64"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(os.to_string(), arch.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn download_and_embed(tool: &str, version: &str, os: &str, arch: &str, out_dir: &PathBuf) {
|
|
|
|
|
let dest = out_dir.join(tool);
|
|
|
|
|
if dest.exists() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let url = match tool {
|
|
|
|
|
"kustomize" => format!(
|
|
|
|
|
"https://github.com/kubernetes-sigs/kustomize/releases/download/\
|
|
|
|
|
kustomize%2F{version}/kustomize_{version}_{os}_{arch}.tar.gz"
|
|
|
|
|
),
|
|
|
|
|
"helm" => format!(
|
|
|
|
|
"https://get.helm.sh/helm-{version}-{os}-{arch}.tar.gz"
|
|
|
|
|
),
|
|
|
|
|
_ => panic!("Unknown tool: {tool}"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let extract_path = match tool {
|
|
|
|
|
"kustomize" => "kustomize".to_string(),
|
|
|
|
|
"helm" => format!("{os}-{arch}/helm"),
|
|
|
|
|
_ => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
eprintln!("cargo:warning=Downloading {tool} {version} for {os}/{arch}...");
|
|
|
|
|
|
|
|
|
|
let response = reqwest::blocking::get(&url)
|
|
|
|
|
.unwrap_or_else(|e| panic!("Failed to download {tool}: {e}"));
|
|
|
|
|
let bytes = response
|
|
|
|
|
.bytes()
|
|
|
|
|
.unwrap_or_else(|e| panic!("Failed to read {tool} response: {e}"));
|
|
|
|
|
|
|
|
|
|
let decoder = GzDecoder::new(&bytes[..]);
|
|
|
|
|
let mut archive = Archive::new(decoder);
|
|
|
|
|
|
|
|
|
|
for entry in archive.entries().expect("Failed to read tar entries") {
|
|
|
|
|
let mut entry = entry.expect("Failed to read tar entry");
|
|
|
|
|
let path = entry
|
|
|
|
|
.path()
|
|
|
|
|
.expect("Failed to read entry path")
|
|
|
|
|
.to_path_buf();
|
|
|
|
|
if path.to_string_lossy() == extract_path {
|
|
|
|
|
let mut data = Vec::new();
|
|
|
|
|
entry
|
|
|
|
|
.read_to_end(&mut data)
|
|
|
|
|
.expect("Failed to read binary");
|
|
|
|
|
fs::write(&dest, &data).expect("Failed to write binary");
|
|
|
|
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
use std::os::unix::fs::PermissionsExt;
|
|
|
|
|
fs::set_permissions(&dest, fs::Permissions::from_mode(0o755))
|
|
|
|
|
.expect("Failed to set permissions");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
eprintln!("cargo:warning=Embedded {tool} ({} bytes)", data.len());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
panic!("Could not find {extract_path} in {tool} archive");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn git_commit_sha() -> String {
|
|
|
|
|
Command::new("git")
|
|
|
|
|
.args(["rev-parse", "--short=8", "HEAD"])
|
|
|
|
|
.output()
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|o| {
|
|
|
|
|
if o.status.success() {
|
|
|
|
|
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string())
|
|
|
|
|
}
|