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.
93 lines
2.4 KiB
Rust
93 lines
2.4 KiB
Rust
/// Print a step header.
|
|
pub fn step(msg: &str) {
|
|
println!("\n==> {msg}");
|
|
}
|
|
|
|
/// Print a success/info line.
|
|
pub fn ok(msg: &str) {
|
|
println!(" {msg}");
|
|
}
|
|
|
|
/// Print a warning to stderr.
|
|
pub fn warn(msg: &str) {
|
|
eprintln!(" WARN: {msg}");
|
|
}
|
|
|
|
/// Return an aligned text table. Columns padded to max width.
|
|
pub fn table(rows: &[Vec<String>], headers: &[&str]) -> String {
|
|
if headers.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
let mut col_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
|
|
for row in rows {
|
|
for (i, cell) in row.iter().enumerate() {
|
|
if i < col_widths.len() {
|
|
col_widths[i] = col_widths[i].max(cell.len());
|
|
}
|
|
}
|
|
}
|
|
|
|
let header_line: String = headers
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, h)| format!("{:<width$}", h, width = col_widths[i]))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
|
|
let separator: String = col_widths
|
|
.iter()
|
|
.map(|&w| "-".repeat(w))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
|
|
let mut lines = vec![header_line, separator];
|
|
|
|
for row in rows {
|
|
let cells: Vec<String> = (0..headers.len())
|
|
.map(|i| {
|
|
let val = row.get(i).map(|s| s.as_str()).unwrap_or("");
|
|
format!("{:<width$}", val, width = col_widths[i])
|
|
})
|
|
.collect();
|
|
lines.push(cells.join(" "));
|
|
}
|
|
|
|
lines.join("\n")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_table_basic() {
|
|
let rows = vec![
|
|
vec!["abc".to_string(), "def".to_string()],
|
|
vec!["x".to_string(), "longer".to_string()],
|
|
];
|
|
let result = table(&rows, &["Col1", "Col2"]);
|
|
assert!(result.contains("Col1"));
|
|
assert!(result.contains("Col2"));
|
|
assert!(result.contains("abc"));
|
|
assert!(result.contains("longer"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_empty_headers() {
|
|
let result = table(&[], &[]);
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_table_column_widths() {
|
|
let rows = vec![vec!["short".to_string(), "x".to_string()]];
|
|
let result = table(&rows, &["LongHeader", "H2"]);
|
|
// Header should set minimum width
|
|
for line in result.lines().skip(2) {
|
|
// Data row: "short" should be padded to "LongHeader" width
|
|
assert!(line.starts_with("short "));
|
|
}
|
|
}
|
|
}
|