Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d1f0465
daemon: gracefully shutdown if wrapped command exits
patricklyc Jun 14, 2026
e33a9d0
Merge branch 'main' into main
patricklyc Jun 15, 2026
09cc841
daemon: exit with status code of wrapped command
patricklyc Jun 15, 2026
5fbda6a
Merge branch 'main' of github.com:patricklyc/intermesh
patricklyc Jun 15, 2026
935156a
daemon: select on OptionFuture instead
patricklyc Jun 16, 2026
1a50dd3
daemon: handle different failure modes
patricklyc Jun 16, 2026
09825b0
semicolon
patricklyc Jun 16, 2026
bb2eefd
daemon: add test
patricklyc Jun 17, 2026
87cacf2
hashes
patricklyc Jun 17, 2026
d813560
add exit code test
patricklyc Jun 17, 2026
9adcacd
test all exit codes
patricklyc Jun 17, 2026
55ce1d4
undo exit code test
patricklyc Jun 17, 2026
a17d6d0
unused import
patricklyc Jun 17, 2026
7dd8944
don't exec after failure
patricklyc Jun 17, 2026
5401630
verify cleanup
patricklyc Jun 17, 2026
0f39481
create exit code test helper
patricklyc Jun 17, 2026
b1b6f21
clean up exit code test
patricklyc Jun 17, 2026
d83baac
restart containers and verify nftable rules clean up
patricklyc Jun 17, 2026
13bb284
remove failed execs
patricklyc Jun 17, 2026
32fba12
start container does not need to return output
patricklyc Jun 17, 2026
96f8f27
restore exit test checks
patricklyc Jun 17, 2026
e063cd0
distinguish docker command error
patricklyc Jun 17, 2026
4bb4915
remove accidental sigterm command
patricklyc Jun 17, 2026
52d82a0
try to verify nftable rules before shutdown
patricklyc Jun 17, 2026
6d1baa7
different nftable rules check timing
patricklyc Jun 17, 2026
1a29432
use two runs to check exit code and clean up for command error case
patricklyc Jun 17, 2026
216e170
use two runs for every case
patricklyc Jun 17, 2026
6b8b3d1
add missing sigterm
patricklyc Jun 17, 2026
036fd23
replace proxy commands
patricklyc Jun 18, 2026
e51012b
remove start_container helper
patricklyc Jun 18, 2026
53d5aed
minor cleanup
patricklyc Jun 18, 2026
ca1985a
improve comments
patricklyc Jun 18, 2026
1177897
revert container idle commands
patricklyc Jun 18, 2026
a982fea
mitigate start_daemon race condition
patricklyc Jun 18, 2026
7016e5e
don't use start_daemon
patricklyc Jun 18, 2026
2b81068
daemon: gracefully shutdown if wrapped command exits
patricklyc Jun 14, 2026
008d2bd
Merge branch 'main' of github.com:patricklyc/intermesh
patricklyc Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions src/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use futures::future::OptionFuture;
use futures::TryStreamExt;
use std::collections::BTreeSet;
use std::fs;
Expand Down Expand Up @@ -118,12 +119,9 @@ pub(crate) async fn run(state: State, on_ready: Option<Command>) -> Result<()> {
let mut admin_handle = tokio::spawn(admin_server.run(grpc, adhoc, cancel.clone()));
let mut proxy_handle = tokio::spawn(proxy.run(cancel.clone()));

if let Some(mut cmd) = on_ready {
tokio::spawn(async move {
let status = cmd.status().await;
process::exit(status.map_or(1, |s| s.code().unwrap_or(1)));
});
}
let mut task_status: Option<i32> = None;
let mut command_handle =
OptionFuture::from(on_ready.map(|mut cmd| tokio::spawn(async move { cmd.status().await })));

// Wait for shutdown signal or unexpected task exit. Signal results are
// ignored - recv() only returns None if "already received" which can't
Expand All @@ -138,6 +136,22 @@ pub(crate) async fn run(state: State, on_ready: Option<Command>) -> Result<()> {
r = &mut gossip_handle => error!(?r, "gossip loop exited"),
r = &mut admin_handle => error!(?r, "admin server exited"),
r = &mut proxy_handle => error!(?r, "proxy exited"),
Some(r) = &mut command_handle => {
match r {
Err(e) => {
task_status = Some(1);
error!(?e, "command failed");
},
Ok(Err(e)) => {
task_status = Some(1);
error!(?e, "command exited");
},
Ok(Ok(s)) => {
task_status = Some(s.code().unwrap_or(1));
info!("command exited with code {}", task_status.assert());
},
}
},
}

info!("shutting down");
Expand Down Expand Up @@ -166,5 +180,11 @@ pub(crate) async fn run(state: State, on_ready: Option<Command>) -> Result<()> {
}

info!("shutdown complete");

// if proxy command exited cleanly, exit with its status code.
if let Some(status) = task_status {
Comment thread
patricklyc marked this conversation as resolved.
process::exit(status)
}

Ok(())
}
91 changes: 91 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,97 @@ fn test_graceful_shutdown() {
);
}

