Skip to content

Commit

Permalink
Merge pull request #1151 from cgwalters/tmpfiles-gen
Browse files Browse the repository at this point in the history
tmpfiles: Support multiple generations
  • Loading branch information
cgwalters authored Feb 27, 2025
2 parents 55303ae + 53c1671 commit d957678
Showing 1 changed file with 104 additions and 46 deletions.
150 changes: 104 additions & 46 deletions tmpfiles/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ use std::ffi::{OsStr, OsString};
use std::fmt::Write as WriteFmt;
use std::io::{BufRead, BufReader, Write as StdWrite};
use std::iter::Peekable;
use std::num::NonZeroUsize;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};

use camino::Utf8PathBuf;
use cap_std::fs::MetadataExt;
use cap_std::fs::{Dir, Permissions, PermissionsExt};
use cap_std_ext::cap_std;
Expand All @@ -18,7 +20,22 @@ use rustix::path::Arg;
use thiserror::Error;

const TMPFILESD: &str = "usr/lib/tmpfiles.d";
const BOOTC_GENERATED: &str = "bootc-autogenerated-var.conf";
/// The path to the file we use for generation
const BOOTC_GENERATED_PREFIX: &str = "bootc-autogenerated-var";

/// The number of times we've generated a tmpfiles.d
#[derive(Debug, Default)]
struct BootcTmpfilesGeneration(u32);

impl BootcTmpfilesGeneration {
fn increment(&self) -> Self {
Self(self.0 + 1)
}

fn path(&self) -> Utf8PathBuf {
format!("{TMPFILESD}/{BOOTC_GENERATED_PREFIX}-{}.conf", self.0).into()
}
}

/// An error when translating tmpfiles.d.
#[derive(Debug, Error)]
Expand Down Expand Up @@ -219,13 +236,22 @@ pub(crate) fn translate_to_tmpfiles_d(
Ok(bufwr)
}

/// The result of a tmpfiles.d generation run
#[derive(Debug, Default)]
pub struct TmpfilesWrittenResult {
/// Set if we generated entries; this is the count and the path.
pub generated: Option<(NonZeroUsize, Utf8PathBuf)>,
/// Total number of unsupported files that were skipped
pub unsupported: usize,
}

/// Translate the content of `/var` underneath the target root to use tmpfiles.d.
pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
rootfs: &Dir,
users: &U,
groups: &G,
) -> Result<()> {
let existing_tmpfiles = read_tmpfiles(rootfs)?;
) -> Result<TmpfilesWrittenResult> {
let (existing_tmpfiles, generation) = read_tmpfiles(rootfs)?;

// We should never have /var/run as a non-symlink. Don't recurse into it, it's
// a hard error.
Expand All @@ -239,43 +265,55 @@ pub fn var_to_tmpfiles<U: uzers::Users, G: uzers::Groups>(
if !rootfs.try_exists(TMPFILESD)? {
return Err(Error::MissingTmpfilesDir {});
}
let mode = Permissions::from_mode(0o644);
rootfs.atomic_replace_with(
Path::new(TMPFILESD).join(BOOTC_GENERATED),
|bufwr| -> Result<()> {
bufwr.get_mut().as_file_mut().set_permissions(mode)?;
let mut prefix = PathBuf::from("/var");
let mut entries = BTreeSet::new();
let mut unsupported = Vec::new();
convert_path_to_tmpfiles_d_recurse(
&mut entries,
&mut unsupported,
users,
groups,
rootfs,
&existing_tmpfiles,
&mut prefix,
false,
)?;
for line in entries {
bufwr.write_all(line.as_bytes())?;
writeln!(bufwr)?;

let mut entries = BTreeSet::new();
let mut prefix = PathBuf::from("/var");
let mut unsupported = Vec::new();
convert_path_to_tmpfiles_d_recurse(
&mut entries,
&mut unsupported,
users,
groups,
rootfs,
&existing_tmpfiles,
&mut prefix,
false,
)?;

// If there's no entries, don't write a file
let Some(entries_count) = NonZeroUsize::new(entries.len()) else {
return Ok(TmpfilesWrittenResult::default());
};

let path = generation.path();
// This should not exist
assert!(!rootfs.try_exists(&path)?);

rootfs.atomic_replace_with(&path, |bufwr| -> Result<()> {
let mode = Permissions::from_mode(0o644);
bufwr.get_mut().as_file_mut().set_permissions(mode)?;

for line in entries.iter() {
bufwr.write_all(line.as_bytes())?;
writeln!(bufwr)?;
}
if !unsupported.is_empty() {
let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5);
for elt in samples {
writeln!(bufwr, "# bootc ignored: {elt:?}")?;
}
if !unsupported.is_empty() {
let (samples, rest) = bootc_utils::iterator_split(unsupported.iter(), 5);
for elt in samples {
writeln!(bufwr, "# bootc ignored: {elt:?}")?;
}
let rest = rest.count();
if rest > 0 {
writeln!(bufwr, "# bootc ignored: ...and {rest} more")?;
}
let rest = rest.count();
if rest > 0 {
writeln!(bufwr, "# bootc ignored: ...and {rest} more")?;
}
Ok(())
},
)?;
}
Ok(())
})?;

Ok(())
Ok(TmpfilesWrittenResult {
generated: Some((entries_count, path)),
unsupported: unsupported.len(),
})
}

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

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

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

let existing_tmpfiles = read_tmpfiles(&rootfs)?;
let existing_tmpfiles = read_tmpfiles(&rootfs)?.0;

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

/// Read all tmpfiles.d entries in the target directory, and return a mapping
/// from (file path) => (single tmpfiles.d entry line)
fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
fn read_tmpfiles(rootfs: &Dir) -> Result<(BTreeMap<PathBuf, String>, BootcTmpfilesGeneration)> {
let Some(tmpfiles_dir) = rootfs.open_dir_optional(TMPFILESD)? else {
return Ok(Default::default());
};
let mut result = BTreeMap::new();
let mut generation = BootcTmpfilesGeneration::default();
for entry in tmpfiles_dir.entries()? {
let entry = entry?;
let name = entry.file_name();
let Some(extension) = Path::new(&name).extension() else {
let (Some(stem), Some(extension)) =
(Path::new(&name).file_stem(), Path::new(&name).extension())
else {
continue;
};
if extension != "conf" {
continue;
}
if let Ok(s) = stem.as_str() {
if s.starts_with(BOOTC_GENERATED_PREFIX) {
generation = generation.increment();
}
}
let r = BufReader::new(entry.open()?);
for line in r.lines() {
let line = line?;
Expand All @@ -445,7 +491,7 @@ fn read_tmpfiles(rootfs: &Dir) -> Result<BTreeMap<PathBuf, String>> {
result.insert(path.to_owned(), line);
}
}
Ok(result)
Ok((result, generation))
}

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

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

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

// Now pretend we're doing a layered container build, and so we need
// a new tmpfiles.d run
rootfs.create_dir_all("var/lib/gen2-test")?;
let w = var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
let wg = w.generated.as_ref().unwrap();
assert_eq!(wg.0, NonZeroUsize::new(1).unwrap());
assert_eq!(w.unsupported, 0);
let gen = gen.increment();
let autovar_path = &gen.path();
assert_eq!(autovar_path, &wg.1);
assert!(rootfs.try_exists(autovar_path).unwrap());
Ok(())
}

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

let gen = BootcTmpfilesGeneration(0);
var_to_tmpfiles(rootfs, userdb, userdb).unwrap();
let tmpfiles = rootfs
.read_to_string(Path::new(TMPFILESD).join(BOOTC_GENERATED))
.unwrap();
let tmpfiles = rootfs.read_to_string(&gen.path()).unwrap();
let ignored = tmpfiles
.lines()
.filter(|line| line.starts_with("# bootc ignored"))
Expand Down

0 comments on commit d957678

Please sign in to comment.