Skip to content

Commit 5acf419

Browse files
committed
lints: Add --fix, support with tmpfiles
Signed-off-by: Colin Walters <[email protected]>
1 parent 2f028eb commit 5acf419

File tree

2 files changed

+105
-22
lines changed

2 files changed

+105
-22
lines changed

lib/src/cli.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,10 @@ pub(crate) enum ContainerOpts {
258258
#[clap(long)]
259259
list: bool,
260260

261+
/// Automatically apply fixes where possible.
262+
#[clap(long)]
263+
fix: bool,
264+
261265
/// Skip checking the targeted lints, by name. Use `--list` to discover the set
262266
/// of available lints.
263267
///
@@ -1051,11 +1055,17 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
10511055
rootfs,
10521056
fatal_warnings,
10531057
list,
1058+
fix,
10541059
skip,
10551060
} => {
10561061
if list {
10571062
return lints::lint_list(std::io::stdout().lock());
10581063
}
1064+
let fix = if fix {
1065+
lints::Applicability::Fix
1066+
} else {
1067+
lints::Applicability::Scan
1068+
};
10591069
let warnings = if fatal_warnings {
10601070
lints::WarningDisposition::FatalWarnings
10611071
} else {
@@ -1069,7 +1079,14 @@ async fn run_from_opt(opt: Opt) -> Result<()> {
10691079

10701080
let root = &Dir::open_ambient_dir(rootfs, cap_std::ambient_authority())?;
10711081
let skip = skip.iter().map(|s| s.as_str());
1072-
lints::lint(root, warnings, root_type, skip, std::io::stdout().lock())?;
1082+
lints::lint(
1083+
root,
1084+
warnings,
1085+
root_type,
1086+
fix,
1087+
skip,
1088+
std::io::stdout().lock(),
1089+
)?;
10731090
Ok(())
10741091
}
10751092
},

lib/src/lints.rs

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::env::consts::ARCH;
1010
use std::fmt::Write as WriteFmt;
1111
use std::os::unix::ffi::OsStrExt;
1212

13-
use anyhow::Result;
13+
use anyhow::{Context, Result};
1414
use bootc_utils::PathQuotedDisplay;
1515
use camino::{Utf8Path, Utf8PathBuf};
1616
use cap_std::fs::Dir;
@@ -21,7 +21,7 @@ use fn_error_context::context;
2121
use indoc::indoc;
2222
use linkme::distributed_slice;
2323
use ostree_ext::ostree_prepareroot;
24-
use serde::Serialize;
24+
use serde::{Deserialize, Serialize};
2525

2626
/// Reference to embedded default baseimage content that should exist.
2727
const BASEIMAGE_REF: &str = "usr/share/doc/bootc/baseimage/base";
@@ -57,12 +57,27 @@ impl LintError {
5757
}
5858
}
5959

