Skip to content

Commit d957678

Browse files
authored
Merge pull request #1151 from cgwalters/tmpfiles-gen
tmpfiles: Support multiple generations
2 parents 55303ae + 53c1671 commit d957678

File tree

1 file changed

+104
-46
lines changed

1 file changed

+104
-46
lines changed

tmpfiles/src/lib.rs

Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ use std::ffi::{OsStr, OsString};
66
use std::fmt::Write as WriteFmt;
77
use std::io::{BufRead, BufReader, Write as StdWrite};
88
use std::iter::Peekable;
9+
use std::num::NonZeroUsize;
910
use std::os::unix::ffi::{OsStrExt, OsStringExt};
1011
use std::path::{Path, PathBuf};
1112

13+
use camino::Utf8PathBuf;
1214
use cap_std::fs::MetadataExt;
1315
use cap_std::fs::{Dir, Permissions, PermissionsExt};
1416
use cap_std_ext::cap_std;
@@ -18,7 +20,22 @@ use rustix::path::Arg;
1820
use thiserror::Error;
1921

2022
const TMPFILESD: &str = "usr/lib/tmpfiles.d";
21-
const BOOTC_GENERATED: &str = "bootc-autogenerated-var.conf";
23+
/// The path to the file we use for generation
24+
const BOOTC_GENERATED_PREFIX: &str = "bootc-autogenerated-var";
25+
26+
/// The number of times we've generated a tmpfiles.d
27+
#[derive(Debug, Default)]
28+
struct BootcTmpfilesGeneration(u32);
29+
30+
impl BootcTmpfilesGeneration {
31+
fn increment(&self) -> Self {
32+
Self(self.0 + 1)
33+
}
34+
35+
fn path(&self) -> Utf8PathBuf {
36+
format!("{TMPFILESD}/{BOOTC_GENERATED_PREFIX}-{}.conf", self.0).into()
37+
}
38+
}
2239

2340
/// An error when translating tmpfiles.d.
2441
#[derive(Debug, Error)]
@@ -219,13 +236,22 @@ pub(crate) fn translate_to_tmpfiles_d(
219236
Ok(bufwr)
220237
}
221238

239+
/// The result of a tmpfiles.d generation run
240+
#[derive(Debug, Default)]
241+
pub struct TmpfilesWrittenResult {
242+
/// Set if we generated entries; this is the count and the path.
243+
pub generated: Option<(NonZeroUsize, Utf8PathBuf)>,
244+
/// Total number of unsupported files that were skipped
245+
pub unsupported: usize,
246+
}
247+
222248
/// Translate the content of `/var` underneath the target root to use tmpfiles.d.
223249
pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
224250
rootfs: &Dir,
225251
users: &U,
226252
groups: &G,
227-
) -> Result<()> {
228-
let existing_tmpfiles = read_tmpfiles(rootfs)?;
253+
) -> Result<TmpfilesWrittenResult> {
254+
let (existing_tmpfiles, generation) = read_tmpfiles(rootfs)?;
229255

230256
// We should never have /var/run as a non-symlink. Don't recurse into it, it's
231257
// a hard error.
@@ -239,43 +265,55 @@ pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
239265
if !rootfs.try_exists(TMPFILESD)? {
240266
return Err(Error::MissingTmpfilesDir {});
241267
}
242-
let mode = Permissions::from_mode(0o644);
243-
rootfs.atomic_replace_with(
244-
Path::new(TMPFILESD).join(BOOTC_GENERATED),
245-
|bufwr| -> Result<()> {
246-
bufwr.get_mut().as_file_mut().set_permissions(mode)?;
247-
let mut prefix = PathBuf::from("/var");
248-
let mut entries = BTreeSet::new();
249-
let mut unsupported = Vec::new();
250-
convert_path_to_tmpfiles_d_recurse(
251-
&mut entries,
252-
&mut unsupported,
253-
users,
254-
groups,
255-
rootfs,
256-
&existing_tmpfiles,
257-
&mut prefix,
258-
false,
259-
)?;
260-
for line in entries {
261-
bufwr.write_all(line.as_bytes())?;
262-
writeln!(bufwr)?;
268+
269+
let mut entries = BTreeSet::new();
270+
let mut prefix = PathBuf::from("/var");
271+
let mut unsupported = Vec::new();
272+
convert_path_to_tmpfiles_d_recurse(
273+
&mut entries,
274+
&mut unsupported,
275+
users,
276+
groups,
277+
rootfs,
278+
&existing_tmpfiles,
279+
&mut prefix,
280+
false,
281+
)?;
282+
283+
// If there's no entries, don't write a file
284+
let Some(entries_count) = NonZeroUsize::new(entries.len()) else {
285+
return Ok(TmpfilesWrittenResult::default());
286+
};
287+
288+
let path = generation.path();
289+
// This should not exist
290+
assert!(!rootfs.try_exists(&path)?);
291+
292+
rootfs.atomic_replace_with(&path, |bufwr| -> Result<()> {
293+
let mode = Permissions::from_mode(0o644);
294+
bufwr.get_mut().as_file_mut().set_permissions(mode)?;
295+
296+
for line in entries.iter() {
297+
bufwr.write_all(line.as_bytes())?;
298+
writeln!(bufwr)?;
299+
}
300+
if !unsupported.is_empty() {
301+
let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5);
302+
for elt in samples {
303+
writeln!(bufwr, "# bootc ignored: {elt:?}")?;
263304
}
264-
if !unsupported.is_empty() {
265-
let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5);
266-
for elt in samples {
267-
writeln!(bufwr, "# bootc ignored: {elt:?}")?;
268-
}
269-
let rest = rest.count();
270-
if rest > 0 {
271-
writeln!(bufwr, "# bootc ignored: ...and {rest} more")?;
272-
}
305+
let rest = rest.count();
306+
if rest > 0 {
307+
writeln!(bufwr, "# bootc ignored: ...and {rest} more")?;
273308
}
274-
Ok(())
275-
},
276-
)?;
309+
}
310+
Ok(())
311+
})?;
277312

278-
Ok(())
313+
Ok(TmpfilesWrittenResult {
314+
generated: Some((entries_count, path)),
315+
unsupported: unsupported.len(),
316+
})
279317
}
280318

