hive-priv: extract exec's non-delegating match arms into named functions
This commit is contained in:
parent
f1c5d49289
commit
584a973344
1 changed files with 86 additions and 70 deletions
|
|
@ -332,34 +332,23 @@ async fn write_line_event(writer: &mut OwnedWriteHalf, stream: PrivStream, data:
|
||||||
/// For streaming ops (`CreateContainer`/`UpdateContainer` with `stream: true`)
|
/// For streaming ops (`CreateContainer`/`UpdateContainer` with `stream: true`)
|
||||||
/// output lines are forwarded to `writer` as `PrivEvent::Line` messages and
|
/// output lines are forwarded to `writer` as `PrivEvent::Line` messages and
|
||||||
/// the returned strings are empty.
|
/// the returned strings are empty.
|
||||||
// One match arm per priv op — a flat 1:1 dispatch table. The length tracks
|
|
||||||
// the op count, not complexity; splitting it would just scatter the mapping.
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
/// Execute one validated request.
|
|
||||||
///
|
///
|
||||||
/// `fd` is the descriptor that arrived with this request, already checked
|
/// `fd` is the descriptor that arrived with this request, already checked
|
||||||
/// against the operation by [`check_fd_agreement`]: `Some` exactly for
|
/// against the operation by [`check_fd_agreement`]: `Some` exactly for
|
||||||
/// the variants that stream into a caller-supplied descriptor, `None`
|
/// the variants that stream into a caller-supplied descriptor, `None`
|
||||||
/// for every other operation.
|
/// for every other operation.
|
||||||
|
// One match arm per priv op — a flat 1:1 dispatch table. Every arm either
|
||||||
|
// delegates directly or validates then delegates; an op whose handling is
|
||||||
|
// more than that gets its own named function instead, so the match's length
|
||||||
|
// tracks the op count, not complexity.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn exec(
|
async fn exec(
|
||||||
req: PrivRequest,
|
req: PrivRequest,
|
||||||
fd: Option<OwnedFd>,
|
fd: Option<OwnedFd>,
|
||||||
writer: &mut OwnedWriteHalf,
|
writer: &mut OwnedWriteHalf,
|
||||||
) -> Result<(String, String)> {
|
) -> Result<(String, String)> {
|
||||||
match req {
|
match req {
|
||||||
PrivRequest::StartContainer { ref name } => {
|
PrivRequest::StartContainer { ref name } => start_container(name).await,
|
||||||
validate_container_name(name)?;
|
|
||||||
let machine = container_system_name(name);
|
|
||||||
// Clear any start-limit lockout left by earlier failures so a
|
|
||||||
// now-correct start isn't blocked. nixos-container start does not
|
|
||||||
// do this itself. Best-effort: if the unit doesn't exist yet
|
|
||||||
// (first-time create) reset-failed is a no-op and we proceed.
|
|
||||||
let _ = Command::new("systemctl")
|
|
||||||
.args(["reset-failed", &format!("container@{machine}.service")])
|
|
||||||
.status()
|
|
||||||
.await;
|
|
||||||
container_run(&["start", &machine]).await
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivRequest::StopContainer { ref name } => {
|
PrivRequest::StopContainer { ref name } => {
|
||||||
validate_container_name(name)?;
|
validate_container_name(name)?;
|
||||||
|
|
@ -419,12 +408,7 @@ async fn exec(
|
||||||
|
|
||||||
PrivRequest::ReloadGatewayNginx => sync_gateway_nginx().await,
|
PrivRequest::ReloadGatewayNginx => sync_gateway_nginx().await,
|
||||||
|
|
||||||
PrivRequest::RunForgeAdmin { ref args } => {
|
PrivRequest::RunForgeAdmin { ref args } => exec_forge_admin(args).await,
|
||||||
for arg in args {
|
|
||||||
validate_forge_admin_arg(arg)?;
|
|
||||||
}
|
|
||||||
run_forge_admin(args).await
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivRequest::SetAgentPaused {
|
PrivRequest::SetAgentPaused {
|
||||||
ref agent_name,
|
ref agent_name,
|
||||||
|
|
@ -447,36 +431,7 @@ async fn exec(
|
||||||
ref token,
|
ref token,
|
||||||
ref account,
|
ref account,
|
||||||
ref homeserver,
|
ref homeserver,
|
||||||
} => {
|
} => write_matrix_token(agent_name, token, account.as_deref(), homeserver.as_deref()),
|
||||||
validate_agent_name(agent_name)?;
|
|
||||||
// Build the token filename. `None` → the hive account's
|
|
||||||
// `matrix-token`; `Some(a)` → `matrix-token-<a>`. The account
|
|
||||||
// suffix MUST be validated as a plain identifier (no `/`, `.`,
|
|
||||||
// `..`) before it goes into the filename, or a crafted account
|
|
||||||
// could traverse out of the state dir — `write_agent_state_file`
|
|
||||||
// trusts its `filename` argument.
|
|
||||||
let filename = match account {
|
|
||||||
None => "matrix-token".to_owned(),
|
|
||||||
Some(a) => {
|
|
||||||
validate_name_chars(a)?;
|
|
||||||
format!("matrix-token-{a}")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let res = write_agent_state_file(agent_name, &filename, &format!("{token}\n"))?;
|
|
||||||
// For an extra account, persist its homeserver in a sidecar
|
|
||||||
// (`matrix-account-<a>.json`) so the daemon can auto-discover the
|
|
||||||
// account without a static `matrixAccounts` config entry. Only
|
|
||||||
// when both `account` and `homeserver` are present; the account
|
|
||||||
// suffix is already validated above.
|
|
||||||
if let (Some(a), Some(hs)) = (account, homeserver) {
|
|
||||||
let meta = serde_json::to_string(&MatrixAccountSidecar {
|
|
||||||
homeserver: hs.as_str(),
|
|
||||||
})
|
|
||||||
.context("serialize matrix account sidecar")?;
|
|
||||||
write_agent_state_file(agent_name, &format!("matrix-account-{a}.json"), &meta)?;
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivRequest::WriteAgentGithubToken {
|
PrivRequest::WriteAgentGithubToken {
|
||||||
ref agent_name,
|
ref agent_name,
|
||||||
|
|
@ -491,23 +446,7 @@ async fn exec(
|
||||||
ref label,
|
ref label,
|
||||||
ref base_url,
|
ref base_url,
|
||||||
ref token,
|
ref token,
|
||||||
} => {
|
} => write_extra_forge_account(agent_name, label, base_url, token),
|
||||||
validate_agent_name(agent_name)?;
|
|
||||||
validate_name_chars(label)?;
|
|
||||||
let res = write_agent_state_file(
|
|
||||||
agent_name,
|
|
||||||
&format!("forge-{label}-token"),
|
|
||||||
&format!("{token}\n"),
|
|
||||||
)?;
|
|
||||||
// Sidecar carries the base URL — there's no host-side nix config
|
|
||||||
// for extra forges, so this is the only place it's persisted.
|
|
||||||
let meta = serde_json::to_string(&ForgeSidecar {
|
|
||||||
base_url: base_url.as_str(),
|
|
||||||
})
|
|
||||||
.context("serialize forge account sidecar")?;
|
|
||||||
write_agent_state_file(agent_name, &format!("forge-{label}.json"), &meta)?;
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
PrivRequest::DeleteAgentExtraForgeAccount {
|
PrivRequest::DeleteAgentExtraForgeAccount {
|
||||||
ref agent_name,
|
ref agent_name,
|
||||||
|
|
@ -622,6 +561,83 @@ async fn exec(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `StartContainer`: clear any start-limit lockout left by earlier failures
|
||||||
|
/// so a now-correct start isn't blocked (`nixos-container start` does not do
|
||||||
|
/// this itself). Best-effort — if the unit doesn't exist yet (first-time
|
||||||
|
/// create), `reset-failed` is a no-op and the start proceeds regardless.
|
||||||
|
async fn start_container(name: &str) -> Result<(String, String)> {
|
||||||
|
validate_container_name(name)?;
|
||||||
|
let machine = container_system_name(name);
|
||||||
|
let _ = Command::new("systemctl")
|
||||||
|
.args(["reset-failed", &format!("container@{machine}.service")])
|
||||||
|
.status()
|
||||||
|
.await;
|
||||||
|
container_run(&["start", &machine]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `RunForgeAdmin`: every arg must pass [`validate_forge_admin_arg`] before
|
||||||
|
/// the admin CLI ever sees it.
|
||||||
|
async fn exec_forge_admin(args: &[String]) -> Result<(String, String)> {
|
||||||
|
for arg in args {
|
||||||
|
validate_forge_admin_arg(arg)?;
|
||||||
|
}
|
||||||
|
run_forge_admin(args).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `WriteAgentMatrixToken`: `account = None` writes the hive account's
|
||||||
|
/// `matrix-token`; `Some(a)` writes `matrix-token-<a>`. The account suffix
|
||||||
|
/// MUST be validated as a plain identifier (no `/`, `.`, `..`) before it goes
|
||||||
|
/// into the filename, or a crafted account could traverse out of the state
|
||||||
|
/// dir — `write_agent_state_file` trusts its `filename` argument. When both
|
||||||
|
/// `account` and `homeserver` are `Some`, also persists a
|
||||||
|
/// `matrix-account-<a>.json` sidecar so the daemon can auto-discover the
|
||||||
|
/// extra account without a static `matrixAccounts` config entry.
|
||||||
|
fn write_matrix_token(
|
||||||
|
agent_name: &str,
|
||||||
|
token: &str,
|
||||||
|
account: Option<&str>,
|
||||||
|
homeserver: Option<&str>,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
validate_agent_name(agent_name)?;
|
||||||
|
let filename = match account {
|
||||||
|
None => "matrix-token".to_owned(),
|
||||||
|
Some(a) => {
|
||||||
|
validate_name_chars(a)?;
|
||||||
|
format!("matrix-token-{a}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let res = write_agent_state_file(agent_name, &filename, &format!("{token}\n"))?;
|
||||||
|
if let (Some(a), Some(hs)) = (account, homeserver) {
|
||||||
|
let meta = serde_json::to_string(&MatrixAccountSidecar { homeserver: hs })
|
||||||
|
.context("serialize matrix account sidecar")?;
|
||||||
|
write_agent_state_file(agent_name, &format!("matrix-account-{a}.json"), &meta)?;
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `WriteAgentExtraForgeAccount`: writes the token, then a
|
||||||
|
/// `forge-<label>.json` sidecar carrying the base URL — there's no
|
||||||
|
/// host-side nix config for extra forges, so this is the only place it's
|
||||||
|
/// persisted.
|
||||||
|
fn write_extra_forge_account(
|
||||||
|
agent_name: &str,
|
||||||
|
label: &str,
|
||||||
|
base_url: &str,
|
||||||
|
token: &str,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
validate_agent_name(agent_name)?;
|
||||||
|
validate_name_chars(label)?;
|
||||||
|
let res = write_agent_state_file(
|
||||||
|
agent_name,
|
||||||
|
&format!("forge-{label}-token"),
|
||||||
|
&format!("{token}\n"),
|
||||||
|
)?;
|
||||||
|
let meta = serde_json::to_string(&ForgeSidecar { base_url })
|
||||||
|
.context("serialize forge account sidecar")?;
|
||||||
|
write_agent_state_file(agent_name, &format!("forge-{label}.json"), &meta)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
/// Shared body for `CreateContainer` / `UpdateContainer`: validate the
|
/// Shared body for `CreateContainer` / `UpdateContainer`: validate the
|
||||||
/// name, build the toplevel ourselves, and run
|
/// name, build the toplevel ourselves, and run
|
||||||
/// `nixos-container <verb> … --system-path <built>` (streaming line
|
/// `nixos-container <verb> … --system-path <built>` (streaming line
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue