feat(gateway): hivectl gateway user management + fix htpasswdFile assertion
Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.
Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.
Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
This commit is contained in:
parent
25d2951d1e
commit
4bff450343
61 changed files with 1084 additions and 547 deletions
|
|
@ -60,10 +60,7 @@ fn tasks_dir() -> PathBuf {
|
|||
} else {
|
||||
// Pre-split fallback: derive harness/ as a sibling of state/.
|
||||
let state = crate::paths::state_dir();
|
||||
state
|
||||
.parent()
|
||||
.map(|p| p.join("harness"))
|
||||
.unwrap_or(state)
|
||||
state.parent().map(|p| p.join("harness")).unwrap_or(state)
|
||||
};
|
||||
base.join("bash-tasks")
|
||||
}
|
||||
|
|
@ -136,9 +133,8 @@ impl TaskFile {
|
|||
|
||||
/// Write a task file atomically (tmp + rename).
|
||||
fn write_task(task: &TaskFile) -> std::io::Result<()> {
|
||||
let json = serde_json::to_string_pretty(task).map_err(|e| {
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
|
||||
})?;
|
||||
let json = serde_json::to_string_pretty(task)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let dest = task_json(&task.id);
|
||||
let tmp = dest.with_extension("json.tmp");
|
||||
std::fs::write(&tmp, json)?;
|
||||
|
|
@ -229,7 +225,7 @@ async fn run_loop(socket: PathBuf) {
|
|||
let claimed: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
|
||||
|
||||
loop {
|
||||
poll_once(&socket, &claimed);
|
||||
poll_once(&socket, &claimed).await;
|
||||
tokio::time::sleep(POLL_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,14 +233,20 @@ async fn run_loop(socket: PathBuf) {
|
|||
/// On boot, find any task files in `running` state and flip them to
|
||||
/// `interrupted`, then fire a wake so the agent unblocks.
|
||||
async fn mark_interrupted(socket: &Path) {
|
||||
let Ok(rd) = std::fs::read_dir(tasks_dir()) else { return };
|
||||
let Ok(rd) = std::fs::read_dir(tasks_dir()) else {
|
||||
return;
|
||||
};
|
||||
for entry in rd.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else { continue };
|
||||
let Some(mut task) = read_task(&id) else { continue };
|
||||
let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else {
|
||||
continue;
|
||||
};
|
||||
let Some(mut task) = read_task(&id) else {
|
||||
continue;
|
||||
};
|
||||
if task.status != TaskStatus::Running {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -258,14 +260,18 @@ async fn mark_interrupted(socket: &Path) {
|
|||
}
|
||||
}
|
||||
|
||||
fn poll_once(socket: &Path, claimed: &Arc<Mutex<HashSet<String>>>) {
|
||||
let Ok(rd) = std::fs::read_dir(tasks_dir()) else { return };
|
||||
async fn poll_once(socket: &Path, claimed: &Arc<Mutex<HashSet<String>>>) {
|
||||
let Ok(rd) = std::fs::read_dir(tasks_dir()) else {
|
||||
return;
|
||||
};
|
||||
for entry in rd.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
||||
continue;
|
||||
}
|
||||
let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else { continue };
|
||||
let Some(id) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else {
|
||||
continue;
|
||||
};
|
||||
{
|
||||
let guard = claimed.lock().unwrap();
|
||||
if guard.contains(&id) {
|
||||
|
|
@ -322,7 +328,11 @@ async fn run_task(mut task: TaskFile, socket: &Path) {
|
|||
let stdout_tail = tail_file(&out_path, SUMMARY_BYTES);
|
||||
let stderr_tail = tail_file(&err_path, SUMMARY_BYTES);
|
||||
|
||||
task.status = if timed_out { TaskStatus::TimedOut } else { TaskStatus::Done };
|
||||
task.status = if timed_out {
|
||||
TaskStatus::TimedOut
|
||||
} else {
|
||||
TaskStatus::Done
|
||||
};
|
||||
task.completed_at = Some(crate::serve_common::now_unix());
|
||||
task.exit_code = exit_code;
|
||||
task.stdout_tail = stdout_tail.clone().filter(|s| !s.is_empty());
|
||||
|
|
@ -344,7 +354,12 @@ async fn run_task(mut task: TaskFile, socket: &Path) {
|
|||
|
||||
/// Run `sh -c cmd`, streaming output to files. Returns `(exit_code, timed_out)`.
|
||||
/// On timeout the child process is explicitly killed before returning.
|
||||
async fn exec_cmd(cmd: &str, out_path: &Path, err_path: &Path, timeout: Duration) -> Result<(i32, bool)> {
|
||||
async fn exec_cmd(
|
||||
cmd: &str,
|
||||
out_path: &Path,
|
||||
err_path: &Path,
|
||||
timeout: Duration,
|
||||
) -> Result<(i32, bool)> {
|
||||
use tokio::process::Command;
|
||||
let mut child = Command::new("sh")
|
||||
.arg("-c")
|
||||
|
|
@ -396,7 +411,9 @@ where
|
|||
let _ = tokio::io::copy(&mut reader, &mut f).await;
|
||||
let _ = f.flush().await;
|
||||
}
|
||||
Err(e) => tracing::warn!(path = %path.display(), error = ?e, "bash_runner: open output file failed"),
|
||||
Err(e) => {
|
||||
tracing::warn!(path = %path.display(), error = ?e, "bash_runner: open output file failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -415,12 +432,7 @@ fn tail_file(path: &Path, max_bytes: usize) -> Option<String> {
|
|||
// Wake delivery
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async fn send_wake(
|
||||
socket: &Path,
|
||||
id: &str,
|
||||
summary: &str,
|
||||
output: Option<(&str, &str)>,
|
||||
) {
|
||||
async fn send_wake(socket: &Path, id: &str, summary: &str, output: Option<(&str, &str)>) {
|
||||
let mut body = format!("bash task `{id}` finished: {summary}");
|
||||
if let Some((stdout, stderr)) = output {
|
||||
if !stdout.is_empty() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue