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> {
let mut command = Command::new("docker");
command.stdout(Stdio::piped()).stderr(Stdio::piped());
command.args(["exec", "-t", &self.inner.id]).args(cmd);
let inner = command.spawn()?;
Ok(Child {
inner,
inner: Some(inner),
_container: self.inner.clone(),
})
}
@ -164,13 +165,22 @@ struct Inner {
// runs inside of, to prevent the scenario of the container being destroyed _before_
// the child is killed
pub struct Child {
inner: process::Child,
inner: Option<process::Child>,
_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 {
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 {
container,
zone_file,
state: Running { _child: child },
state: Running { child },
})
}
}
@ -188,7 +188,7 @@ impl<'a> NameServer<'a, Signed> {
Ok(NameServer {
container,
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> {
pub fn ipv4_addr(&self) -> Ipv4Addr {
self.container.ipv4_addr()
@ -238,7 +263,7 @@ pub struct Signed {
}
pub struct Running {
_child: Child,
child: Child,
}
fn primary_ns(ns_count: usize) -> FQDN<'static> {
@ -345,4 +370,14 @@ mod tests {
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 {
container: Container,
_child: Child,
child: Child,
}
impl RecursiveResolver {
@ -37,15 +37,33 @@ impl RecursiveResolver {
let child = container.spawn(&["unbound", "-d"])?;
Ok(Self {
_child: child,
container,
})
Ok(Self { child, container })
}
pub fn ipv4_addr(&self) -> Ipv4Addr {
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 {
@ -229,4 +247,15 @@ mod tests {
Ok(())
}
#[test]
fn terminate_works() -> Result<()> {
let resolver = RecursiveResolver::start(&[], &[])?;
let logs = resolver.terminate()?;
eprintln!("{logs}");
assert!(logs.contains("start of service"));
Ok(())
}
}