hive-priv: test main.rs's request dispatch and validation, and infra.rs
check_socket_activation and response_of are extracted from socket_listener and dispatch unchanged, and infra.rs takes a &impl Spawn, so tests run stand-ins instead of systemctl.
This commit is contained in:
parent
b8a1e830cf
commit
6a1e47532d
1 changed files with 796 additions and 23 deletions
|
|
@ -2,9 +2,9 @@
|
|||
//! [`InfraContainer`] unit) and `ReloadGatewayNginx`. Neither takes a unit or
|
||||
//! container name from the caller.
|
||||
|
||||
use crate::shared::{RealSpawn, Spawn};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_priv_sock::{InfraAction, InfraContainer};
|
||||
use tokio::process::Command;
|
||||
|
||||
/// `ControlInfraContainer` — start/stop a hive infrastructure service via
|
||||
/// `systemctl <verb> <unit>`. The [`InfraContainer`] enum is the allowlist:
|
||||
|
|
@ -18,10 +18,19 @@ use tokio::process::Command;
|
|||
pub(crate) async fn control_infra_container(
|
||||
container: InfraContainer,
|
||||
action: InfraAction,
|
||||
) -> Result<(String, String)> {
|
||||
control_infra_container_with(&RealSpawn, container, action).await
|
||||
}
|
||||
|
||||
async fn control_infra_container_with(
|
||||
spawn: &impl Spawn,
|
||||
container: InfraContainer,
|
||||
action: InfraAction,
|
||||
) -> Result<(String, String)> {
|
||||
let verb = action.systemctl_verb();
|
||||
let unit = container.service_unit();
|
||||
let out = Command::new("systemctl")
|
||||
let out = spawn
|
||||
.command("systemctl")
|
||||
.args([verb, unit])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -55,7 +64,12 @@ pub(crate) async fn control_infra_container(
|
|||
/// Returns `(String::new(), String::new())` on success so it fits the
|
||||
/// `exec` return type directly.
|
||||
pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
|
||||
let state_out = Command::new("systemctl")
|
||||
sync_gateway_nginx_with(&RealSpawn).await
|
||||
}
|
||||
|
||||
async fn sync_gateway_nginx_with(spawn: &impl Spawn) -> Result<(String, String)> {
|
||||
let state_out = spawn
|
||||
.command("systemctl")
|
||||
.args(["show", "--property=ActiveState", "--value", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -72,7 +86,8 @@ pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
// start-limit failure; plain start when inactive or unknown.
|
||||
match state.as_str() {
|
||||
"active" => {
|
||||
let out = Command::new("systemctl")
|
||||
let out = spawn
|
||||
.command("systemctl")
|
||||
.args(["reload", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -87,11 +102,13 @@ pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
}
|
||||
"failed" => {
|
||||
// Clear start-limit so the next start can proceed.
|
||||
let _ = Command::new("systemctl")
|
||||
let _ = spawn
|
||||
.command("systemctl")
|
||||
.args(["reset-failed", "nginx"])
|
||||
.status()
|
||||
.await;
|
||||
let out = Command::new("systemctl")
|
||||
let out = spawn
|
||||
.command("systemctl")
|
||||
.args(["start", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -106,7 +123,8 @@ pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
}
|
||||
_ => {
|
||||
// inactive, activating, deactivating, unknown — just start.
|
||||
let out = Command::new("systemctl")
|
||||
let out = spawn
|
||||
.command("systemctl")
|
||||
.args(["start", "nginx"])
|
||||
.output()
|
||||
.await
|
||||
|
|
@ -125,7 +143,12 @@ pub(crate) async fn sync_gateway_nginx() -> Result<(String, String)> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{InfraAction, InfraContainer};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{
|
||||
InfraAction, InfraContainer, control_infra_container_with, sync_gateway_nginx_with,
|
||||
};
|
||||
use crate::shared::test_support::{MissingTool, ScriptSpawn, scratch};
|
||||
use hive_priv_sock::PrivRequest;
|
||||
|
||||
/// A start/stop naming the forge or matrix container fails to decode, so
|
||||
|
|
@ -148,4 +171,192 @@ mod tests {
|
|||
})
|
||||
));
|
||||
}
|
||||
|
||||
/// Stand-in for `systemctl`: every call is logged as `systemctl args…`,
|
||||
/// then `behave` (a shell script over `$@`) decides output and status.
|
||||
struct Stub {
|
||||
dir: PathBuf,
|
||||
script: String,
|
||||
}
|
||||
|
||||
impl Stub {
|
||||
fn new(behave: &str) -> Self {
|
||||
let dir = scratch();
|
||||
let script = format!("echo \"$0 $*\" >> {d}/calls\n{behave}", d = dir.display());
|
||||
Self { dir, script }
|
||||
}
|
||||
|
||||
fn spawn(&self) -> ScriptSpawn {
|
||||
ScriptSpawn(self.script.clone())
|
||||
}
|
||||
|
||||
fn calls(&self) -> Vec<String> {
|
||||
std::fs::read_to_string(self.dir.join("calls"))
|
||||
.map(|log| log.lines().map(str::to_owned).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stub {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.dir).ok();
|
||||
}
|
||||
}
|
||||
|
||||
const SHOW: &str = "systemctl show --property=ActiveState --value nginx";
|
||||
|
||||
/// A host whose nginx reports `state`; every other verb succeeds.
|
||||
fn nginx_in(state: &str) -> Stub {
|
||||
Stub::new(&format!("case \"$1\" in show) echo {state};; esac"))
|
||||
}
|
||||
|
||||
async fn sync(stub: &Stub) -> anyhow::Result<(String, String)> {
|
||||
sync_gateway_nginx_with(&stub.spawn()).await
|
||||
}
|
||||
|
||||
async fn control(
|
||||
stub: &Stub,
|
||||
container: InfraContainer,
|
||||
action: InfraAction,
|
||||
) -> anyhow::Result<(String, String)> {
|
||||
control_infra_container_with(&stub.spawn(), container, action).await
|
||||
}
|
||||
|
||||
fn err_text(r: anyhow::Result<(String, String)>) -> String {
|
||||
format!("{:#}", r.unwrap_err())
|
||||
}
|
||||
|
||||
// ---- ControlInfraContainer ----
|
||||
|
||||
/// The unit comes from the variant and the verb from the action: the
|
||||
/// whole argv is `systemctl <verb> nginx.service`, nothing else.
|
||||
#[tokio::test]
|
||||
async fn control_runs_exactly_the_verb_on_the_variants_unit() {
|
||||
for (action, verb) in [(InfraAction::Start, "start"), (InfraAction::Stop, "stop")] {
|
||||
let stub = Stub::new("echo out; echo err >&2");
|
||||
let (stdout, stderr) = control(&stub, InfraContainer::Gateway, action)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
stub.calls(),
|
||||
[format!("systemctl {verb} nginx.service")],
|
||||
"{verb}"
|
||||
);
|
||||
assert_eq!((stdout.as_str(), stderr.as_str()), ("out\n", "err\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-zero exit is an error naming the verb, the unit, the status and
|
||||
/// the trimmed stderr, so the operator sees why `hivectl` failed.
|
||||
#[tokio::test]
|
||||
async fn control_reports_a_failed_systemctl_with_its_stderr() {
|
||||
let stub = Stub::new("echo 'unit is masked' >&2; exit 5");
|
||||
let msg = err_text(control(&stub, InfraContainer::Gateway, InfraAction::Stop).await);
|
||||
assert!(msg.contains("systemctl stop nginx.service exited"), "{msg}");
|
||||
assert!(msg.ends_with("unit is masked"), "{msg}");
|
||||
}
|
||||
|
||||
/// A host without `systemctl` is an error that names the command.
|
||||
#[tokio::test]
|
||||
async fn control_reports_a_missing_systemctl_by_name() {
|
||||
let msg = err_text(
|
||||
control_infra_container_with(&MissingTool, InfraContainer::Gateway, InfraAction::Start)
|
||||
.await,
|
||||
);
|
||||
assert!(msg.contains("systemctl start nginx.service"), "{msg}");
|
||||
}
|
||||
|
||||
// ---- ReloadGatewayNginx ----
|
||||
|
||||
/// A running nginx is reloaded, never restarted.
|
||||
#[tokio::test]
|
||||
async fn an_active_nginx_is_reloaded() {
|
||||
let stub = nginx_in("active");
|
||||
assert_eq!(sync(&stub).await.unwrap(), (String::new(), String::new()));
|
||||
assert_eq!(stub.calls(), [SHOW, "systemctl reload nginx"]);
|
||||
}
|
||||
|
||||
/// A failed nginx has its start-limit cleared before the start.
|
||||
#[tokio::test]
|
||||
async fn a_failed_nginx_is_reset_then_started() {
|
||||
let stub = nginx_in("failed");
|
||||
sync(&stub).await.unwrap();
|
||||
assert_eq!(
|
||||
stub.calls(),
|
||||
[
|
||||
SHOW,
|
||||
"systemctl reset-failed nginx",
|
||||
"systemctl start nginx"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// Any other state, including one the query could not produce, is a
|
||||
/// plain start: no reload of a unit that is not running.
|
||||
#[tokio::test]
|
||||
async fn every_other_state_is_a_plain_start() {
|
||||
for state in ["inactive", "activating", "deactivating", "reloading", ""] {
|
||||
let stub = nginx_in(state);
|
||||
sync(&stub).await.unwrap();
|
||||
assert_eq!(stub.calls(), [SHOW, "systemctl start nginx"], "{state:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A failing state query is only logged: the sync still starts nginx
|
||||
/// from whatever the query printed, here nothing.
|
||||
#[tokio::test]
|
||||
async fn a_failed_state_query_still_starts_nginx() {
|
||||
let stub = Stub::new("case \"$1\" in show) echo boom >&2; exit 1;; esac");
|
||||
sync(&stub).await.unwrap();
|
||||
assert_eq!(stub.calls(), [SHOW, "systemctl start nginx"]);
|
||||
}
|
||||
|
||||
/// A failed reload is an error carrying systemctl's stderr; a failed
|
||||
/// `reset-failed` is not, because the start that follows is what counts.
|
||||
#[tokio::test]
|
||||
async fn a_failed_reload_is_an_error_and_a_failed_reset_is_not() {
|
||||
let stub = Stub::new(
|
||||
"case \"$1\" in show) echo active;; reload) echo 'bad config' >&2; exit 1;; esac",
|
||||
);
|
||||
let msg = err_text(sync(&stub).await);
|
||||
assert!(msg.contains("gateway nginx reload failed"), "{msg}");
|
||||
assert!(msg.ends_with("bad config"), "{msg}");
|
||||
|
||||
let stub = Stub::new("case \"$1\" in show) echo failed;; reset-failed) exit 1;; esac");
|
||||
sync(&stub).await.unwrap();
|
||||
assert_eq!(
|
||||
stub.calls(),
|
||||
[
|
||||
SHOW,
|
||||
"systemctl reset-failed nginx",
|
||||
"systemctl start nginx"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// A start that fails is an error on both start paths, and the plain
|
||||
/// one names the state it started from.
|
||||
#[tokio::test]
|
||||
async fn a_failed_start_is_an_error_on_both_start_paths() {
|
||||
let stub = Stub::new(
|
||||
"case \"$1\" in show) echo failed;; start) echo 'start limit hit' >&2; exit 1;; esac",
|
||||
);
|
||||
let msg = err_text(sync(&stub).await);
|
||||
assert!(msg.contains("start (after reset-failed) failed"), "{msg}");
|
||||
assert!(msg.ends_with("start limit hit"), "{msg}");
|
||||
|
||||
let stub = Stub::new(
|
||||
"case \"$1\" in show) echo inactive;; start) echo 'no such unit' >&2; exit 1;; esac",
|
||||
);
|
||||
let msg = err_text(sync(&stub).await);
|
||||
assert!(msg.contains("state=\"inactive\""), "{msg}");
|
||||
assert!(msg.ends_with("no such unit"), "{msg}");
|
||||
}
|
||||
|
||||
/// Without `systemctl` the state query itself fails, and says so.
|
||||
#[tokio::test]
|
||||
async fn a_missing_systemctl_fails_the_state_query() {
|
||||
let msg = err_text(sync_gateway_nginx_with(&MissingTool).await);
|
||||
assert!(msg.contains("query gateway nginx ActiveState"), "{msg}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue