add API to gracefully terminate name server & resolver

This commit is contained in:
Jorge Aparicio 2024-02-08 17:54:35 +01:00
parent 3e78cfa30e
commit 095b68b887
3 changed files with 85 additions and 11 deletions

View File

@ -137,11 +137,12 @@ impl Container {
pub fn spawn(&self, cmd: &[&str]) -> Result<Child> { pub fn spawn(&self, cmd: &[&str]) -> Result<Child> {
let mut command = Command::new("docker"); let mut command = Command::new("docker");
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command.args(["exec", "-t", &self.inner.id]).args(cmd); command.args(["exec", "-t", &self.inner.id]).args(cmd);
let inner = command.spawn()?; let inner = command.spawn()?;
Ok(Child { Ok(Child {
inner, inner: Some(inner),
_container: self.inner.clone(), _container: self.inner.clone(),
}) })
} }
@ -164,13 +165,22 @@ struct Inner {
// runs inside of, to prevent the scenario of the container being destroyed _before_ // runs inside of, to prevent the scenario of the container being destroyed _before_
// the child is killed // the child is killed
pub struct Child { pub struct Child {
inner: process::Child, inner: Option<process::Child>,
_container: Arc<Inner>, _container: Arc<Inner>,
} }
impl Child {
pub fn wait(mut self) -> Result<Output> {
let output = self.inner.take().expect("unreachable").wait_with_output()?;
output.try_into()
}
}
impl Drop for Child { impl Drop for Child {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.inner.kill(); if let Some(mut inner) = self.inner.take() {
let _ = inner.kill();
}
} }
} }

View File

@ -152,7 +152,7 @@ impl<'a> NameServer<'a, Stopped> {
Ok(NameServer { Ok(NameServer {
container, container,
zone_file, zone_file,
state: Running { _child: child }, state: Running { child },
}) })
} }
} }
@ -188,7 +188,7 @@ impl<'a> NameServer<'a, Signed> {
Ok(NameServer { Ok(NameServer {
container, container,
zone_file, zone_file,
state: Running { _child: child }, state: Running { child },
}) })
} }
@ -209,6 +209,31 @@ impl<'a> NameServer<'a, Signed> {
} }
} }
impl<'a> NameServer<'a, Running> {
/// gracefully terminates the name server collecting all logs
pub fn terminate(self) -> Result<String> {
let pidfile = "/run/nsd/nsd.pid";
// if `terminate` is called right after `start` NSD may not have had the chance to create
// the PID file so if it doesn't exist wait for a bit before invoking `kill`
let kill = format!(
"test -f {pidfile} || sleep 1
kill -TERM $(cat {pidfile})"
);
self.container.status_ok(&["sh", "-c", &kill])?;
let output = self.state.child.wait()?;
if !output.status.success() {
return Err("could not terminate the `unbound` process".into());
}
assert!(
output.stderr.is_empty(),
"stderr should be returned if not empty"
);
Ok(output.stdout)
}
}
impl<'a, S> NameServer<'a, S> { impl<'a, S> NameServer<'a, S> {
pub fn ipv4_addr(&self) -> Ipv4Addr { pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr() self.container.ipv4_addr()
@ -238,7 +263,7 @@ pub struct Signed {
} }
pub struct Running { pub struct Running {
_child: Child, child: Child,
} }
fn primary_ns(ns_count: usize) -> FQDN<'static> { fn primary_ns(ns_count: usize) -> FQDN<'static> {
@ -345,4 +370,14 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn terminate_works() -> Result<()> {
let ns = NameServer::new(FQDN::ROOT)?.start()?;
let logs = ns.terminate()?;
assert!(logs.contains("nsd starting"));
Ok(())
}
} }

View File

@ -7,7 +7,7 @@ use crate::Result;
pub struct RecursiveResolver { pub struct RecursiveResolver {
container: Container, container: Container,
_child: Child, child: Child,
} }
impl RecursiveResolver { impl RecursiveResolver {
@ -37,15 +37,33 @@ impl RecursiveResolver {
let child = container.spawn(&["unbound", "-d"])?; let child = container.spawn(&["unbound", "-d"])?;
Ok(Self { Ok(Self { child, container })
_child: child,
container,
})
} }
pub fn ipv4_addr(&self) -> Ipv4Addr { pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr() self.container.ipv4_addr()
} }
/// gracefully terminates the name server collecting all logs
pub fn terminate(self) -> Result<String> {
let pidfile = "/run/unbound.pid";
let kill = format!(
"test -f {pidfile} || sleep 1
kill -TERM $(cat {pidfile})"
);
self.container.status_ok(&["sh", "-c", &kill])?;
let output = self.child.wait()?;
if !output.status.success() {
return Err("could not terminate the `unbound` process".into());
}
assert!(
output.stderr.is_empty(),
"stderr should be returned if not empty"
);
Ok(output.stdout)
}
} }
fn unbound_conf(use_dnssec: bool) -> String { fn unbound_conf(use_dnssec: bool) -> String {
@ -229,4 +247,15 @@ mod tests {
Ok(()) Ok(())
} }
#[test]
fn terminate_works() -> Result<()> {
let resolver = RecursiveResolver::start(&[], &[])?;
let logs = resolver.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("start of service"));
Ok(())
}
} }