Skip to content

Commit d4c1530

Browse files
authored
feat: Support --empty-ns checkpoint/restore option (#3700)
* Support `--empty-ns` checkpoint option Pass --empty-ns from the CLI to CRIU so the network namespace is created empty on restore instead of being dumped. Youki does not manage network devices, so their external dependencies cannot be described to CRIU. Runc sets this unconditionally for the same reason. To verifty the functionality, contest check the existence of netdev-*.img which holds the network devices of a dumped namespace. Signed-off-by: donkomura <koiru3822fs@gmail.com> * Reject unsupported namespaces for --empty-ns Only `network` namespace is supported for empty-ns option, but the given value was not validated. Add the check and reject others to match runc. Also document that `network` applies even without the flag. Signed-off-by: donkomura <koiru3822fs@gmail.com> --------- Signed-off-by: donkomura <koiru3822fs@gmail.com>
1 parent b6e12c8 commit d4c1530

7 files changed

Lines changed: 178 additions & 3 deletions

File tree

crates/libcontainer/src/container/container.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ pub struct CheckpointOptions {
244244
pub work_path: Option<PathBuf>,
245245
pub manage_cgroups_mode: rust_criu::CgMode,
246246
pub link_remap: bool,
247+
pub empty_net_ns: bool,
247248
}
248249

249250
#[cfg(test)]

crates/libcontainer/src/container/container_checkpoint.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ impl Container {
170170
);
171171
criu.cgroups_mode(opts.manage_cgroups_mode.clone());
172172
criu.set_link_remap(opts.link_remap);
173+
// youki does not manage network devices, so external dependencies such as the
174+
// host side of a veth pair cannot be described to CRIU. Dumping them makes
175+
// restore fail with "Unknown peer net namespace", so runc sets this unconditionally.
176+
// See: https://github.com/opencontainers/runc/commit/8187fb740c202f5d29f1717bb933143c10dae8a1
177+
criu.set_empty_net_ns(opts.empty_net_ns);
173178

174179
// Register network and PID namespaces as external to CRIU.
175180
//

crates/liboci-cli/src/checkpoint.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ pub struct Checkpoint {
5151
// pub pre_dump: bool,
5252
#[arg(long, default_value = "soft", value_parser = clap::builder::PossibleValuesParser::new(["ignore", "full", "strict", "soft"]))]
5353
pub manage_cgroups_mode: String,
54-
// TODO: Checkpoint a namespace, but don't save its properties
55-
// #[arg(long)]
56-
// pub empty_ns: bool,
54+
/// Checkpoint a namespace, but don't save its properties
55+
///
56+
/// Only `network` is accepted, and it applies even without this flag: youki does not manage
57+
/// network devices, so their external dependencies cannot be described to CRIU.
58+
#[arg(long, default_value = "network")]
59+
pub empty_ns: String,
5760
// TODO: Enable auto-deduplication
5861
// #[arg(long)]
5962
// pub auto_dedup: bool,

crates/youki/src/commands/checkpoint.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use liboci_cli::Checkpoint;
66

77
use crate::commands::load_container;
88

9+
const NETWORK_NS: &str = "network";
10+
911
pub fn checkpoint(args: Checkpoint, root_path: PathBuf) -> Result<()> {
1012
tracing::debug!("start checkpointing container {}", args.container_id);
1113
let mut container = load_container(root_path, &args.container_id)?;
@@ -20,12 +22,20 @@ pub fn checkpoint(args: Checkpoint, root_path: PathBuf) -> Result<()> {
2022
work_path: args.work_path,
2123
manage_cgroups_mode: parse_cgroups_mode(&args.manage_cgroups_mode)?,
2224
link_remap: args.link_remap,
25+
empty_net_ns: parse_empty_ns(&args.empty_ns)?,
2326
};
2427
container
2528
.checkpoint(&opts)
2629
.with_context(|| format!("failed to checkpoint container {}", args.container_id))
2730
}
2831

32+
fn parse_empty_ns(s: &str) -> Result<bool, anyhow::Error> {
33+
match s {
34+
NETWORK_NS => Ok(true),
35+
_ => Err(anyhow::anyhow!("namespace {s:?} is not supported")),
36+
}
37+
}
38+
2939
fn parse_cgroups_mode(s: &str) -> Result<rust_criu::CgMode, anyhow::Error> {
3040
match s {
3141
"ignore" => Ok(rust_criu::CgMode::IGNORE),
@@ -67,4 +77,22 @@ mod tests {
6777
assert!(parse_cgroups_mode("unknown").is_err());
6878
assert!(parse_cgroups_mode("").is_err());
6979
}
80+
81+
#[test]
82+
fn test_parse_empty_ns_ok() {
83+
assert!(matches!(parse_empty_ns("network"), Ok(true)));
84+
}
85+
86+
#[test]
87+
fn test_parse_empty_ns_ng() {
88+
for ns in [
89+
"pid", "mount", "ipc", "user", "uts", "cgroup", "Network", "",
90+
] {
91+
let err = parse_empty_ns(ns).unwrap_err();
92+
assert_eq!(
93+
err.to_string(),
94+
format!("namespace {ns:?} is not supported")
95+
);
96+
}
97+
}
7098
}

tests/contest/contest/src/tests/checkpoint_restore/mod.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,81 @@ fn checkpoint_and_restore_with_link_remap() -> TestResult {
15321532
}
15331533
}
15341534

1535+
fn checkpoint_and_restore_empty_net_ns() -> TestResult {
1536+
let ctx = match setup_cr_test(|_, _| {}) {
1537+
Ok(c) => c,
1538+
Err(e) => return e,
1539+
};
1540+
if let Err(e) = ctx.start() {
1541+
return e;
1542+
}
1543+
1544+
let id = &ctx.id;
1545+
let bundle = &ctx.bundle;
1546+
let image_dir = &ctx.image_dir;
1547+
let work_dir = &ctx.work_dir;
1548+
1549+
if let Err(e) = checkpoint_container(
1550+
bundle.path(),
1551+
id,
1552+
image_dir,
1553+
Some(work_dir),
1554+
&["--empty-ns", "network"],
1555+
&[],
1556+
) {
1557+
return TestResult::Failed(anyhow!("checkpoint with --empty-ns network failed: {e}"));
1558+
}
1559+
1560+
// netdev-<id>.img holds the network devices of a dumped namespace
1561+
match std::fs::read_dir(image_dir) {
1562+
Ok(entries) => {
1563+
let netdev_img = entries.flatten().find(|entry| {
1564+
let name = entry.file_name().to_string_lossy().into_owned();
1565+
name.starts_with("netdev-") && name.ends_with(".img")
1566+
});
1567+
if let Some(entry) = netdev_img {
1568+
return TestResult::Failed(anyhow!(
1569+
"{:?} was written: the network namespace was dumped although it must be emptied",
1570+
entry.path()
1571+
));
1572+
}
1573+
}
1574+
Err(e) => return TestResult::Failed(anyhow!("failed to read image-dir: {e}")),
1575+
}
1576+
1577+
if let Err(e) = wait_for_state(
1578+
id,
1579+
bundle,
1580+
WaitTarget::Deleted,
1581+
Duration::from_secs(5),
1582+
Duration::from_millis(100),
1583+
) {
1584+
return TestResult::Failed(anyhow!(
1585+
"container state still accessible after checkpoint: {e}"
1586+
));
1587+
}
1588+
1589+
if let Err(e) = restore_container(bundle.path(), id, image_dir, Some(work_dir), &[], &[]) {
1590+
return TestResult::Failed(anyhow!("restore failed: {e}"));
1591+
}
1592+
1593+
if let Err(e) = wait_for_state(
1594+
id,
1595+
bundle,
1596+
WaitTarget::Status(LifecycleStatus::Running),
1597+
Duration::from_secs(10),
1598+
Duration::from_millis(100),
1599+
) {
1600+
return TestResult::Failed(anyhow!("not running after restore: {e}"));
1601+
}
1602+
1603+
if let Err(e) = ping_container(bundle.path()) {
1604+
return TestResult::Failed(anyhow!("ping container failed after restore: {e}"));
1605+
}
1606+
1607+
TestResult::Passed
1608+
}
1609+
15351610
pub fn get_checkpoint_restore_tests() -> TestGroup {
15361611
let mut tg = TestGroup::new("checkpoint_restore");
15371612
// Run sequentially: CRIU uses global kernel resources and parallel
@@ -1600,5 +1675,9 @@ pub fn get_checkpoint_restore_tests() -> TestGroup {
16001675
"checkpoint_and_restore_with_link_remap",
16011676
checkpoint_and_restore_with_link_remap
16021677
))]);
1678+
tg.add(vec![Box::new(cr_test!(
1679+
"checkpoint_and_restore_empty_net_ns",
1680+
checkpoint_and_restore_empty_net_ns
1681+
))]);
16031682
tg
16041683
}

