|
2 | 2 | //! |
3 | 3 | //! This module implements `bootc container lint`. |
4 | 4 |
|
| 5 | +use std::collections::BTreeSet; |
5 | 6 | use std::env::consts::ARCH; |
6 | 7 | use std::os::unix::ffi::OsStrExt; |
7 | 8 |
|
8 | 9 | use anyhow::{Context, Result}; |
| 10 | +use camino::{Utf8Path, Utf8PathBuf}; |
9 | 11 | use cap_std::fs::Dir; |
10 | 12 | use cap_std_ext::cap_std; |
| 13 | +use cap_std_ext::cap_std::fs::MetadataExt; |
11 | 14 | use cap_std_ext::dirext::CapStdExtDirExt as _; |
12 | 15 | use fn_error_context::context; |
13 | 16 |
|
@@ -53,6 +56,8 @@ enum LintType { |
53 | 56 | /// If this fails, it is known to be fatal - the system will not install or |
54 | 57 | /// is effectively guaranteed to fail at runtime. |
55 | 58 | Fatal, |
| 59 | + /// This is not a fatal problem, but something you likely want to fix. |
| 60 | + Warning, |
56 | 61 | } |
57 | 62 |
|
58 | 63 | struct Lint { |
@@ -93,31 +98,54 @@ const LINTS: &[Lint] = &[ |
93 | 98 | ty: LintType::Fatal, |
94 | 99 | f: check_baseimage_root, |
95 | 100 | }, |
| 101 | + Lint { |
| 102 | + name: "var-log", |
| 103 | + ty: LintType::Warning, |
| 104 | + f: check_varlog, |
| 105 | + }, |
96 | 106 | ]; |
97 | 107 |
|
98 | 108 | /// check for the existence of the /var/run directory |
99 | 109 | /// if it exists we need to check that it links to /run if not error |
100 | 110 | /// if it does not exist error. |
101 | 111 | #[context("Linting")] |
102 | | -pub(crate) fn lint(root: &Dir) -> Result<()> { |
| 112 | +pub(crate) fn lint(root: &Dir, fatal_warnings: bool) -> Result<()> { |
103 | 113 | let mut fatal = 0usize; |
| 114 | + let mut warnings = 0usize; |
104 | 115 | let mut passed = 0usize; |
105 | 116 | for lint in LINTS { |
106 | 117 | let name = lint.name; |
107 | 118 | let r = match (lint.f)(&root) { |
108 | 119 | Ok(r) => r, |
109 | 120 | Err(e) => anyhow::bail!("Unexpected runtime error running lint {name}: {e}"), |
110 | 121 | }; |
| 122 | + |
111 | 123 | if let Err(e) = r { |
112 | | - eprintln!("Failed lint: {name}: {e}"); |
113 | | - fatal += 1; |
| 124 | + match lint.ty { |
| 125 | + LintType::Fatal => { |
| 126 | + eprintln!("Failed lint: {name}: {e}"); |
| 127 | + fatal += 1; |
| 128 | + } |
| 129 | + LintType::Warning => { |
| 130 | + eprintln!("Lint warning: {name}: {e}"); |
| 131 | + warnings += 1; |
| 132 | + } |
| 133 | + } |
114 | 134 | } else { |
115 | 135 | // We'll be quiet for now |
116 | 136 | tracing::debug!("OK {name} (type={:?})", lint.ty); |
117 | 137 | passed += 1; |
118 | 138 | } |
119 | 139 | } |
120 | 140 | println!("Checks passed: {passed}"); |
| 141 | + let fatal = if fatal_warnings { |
| 142 | + fatal + warnings |
| 143 | + } else { |
| 144 | + fatal |
| 145 | + }; |
| 146 | + if warnings > 0 { |
| 147 | + println!("Warnings: {warnings}"); |
| 148 | + } |
121 | 149 | if fatal > 0 { |
122 | 150 | anyhow::bail!("Checks failed: {fatal}") |
123 | 151 | } |
@@ -239,6 +267,47 @@ fn check_baseimage_root(dir: &Dir) -> LintResult { |
239 | 267 | lint_ok() |
240 | 268 | } |
241 | 269 |
|
| 270 | +fn collect_nonempty_regfiles( |
| 271 | + root: &Dir, |
| 272 | + path: &Utf8Path, |
| 273 | + out: &mut BTreeSet<Utf8PathBuf>, |
| 274 | +) -> Result<()> { |
| 275 | + for entry in root.entries_utf8()? { |
| 276 | + let entry = entry?; |
| 277 | + let ty = entry.file_type()?; |
| 278 | + let path = path.join(entry.file_name()?); |
| 279 | + if ty.is_file() { |
| 280 | + let meta = entry.metadata()?; |
| 281 | + if meta.size() > 0 { |
| 282 | + out.insert(path); |
| 283 | + } |
| 284 | + } else if ty.is_dir() { |
| 285 | + let d = entry.open_dir()?; |
| 286 | + collect_nonempty_regfiles(d.as_cap_std(), &path, out)?; |
| 287 | + } |
| 288 | + } |
| 289 | + Ok(()) |
| 290 | +} |
| 291 | + |
| 292 | +fn check_varlog(root: &Dir) -> LintResult { |
| 293 | + let Some(d) = root.open_dir_optional("var/log")? else { |
| 294 | + return lint_ok(); |
| 295 | + }; |
| 296 | + let mut nonempty_regfiles = BTreeSet::new(); |
| 297 | + collect_nonempty_regfiles(&d, "/var/log".into(), &mut nonempty_regfiles)?; |
| 298 | + let mut nonempty_regfiles = nonempty_regfiles.into_iter(); |
| 299 | + let Some(first) = nonempty_regfiles.next() else { |
| 300 | + return lint_ok(); |
| 301 | + }; |
| 302 | + let others = nonempty_regfiles.len(); |
| 303 | + let others = if others > 0 { |
| 304 | + format!(" (and {others} more)") |
| 305 | + } else { |
| 306 | + "".into() |
| 307 | + }; |
| 308 | + lint_err(format!("Found non-empty logfile: {first}{others}")) |
| 309 | +} |
| 310 | + |
242 | 311 | #[cfg(test)] |
243 | 312 | mod tests { |
244 | 313 | use super::*; |
@@ -301,6 +370,38 @@ mod tests { |
301 | 370 | Ok(()) |
302 | 371 | } |
303 | 372 |
|
| 373 | + #[test] |
| 374 | + fn test_varlog() -> Result<()> { |
| 375 | + let root = &fixture()?; |
| 376 | + check_varlog(root).unwrap().unwrap(); |
| 377 | + root.create_dir_all("var/log")?; |
| 378 | + check_varlog(root).unwrap().unwrap(); |
| 379 | + root.symlink_contents("../../usr/share/doc/systemd/README.logs", "var/log/README")?; |
| 380 | + check_varlog(root).unwrap().unwrap(); |
| 381 | + |
| 382 | + root.atomic_write("var/log/somefile.log", "log contents")?; |
| 383 | + let Err(e) = check_varlog(root).unwrap() else { |
| 384 | + unreachable!() |
| 385 | + }; |
| 386 | + assert_eq!( |
| 387 | + e.to_string(), |
| 388 | + "Found non-empty logfile: /var/log/somefile.log" |
| 389 | + ); |
| 390 | + |
| 391 | + root.create_dir_all("var/log/someproject")?; |
| 392 | + root.atomic_write("var/log/someproject/audit.log", "audit log")?; |
| 393 | + root.atomic_write("var/log/someproject/info.log", "info")?; |
| 394 | + let Err(e) = check_varlog(root).unwrap() else { |
| 395 | + unreachable!() |
| 396 | + }; |
| 397 | + assert_eq!( |
| 398 | + e.to_string(), |
| 399 | + "Found non-empty logfile: /var/log/somefile.log (and 2 more)" |
| 400 | + ); |
| 401 | + |
| 402 | + Ok(()) |
| 403 | + } |
| 404 | + |
304 | 405 | #[test] |
305 | 406 | fn test_non_utf8() { |
306 | 407 | use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; |
|
0 commit comments