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:
atlas 2026-06-01 23:00:38 +02:00
commit 4bff450343
61 changed files with 1084 additions and 547 deletions

View file

@ -449,11 +449,12 @@ pub async fn rebuild_no_meta(
"kill before cold-start retry failed (ignored)"
);
});
run(&["start", &container]).await
.map_err(|e| anyhow::anyhow!(
run(&["start", &container]).await.map_err(|e| {
anyhow::anyhow!(
"cold-start fallback also failed: {e:#} \
(original start error: {start_err:#})"
))
)
})
} else {
Ok(())
}
@ -494,9 +495,7 @@ async fn prebuild_toplevel(name: &str, flake_ref: &str) -> Result<()> {
// pair (no current callsite does, but the pair is redundant
// and worth checking once).
if fragment != name {
anyhow::bail!(
"prebuild_toplevel: flake_ref fragment '{fragment}' ≠ agent name '{name}'"
);
anyhow::bail!("prebuild_toplevel: flake_ref fragment '{fragment}' ≠ agent name '{name}'");
}
let attr = format!("{flake_root}#nixosConfigurations.{name}.config.system.build.toplevel");
let args = vec![
@ -1135,8 +1134,7 @@ fn set_nspawn_flags(
);
}
let own_config = format!("{HOST_AGENTS_ROOT}/{agent_name}/config");
std::fs::create_dir_all(&own_config)
.with_context(|| format!("create {own_config}"))?;
std::fs::create_dir_all(&own_config).with_context(|| format!("create {own_config}"))?;
let _ = write!(binds, " --bind-ro={own_config}:/agents/{agent_name}/config");
}
@ -1337,9 +1335,9 @@ async fn run(args: &[&str]) -> Result<()> {
// every notification with the eval-error verbatim.
let journal = container_journal_tail(args).await;
match log_id {
Some(id) => bail!(
"nixos-container {cmdline} failed ({status}); see build log #{id}{journal}"
),
Some(id) => {
bail!("nixos-container {cmdline} failed ({status}); see build log #{id}{journal}")
}
None => bail!("nixos-container {cmdline} failed ({status}){journal}"),
}
}
@ -1443,4 +1441,3 @@ mod tests {
);
}
}