tests/contest/contest/src/tests/lifecycle/checkpoint.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,3 +663,46 @@ pub fn checkpoint_with_external_namespaces(project_path: &Path, id: &str) -> Tes
663663

664664
TestResult::Passed
665665
}
666+
667+
/// Checkpoint a container and verify that the content of its network namespace
668+
/// was not dumped.
669+
pub fn checkpoint_empty_net_ns(project_path: &Path, id: &str) -> TestResult {
670+
let (_temp_dir, image_path) = match create_checkpoint_image_dir() {
671+
Ok(v) => v,
672+
Err(e) => return e,
673+
};
674+
675+
let result = checkpoint(
676+
project_path,
677+
id,
678+
&image_path,
679+
vec!["--leave-running", "--empty-ns", "network"],
680+
None,
681+
);
682+
if !matches!(result, TestResult::Passed) {
683+
return result;
684+
}
685+
686+
// netdev-<id>.img holds the network devices of a dumped namespace
687+
let netdev_img = match std::fs::read_dir(&image_path) {
688+
Ok(entries) => entries
689+
.flatten()
690+
.find(|entry| {
691+
let name = entry.file_name().to_string_lossy().into_owned();
692+
name.starts_with("netdev-") && name.ends_with(".img")
693+
})
694+
.map(|entry| entry.path()),
695+
Err(e) => {
696+
return TestResult::Failed(anyhow::anyhow!("failed to read {:?}: {}", &image_path, e));
697+
}
698+
};
699+
700+
if let Some(img) = netdev_img {
701+
return TestResult::Failed(anyhow::anyhow!(
702+
"{:?} was written: the network namespace was dumped although it must be emptied",
703+
img,
704+
));
705+
}
706+
707+
TestResult::Passed
708+
}

