fix: stdin password, port-forward retry, seed advisory lock
- set-password reads from stdin when password arg omitted - Port-forward proxy retries on pod restart instead of failing - cmd_seed acquires PID-based advisory lockfile to prevent concurrent runs
This commit is contained in:
29
src/cli.rs
29
src/cli.rs
@@ -267,8 +267,8 @@ pub enum UserAction {
|
||||
SetPassword {
|
||||
/// Email or identity ID.
|
||||
target: String,
|
||||
/// New password.
|
||||
password: String,
|
||||
/// New password. If omitted, reads from stdin.
|
||||
password: Option<String>,
|
||||
},
|
||||
/// Onboard new user (create + welcome email).
|
||||
Onboard {
|
||||
@@ -427,7 +427,19 @@ mod tests {
|
||||
match cli.verb {
|
||||
Some(Verb::User { action: Some(UserAction::SetPassword { target, password }) }) => {
|
||||
assert_eq!(target, "admin@example.com");
|
||||
assert_eq!(password, "hunter2");
|
||||
assert_eq!(password, Some("hunter2".to_string()));
|
||||
}
|
||||
_ => panic!("expected User SetPassword"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_set_password_no_password() {
|
||||
let cli = parse(&["sunbeam", "user", "set-password", "admin@example.com"]);
|
||||
match cli.verb {
|
||||
Some(Verb::User { action: Some(UserAction::SetPassword { target, password }) }) => {
|
||||
assert_eq!(target, "admin@example.com");
|
||||
assert!(password.is_none());
|
||||
}
|
||||
_ => panic!("expected User SetPassword"),
|
||||
}
|
||||
@@ -871,7 +883,16 @@ pub async fn dispatch() -> Result<()> {
|
||||
crate::users::cmd_user_enable(&target).await
|
||||
}
|
||||
Some(UserAction::SetPassword { target, password }) => {
|
||||
crate::users::cmd_user_set_password(&target, &password).await
|
||||
let pw = match password {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
eprint!("Password: ");
|
||||
let mut pw = String::new();
|
||||
std::io::stdin().read_line(&mut pw)?;
|
||||
pw.trim().to_string()
|
||||
}
|
||||
};
|
||||
crate::users::cmd_user_set_password(&target, &pw).await
|
||||
}
|
||||
Some(UserAction::Onboard {
|
||||
email,
|
||||
|
||||
@@ -132,18 +132,42 @@ async fn port_forward(
|
||||
.port();
|
||||
|
||||
let pod_name = pod_name.to_string();
|
||||
let ns = namespace.to_string();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut current_pod = pod_name;
|
||||
loop {
|
||||
let (mut client_stream, _) = match listener.accept().await {
|
||||
Ok(s) => s,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
let mut pf = match pods.portforward(&pod_name, &[remote_port]).await {
|
||||
let pf_result = pods.portforward(¤t_pod, &[remote_port]).await;
|
||||
let mut pf = match pf_result {
|
||||
Ok(pf) => pf,
|
||||
Err(e) => {
|
||||
eprintln!("port-forward error: {e}");
|
||||
continue;
|
||||
tracing::warn!("Port-forward failed, re-resolving pod: {e}");
|
||||
// Re-resolve the pod in case it restarted with a new name
|
||||
if let Ok(new_client) = k::get_client().await {
|
||||
let new_pods: Api<Pod> = Api::namespaced(new_client.clone(), &ns);
|
||||
let lp = ListParams::default();
|
||||
if let Ok(pod_list) = new_pods.list(&lp).await {
|
||||
if let Some(name) = pod_list
|
||||
.items
|
||||
.iter()
|
||||
.find(|p| {
|
||||
p.metadata
|
||||
.name
|
||||
.as_deref()
|
||||
.map(|n| n.starts_with(current_pod.split('-').next().unwrap_or("")))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.and_then(|p| p.metadata.name.clone())
|
||||
{
|
||||
current_pod = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue; // next accept() iteration will retry
|
||||
}
|
||||
};
|
||||
|
||||
@@ -928,7 +952,76 @@ async fn seed_kratos_admin_identity(bao: &BaoClient) -> (String, String) {
|
||||
// ── cmd_seed — main entry point ─────────────────────────────────────────────
|
||||
|
||||
/// Seed OpenBao KV with crypto-random credentials, then mirror to K8s Secrets.
|
||||
/// File-based advisory lock for `cmd_seed` to prevent concurrent runs.
|
||||
struct SeedLock {
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SeedLock {
|
||||
fn acquire() -> Result<Self> {
|
||||
let lock_path = dirs::data_dir()
|
||||
.unwrap_or_else(|| dirs::home_dir().unwrap().join(".local/share"))
|
||||
.join("sunbeam")
|
||||
.join("seed.lock");
|
||||
std::fs::create_dir_all(lock_path.parent().unwrap())?;
|
||||
|
||||
match std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path)
|
||||
{
|
||||
Ok(mut f) => {
|
||||
use std::io::Write;
|
||||
write!(f, "{}", std::process::id())?;
|
||||
Ok(SeedLock { path: lock_path })
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
// Check if the PID in the file is still alive
|
||||
if let Ok(pid_str) = std::fs::read_to_string(&lock_path) {
|
||||
if let Ok(pid) = pid_str.trim().parse::<i32>() {
|
||||
// kill(pid, 0) checks if process exists without sending a signal
|
||||
let alive = is_pid_alive(pid);
|
||||
if alive {
|
||||
return Err(SunbeamError::secrets(
|
||||
"Another sunbeam seed is already running. Wait for it to finish.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stale lock, remove and retry
|
||||
std::fs::remove_file(&lock_path)?;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&lock_path)?;
|
||||
use std::io::Write;
|
||||
write!(f, "{}", std::process::id())?;
|
||||
Ok(SeedLock { path: lock_path })
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SeedLock {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a process with the given PID is still alive.
|
||||
fn is_pid_alive(pid: i32) -> bool {
|
||||
std::process::Command::new("kill")
|
||||
.args(["-0", &pid.to_string()])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn cmd_seed() -> Result<()> {
|
||||
let _lock = SeedLock::acquire()?;
|
||||
step("Seeding secrets...");
|
||||
|
||||
let seed_result = seed_openbao().await?;
|
||||
|
||||
Reference in New Issue
Block a user