281319
/// Recursively explore target directory and translate content to tmpfiles.d entries. See
@@ -370,7 +408,7 @@ fn convert_path_to_tmpfiles_d_recurse<U: uzers::Users, G: uzers::Groups>(
370408

371409
/// Convert /var for the current root to use systemd tmpfiles.d.
372410
#[allow(unsafe_code)]
373-
pub fn convert_var_to_tmpfiles_current_root() -> Result<()> {
411+
pub fn convert_var_to_tmpfiles_current_root() -> Result<TmpfilesWrittenResult> {
374412
let rootfs = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
375413

376414
// See the docs for why this is unsafe
@@ -398,7 +436,7 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
398436
// See the docs for why this is unsafe
399437
let usergroups = unsafe { UsersSnapshot::new() };
400438

401-
let existing_tmpfiles = read_tmpfiles(&rootfs)?;
439+
let existing_tmpfiles = read_tmpfiles(&rootfs)?.0;
402440

403441
let mut prefix = PathBuf::from("/var");
404442
let mut tmpfiles = BTreeSet::new();
@@ -421,20 +459,28 @@ pub fn find_missing_tmpfiles_current_root() -> Result<TmpfilesResult> {
421459

422460
/// Read all tmpfiles.d entries in the target directory, and return a mapping
423461
/// from (file path) => (single tmpfiles.d entry line)
424-
fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
462+
fn read_tmpfiles(rootfs: &Dir) -> Result<(BTreeMap<PathBuf, String>, BootcTmpfilesGeneration)> {
425463
let Some(tmpfiles_dir) = rootfs.open_dir_optional(TMPFILESD)? else {
426464
return Ok(Default::default());
427465
};
428466
let mut result = BTreeMap::new();
467+
let mut generation = BootcTmpfilesGeneration::default();
429468
for entry in tmpfiles_dir.entries()? {
430469
let entry = entry?;
431470
let name = entry.file_name();
432-
let Some(extension) = Path::new(&name).extension() else {
471+
let (Some(stem), Some(extension)) =
472+
(Path::new(&name).file_stem(), Path::new(&name).extension())
473+
else {
433474
continue;
434475
};
435476
if extension != "conf" {
436477
continue;
437478
}
479+
if let Ok(s) = stem.as_str() {
480+
if s.starts_with(BOOTC_GENERATED_PREFIX) {
481+
generation = generation.increment();
482+
}
483+
}
438484
let r = BufReader::new(entry.open()?);
439485
for line in r.lines() {
440486
let line = line?;
@@ -445,7 +491,7 @@ fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
445491
result.insert(path.to_owned(), line);
446492
}
447493
}
448-
Ok(result)
494+
Ok((result, generation))
449495
}
450496

451497
fn tmpfiles_entry_get_path(line: &str) -> Result<PathBuf> {
@@ -541,7 +587,9 @@ mod tests {
541587

542588
var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
543589

544-
let autovar_path = &Path::new(TMPFILESD).join(BOOTC_GENERATED);
590+
// This is the first run
591+
let gen = BootcTmpfilesGeneration(0);
592+
let autovar_path = &gen.path();
545593
assert!(rootfs.try_exists(autovar_path).unwrap());
546594
let entries: Vec<String> = rootfs
547595
.read_to_string(autovar_path)
@@ -560,6 +608,17 @@ mod tests {
560608
similar_asserts::assert_eq!(entries, expected);
561609
assert!(!rootfs.try_exists("var/lib").unwrap());
562610

611+
// Now pretend we're doing a layered container build, and so we need
612+
// a new tmpfiles.d run
613+
rootfs.create_dir_all("var/lib/gen2-test")?;
614+
let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
615+
let wg = w.generated.as_ref().unwrap();
616+
assert_eq!(wg.0, NonZeroUsize::new(1).unwrap());
617+
assert_eq!(w.unsupported, 0);
618+
let gen = gen.increment();
619+
let autovar_path = &gen.path();
620+
assert_eq!(autovar_path, &wg.1);
621+
assert!(rootfs.try_exists(autovar_path).unwrap());
563622
Ok(())
564623
}
565624

@@ -575,10 +634,9 @@ mod tests {
575634
rootfs.create_dir_all("var/log/foo")?;
576635
rootfs.write("var/log/foo/foo.log", b"some other log")?;
577636

637+
let gen = BootcTmpfilesGeneration(0);
578638
var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
579-
let tmpfiles = rootfs
580-
.read_to_string(Path::new(TMPFILESD).join(BOOTC_GENERATED))
581-
.unwrap();
639+
let tmpfiles = rootfs.read_to_string(&gen.path()).unwrap();
582640
let ignored = tmpfiles
583641
.lines()
584642
.filter(|line| line.starts_with("# bootc ignored"))

0 commit comments

Comments
 (0)