tests/contest/contest/src/tests/lifecycle/container_lifecycle.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ impl ContainerLifecycle {
196196
)
197197
}
198198

199+
pub fn checkpoint_empty_net_ns(&self) -> TestResult {
200+
if !criu_installed() {
201+
return TestResult::Skipped("CRIU is not installed".to_string());
202+
}
203+
204+
checkpoint::checkpoint_empty_net_ns(self.project_path.path(), &self.container_id)
205+
}
206+
199207
// NOTE: The following two methods (`checkpoint_link_remap` and
200208
// `checkpoint_with_external_namespaces`) deviate from the pattern used by
201209
// the other checkpoint methods in this impl block. The typical pattern is:
@@ -333,6 +341,10 @@ impl TestableGroup for ContainerLifecycle {
333341
"checkpoint with cgroups-mode soft",
334342
self.checkpoint_manage_cgroups_mode_soft(),
335343
),
344+
(
345+
"checkpoint with empty network namespace",
346+
self.checkpoint_empty_net_ns(),
347+
),
336348
("checkpoint with link-remap", Self::checkpoint_link_remap()),
337349
(
338350
"checkpoint with tcp-skip-in-flight",
@@ -370,6 +382,10 @@ impl TestableGroup for ContainerLifecycle {
370382
"checkpoint with cgroups-mode soft",
371383
self.checkpoint_manage_cgroups_mode_soft(),
372384
)),
385+
"checkpoint_empty_net_ns" => ret.push((
386+
"checkpoint with empty network namespace",
387+
self.checkpoint_empty_net_ns(),
388+
)),
373389
"checkpoint_link_remap" => {
374390
ret.push(("checkpoint with link-remap", Self::checkpoint_link_remap()))
375391
}

0 commit comments

Comments
 (0)