|
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