#[test]
#[ignore = "e2e test requiring docker"]
fn test_proxy_command_exit() {
let harness = Harness::new();

let proxy_cmd = r"intermesh proxy -- sleep 1";
let node = harness.launch("root", proxy_cmd).assert();

// Verify daemon's exit code
assert_eq!(node.exit_code().assert(), 0);

// Start a new container
let cmd = r"tail -f /dev/null";
let node = harness.launch("root", cmd).assert();
let _ = node.exec(&["intermesh", "proxy", "--", "sleep", "1"]);

// Wait for admin socket to disappear.
wait(|| node.exec(&["test", "!", "-S", "/var/run/intermesh/admin.sock"])).assert();

// Verify nftables rules were cleaned up
let result = node.exec(&["nft", "list", "table", "ip", "intermesh"]);
assert!(
result.is_err(),
"nftables table should be removed on shutdown"
);
}

#[test]
#[ignore = "e2e test requiring docker"]
fn test_proxy_command_error() {
let harness = Harness::new();

let proxy_cmd = r#"intermesh proxy -- sh -c "exit 123""#;
let node = harness.launch("root", proxy_cmd).assert();

// Verify daemon's exit code
assert_eq!(node.exit_code().assert(), 123);

// Start a new container
let cmd = r"tail -f /dev/null";
let node = harness.launch("root", cmd).assert();
let _ = node.exec(&["intermesh", "proxy", "--", "exit", "123"]);

// Wait for admin socket to disappear.
wait(|| node.exec(&["test", "!", "-S", "/var/run/intermesh/admin.sock"])).assert();

// Verify nftables rules were cleaned up
let result = node.exec(&["nft", "list", "table", "ip", "intermesh"]);
assert!(
result.is_err(),
"nftables table should be removed on shutdown"
);
}

#[test]
#[ignore = "e2e test requiring docker"]
fn test_proxy_command_killed() {
let harness = Harness::new();

let proxy_cmd = r"intermesh proxy -- sleep inf";
let node = harness.launch("root", proxy_cmd).assert();

// Verify nftables rules were injected
wait(|| node.exec(&["nft", "list", "table", "ip", "intermesh"])).assert();

// Kill wrapped command via SIGTERM
node.exec(&["pkill", "-TERM", "sleep"]).assert();

// Verify daemon's exit code
assert_eq!(node.exit_code().assert(), 1);

// Start a new container
let cmd = r"tail -f /dev/null";
let node = harness.launch("root", cmd).assert();
node.start_daemon(&["--intercept", "--", "sleep", "inf"])
.assert();

// Kill wrapped command via SIGTERM
node.exec(&["pkill", "-TERM", "sleep"]).assert();

// Wait for admin socket to disappear.
wait(|| node.exec(&["test", "!", "-S", "/var/run/intermesh/admin.sock"])).assert();

// Verify nftables rules were cleaned up
let result = node.exec(&["nft", "list", "table", "ip", "intermesh"]);
assert!(
result.is_err(),
"nftables table should be removed on shutdown"
);
}

#[test]
#[ignore = "e2e test requiring docker"]
fn test_runtime_endorsements_are_saved() {
Expand Down
12 changes: 12 additions & 0 deletions tests/helpers/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,18 @@ impl Node<'_> {
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

pub(crate) fn exit_code(&self) -> Result<u32> {
let output = Command::new("docker")
.args(["wait", &self.container])
.output()?;

if !output.status.success() {
bail!("{}", command_error(&output, &self.container))
}

Ok(String::from_utf8(output.stdout)?.trim().parse::<u32>()?)
}

pub(crate) fn get_dump(&self) -> Result<StateDump> {
let dump_toml = self.exec(&["intermesh", "debug", "dump", "--toml"])?;
toml::from_str(&dump_toml).map_err(Into::into)
Expand Down
Loading