60+
#[derive(Debug, Clone)]
61+
pub(crate) enum Applicability {
62+
Scan,
63+
Fix,
64+
}
65+
6066
type LintFn = fn(&Dir) -> LintResult;
67+
type LintFnExt = fn(&Dir, apply: Applicability) -> LintResult;
68+
69+
#[derive(Debug, Serialize)]
70+
#[serde(rename_all = "kebab-case")]
71+
enum LintFnTy {
72+
Scan(#[serde(skip)] LintFn),
73+
ScanOrFix(#[serde(skip)] LintFnExt),
74+
}
75+
6176
#[distributed_slice]
6277
pub(crate) static LINTS: [Lint];
6378

6479
/// The classification of a lint type.
65-
#[derive(Debug, Serialize)]
80+
#[derive(Debug, Deserialize, Serialize)]
6681
#[serde(rename_all = "kebab-case")]
6782
enum LintType {
6883
/// If this fails, it is known to be fatal - the system will not install or
@@ -78,7 +93,8 @@ pub(crate) enum WarningDisposition {
7893
FatalWarnings,
7994
}
8095

81-
#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq)]
96+
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
97+
#[serde(rename_all = "kebab-case")]
8298
pub(crate) enum RootType {
8399
Running,
84100
Alternative,
@@ -90,8 +106,7 @@ struct Lint {
90106
name: &'static str,
91107
#[serde(rename = "type")]
92108
ty: LintType,
93-
#[serde(skip)]
94-
f: LintFn,
109+
apply: LintFnTy,
95110
description: &'static str,
96111
// Set if this only applies to a specific root type.
97112
#[serde(skip_serializing_if = "Option::is_none")]
@@ -107,7 +122,7 @@ impl Lint {
107122
Lint {
108123
name: name,
109124
ty: LintType::Fatal,
110-
f: f,
125+
apply: LintFnTy::Scan(f),
111126
description: description,
112127
root_type: None,
113128
}
@@ -121,12 +136,28 @@ impl Lint {
121136
Lint {
122137
name: name,
123138
ty: LintType::Warning,
124-
f: f,
139+
apply: LintFnTy::Scan(f),
140+
description: description,
141+
root_type: None,
142+
}
143+
}
144+
145+
pub(crate) const fn new(
146+
name: &'static str,
147+
description: &'static str,
148+
f: LintFnTy,
149+
ty: LintType,
150+
) -> Self {
151+
Lint {
152+
name,
125153
description: description,
154+
ty,
155+
apply: f,
126156
root_type: None,
127157
}
128158
}
129159

160+
/// Set the root filesystem type
130161
const fn set_root_type(mut self, v: RootType) -> Self {
131162
self.root_type = Some(v);
132163
self
@@ -150,6 +181,7 @@ struct LintExecutionResult {
150181
fn lint_inner<'skip>(
151182
root: &Dir,
152183
root_type: RootType,
184+
fix: Applicability,
153185
skip: impl IntoIterator<Item = &'skip str>,
154186
mut output: impl std::io::Write,
155187
) -> Result<LintExecutionResult> {
@@ -173,11 +205,15 @@ fn lint_inner<'skip>(
173205
}
174206
}
175207

176-
let r = match (lint.f)(&root) {
177-
Ok(r) => r,
178-
Err(e) => anyhow::bail!("Unexpected runtime error running lint {name}: {e}"),
179-
};
208+
// Call the lint function, and propagate the first level of error (runtime errors)
209+
// immediately; we don't continue on and execute any other lints.
210+
let r = match lint.apply {
211+
LintFnTy::ScanOrFix(f) => f(&root, fix.clone()),
212+
LintFnTy::Scan(f) => f(&root),
213+
}
214+
.with_context(|| format!("Runtime error executing lint {name}"))?;
180215

216+
// The lint executed OK, but may have successfully detected problems.
181217
if let Err(e) = r {
182218
match lint.ty {
183219
LintType::Fatal => {
@@ -212,10 +248,11 @@ pub(crate) fn lint<'skip>(
212248
root: &Dir,
213249
warning_disposition: WarningDisposition,
214250
root_type: RootType,
251+
fix: Applicability,
215252
skip: impl IntoIterator<Item = &'skip str>,
216253
mut output: impl std::io::Write,
217254
) -> Result<()> {
218-
let r = lint_inner(root, root_type, skip, &mut output)?;
255+
let r = lint_inner(root, root_type, fix, skip, &mut output)?;
219256
writeln!(output, "Checks passed: {}", r.passed)?;
220257
if r.skipped > 0 {
221258
writeln!(output, "Checks skipped: {}", r.skipped)?;
@@ -503,7 +540,7 @@ fn check_varlog(root: &Dir) -> LintResult {
503540
}
504541

505542
#[distributed_slice(LINTS)]
506-
static LINT_VAR_TMPFILES: Lint = Lint::new_warning(
543+
static LINT_VAR_TMPFILES: Lint = Lint::new(
507544
"var-tmpfiles",
508545
indoc! { r#"
509546
Check for content in /var that does not have corresponding systemd tmpfiles.d entries.
@@ -514,10 +551,27 @@ Instead, it's recommended to have /var effectively empty in the container image,
514551
and use systemd tmpfiles.d to generate empty directories and compatibility symbolic links
515552
as part of each boot.
516553
"#},
517-
check_var_tmpfiles,
554+
LintFnTy::ScanOrFix(check_var_tmpfiles),
555+
LintType::Warning,
518556
)
519557
.set_root_type(RootType::Running);
520-
fn check_var_tmpfiles(_root: &Dir) -> LintResult {
558+
fn check_var_tmpfiles(_root: &Dir, fix: Applicability) -> LintResult {
559+
// Handle the fix case first by doing the conversion where we can
560+
match fix {
561+
Applicability::Fix => {
562+
let r = bootc_tmpfiles::convert_var_to_tmpfiles_current_root()?;
563+
if let Some((count, path)) = r.generated {
564+
println!(
565+
"tmpfiles.d: Generated: {} with entries: {}",
566+
PathQuotedDisplay::new(&path),
567+
count
568+
);
569+
} else {
570+
println!("tmpfiles.d: Nogenerated");
571+
}
572+
}
573+
Applicability::Scan => {}
574+
};
521575
let r = bootc_tmpfiles::find_missing_tmpfiles_current_root()?;
522576
if r.tmpfiles.is_empty() && r.unsupported.is_empty() {
523577
return lint_ok();
@@ -680,10 +734,10 @@ mod tests {
680734
let mut out = Vec::new();
681735
let warnings = WarningDisposition::FatalWarnings;
682736
let root_type = RootType::Alternative;
683-
lint(root, warnings, root_type, [], &mut out).unwrap();
737+
lint(root, warnings, root_type, Applicability::Scan, [], &mut out).unwrap();
684738
root.create_dir_all("var/run/foo")?;
685739
let mut out = Vec::new();
686-
assert!(lint(root, warnings, root_type, [], &mut out).is_err());
740+
assert!(lint(root, warnings, root_type, Applicability::Scan, [], &mut out).is_err());
687741
Ok(())
688742
}
689743

@@ -694,14 +748,14 @@ mod tests {
694748
// Verify that all lints run
695749
let mut out = Vec::new();
696750
let root_type = RootType::Alternative;
697-
let r = lint_inner(root, root_type, [], &mut out).unwrap();
751+
let r = lint_inner(root, root_type, Applicability::Scan, [], &mut out).unwrap();
698752
let running_only_lints = LINTS.len().checked_sub(*ALTROOT_LINTS).unwrap();
699753
assert_eq!(r.passed, *ALTROOT_LINTS);
700754
assert_eq!(r.fatal, 0);
701755
assert_eq!(r.skipped, running_only_lints);
702756
assert_eq!(r.warnings, 0);
703757

704-
let r = lint_inner(root, root_type, ["var-log"], &mut out).unwrap();
758+
let r = lint_inner(root, root_type, Applicability::Scan, ["var-log"], &mut out).unwrap();
705759
// Trigger a failure in var-log
706760
root.create_dir_all("var/log/dnf")?;
707761
root.write("var/log/dnf/dnf.log", b"dummy dnf log")?;
@@ -712,7 +766,7 @@ mod tests {
712766

713767
// But verify that not skipping it results in a warning
714768
let mut out = Vec::new();
715-
let r = lint_inner(root, root_type, [], &mut out).unwrap();
769+
let r = lint_inner(root, root_type, Applicability::Scan, [], &mut out).unwrap();
716770
assert_eq!(r.passed, ALTROOT_LINTS.checked_sub(1).unwrap());
717771
assert_eq!(r.fatal, 0);
718772
assert_eq!(r.skipped, running_only_lints);
@@ -928,5 +982,17 @@ mod tests {
928982
lint_list(&mut r).unwrap();
929983
let lints: Vec<serde_yaml::Value> = serde_yaml::from_slice(&r).unwrap();
930984
assert_eq!(lints.len(), LINTS.len());
985+
let tmpfiles = lints
986+
.iter()
987+
.find(|v| {
988+
let v = v.as_mapping().unwrap();
989+
let name = v.get("name").unwrap();
990+
return name == "var-tmpfiles";
991+
})
992+
.unwrap()
993+
.as_mapping()
994+
.unwrap();
995+
let ty = tmpfiles.get("apply").unwrap();
996+
assert_eq!(ty.as_str().unwrap(), "scan-or-fix");
931997
}
932998
}

0 commit comments

Comments
 (0)