Skip to content

Commit ae9169f

Browse files
committed
build-sys: Consistently use RUN --network=none and add check
Ensure all RUN instructions after the "external dependency cutoff point" marker include `--network=none` right after `RUN`. This enforces that external dependencies are clearly delineated in the early stages of the Dockerfile. The check is part of `cargo xtask check-buildsys` and includes unit tests. Assisted-by: OpenCode (Sonnet 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 47728dd commit ae9169f

File tree

3 files changed

+177
-53
lines changed

3 files changed

+177
-53
lines changed

Dockerfile

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ ENV container=oci
6363
STOPSIGNAL SIGRTMIN+3
6464
CMD ["/sbin/init"]
6565

66+
# -------------
67+
# external dependency cutoff point:
6668
# NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure
6769
# all external dependencies are clearly delineated.
70+
# This is verified in `cargo xtask check-buildsys`.
71+
# -------------
6872

6973
FROM buildroot as build
7074
# Version for RPM build (optional, computed from git in Justfile)
@@ -73,7 +77,7 @@ ARG pkgversion
7377
ARG SOURCE_DATE_EPOCH
7478
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}
7579
# Build RPM directly from source, using cached target directory
76-
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm
80+
RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm
7781

7882
FROM buildroot as sdboot-signed
7983
# The secureboot key and cert are passed via Justfile
@@ -89,11 +93,11 @@ FROM build as units
8993
# A place that we're more likely to be able to set xattrs
9094
VOLUME /var/tmp
9195
ENV TMPDIR=/var/tmp
92-
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make install-unit-tests
96+
RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make install-unit-tests
9397

9498
# This just does syntax checking
9599
FROM buildroot as validate
96-
RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome --network=none make validate
100+
RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome make validate
97101

98102
# Common base for final images: configures variant, rootfs, and injects extra content
99103
FROM base as final-common
@@ -103,13 +107,12 @@ RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \
103107
--mount=type=bind,from=sdboot-signed,target=/run/sdboot-signed \
104108
/run/packaging/configure-variant "${variant}"
105109
ARG rootfs=""
106-
RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}"
110+
RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/configure-rootfs "${variant}" "${rootfs}"
107111
COPY --from=packaging /usr-extras/ /usr/
108112

109113
# Final target: installs pre-built packages from /run/packages volume mount.
110114
# Use with: podman build --target=final -v path/to/packages:/run/packages:ro
111115
FROM final-common as final
112-
RUN --mount=type=bind,from=packaging,target=/run/packaging \
113-
--network=none \
116+
RUN --network=none --mount=type=bind,from=packaging,target=/run/packaging \
114117
/run/packaging/install-rpm-and-setup /run/packages
115-
RUN bootc container lint --fatal-warnings
118+
RUN --network=none bootc container lint --fatal-warnings

crates/xtask/src/buildsys.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! Build system validation checks.
2+
3+
use std::collections::BTreeMap;
4+
5+
use anyhow::{Context, Result};
6+
use camino::{Utf8Path, Utf8PathBuf};
7+
use fn_error_context::context;
8+
use xshell::{cmd, Shell};
9+
10+
const DOCKERFILE_NETWORK_CUTOFF: &str = "external dependency cutoff point";
11+
12+
/// Check build system properties
13+
///
14+
/// - Reproducible builds for the RPM
15+
/// - Dockerfile network isolation after cutoff point
16+
#[context("Checking build system")]
17+
pub fn check_buildsys(sh: &Shell, dockerfile_path: &Utf8Path) -> Result<()> {
18+
check_package_reproducibility(sh)?;
19+
check_dockerfile_network_isolation(dockerfile_path)?;
20+
Ok(())
21+
}
22+
23+
/// Verify that consecutive `just package` invocations produce identical RPM checksums.
24+
#[context("Checking package reproducibility")]
25+
fn check_package_reproducibility(sh: &Shell) -> Result<()> {
26+
println!("Checking reproducible builds...");
27+
// Helper to compute SHA256 of bootc RPMs in target/packages/
28+
fn get_rpm_checksums(sh: &Shell) -> Result<BTreeMap<String, String>> {
29+
// Find bootc*.rpm files in target/packages/
30+
let packages_dir = Utf8Path::new("target/packages");
31+
let mut rpm_files: Vec<Utf8PathBuf> = Vec::new();
32+
for entry in std::fs::read_dir(packages_dir).context("Reading target/packages")? {
33+
let entry = entry?;
34+
let path = Utf8PathBuf::try_from(entry.path())?;
35+
if path.extension() == Some("rpm") {
36+
rpm_files.push(path);
37+
}
38+
}
39+
40+
assert!(!rpm_files.is_empty());
41+
42+
let mut checksums = BTreeMap::new();
43+
for rpm_path in &rpm_files {
44+
let output = cmd!(sh, "sha256sum {rpm_path}").read()?;
45+
let (hash, filename) = output
46+
.split_once(" ")
47+
.with_context(|| format!("failed to parse sha256sum output: '{}'", output))?;
48+
checksums.insert(filename.to_owned(), hash.to_owned());
49+
}
50+
Ok(checksums)
51+
}
52+
53+
cmd!(sh, "just package").run()?;
54+
let first_checksums = get_rpm_checksums(sh)?;
55+
cmd!(sh, "just package").run()?;
56+
let second_checksums = get_rpm_checksums(sh)?;
57+
58+
itertools::assert_equal(first_checksums, second_checksums);
59+
println!("ok package reproducibility");
60+
61+
Ok(())
62+
}
63+
64+
/// Verify that all RUN instructions in the Dockerfile after the network cutoff
65+
/// point include `--network=none`.
66+
#[context("Checking Dockerfile network isolation")]
67+
fn check_dockerfile_network_isolation(dockerfile_path: &Utf8Path) -> Result<()> {
68+
println!("Checking Dockerfile network isolation...");
69+
let dockerfile = std::fs::read_to_string(dockerfile_path).context("Reading Dockerfile")?;
70+
verify_dockerfile_network_isolation(&dockerfile)?;
71+
println!("ok Dockerfile network isolation");
72+
Ok(())
73+
}
74+
75+
const RUN_NETWORK_NONE: &str = "RUN --network=none";
76+
77+
/// Verify that all RUN instructions after the network cutoff marker start with
78+
/// `RUN --network=none`.
79+
///
80+
/// Returns Ok(()) if all RUN instructions comply, or an error listing violations.
81+
pub fn verify_dockerfile_network_isolation(dockerfile: &str) -> Result<()> {
82+
// Find the cutoff point
83+
let cutoff_line = dockerfile
84+
.lines()
85+
.position(|line| line.contains(DOCKERFILE_NETWORK_CUTOFF))
86+
.ok_or_else(|| {
87+
anyhow::anyhow!(
88+
"Dockerfile missing '{}' marker comment",
89+
DOCKERFILE_NETWORK_CUTOFF
90+
)
91+
})?;
92+
93+
// Check all RUN instructions after the cutoff point
94+
let mut errors = Vec::new();
95+
96+
for (idx, line) in dockerfile.lines().enumerate().skip(cutoff_line + 1) {
97+
let line_num = idx + 1; // 1-based line numbers
98+
let trimmed = line.trim();
99+
100+
// Check if this is a RUN instruction
101+
if trimmed.starts_with("RUN ") {
102+
// Must start with exactly "RUN --network=none"
103+
if !trimmed.starts_with(RUN_NETWORK_NONE) {
104+
errors.push(format!(
105+
" line {}: RUN instruction must start with `{}`",
106+
line_num, RUN_NETWORK_NONE
107+
));
108+
}
109+
}
110+
}
111+
112+
if !errors.is_empty() {
113+
anyhow::bail!(
114+
"Dockerfile has RUN instructions after '{}' that don't start with `{}`:\n{}",
115+
DOCKERFILE_NETWORK_CUTOFF,
116+
RUN_NETWORK_NONE,
117+
errors.join("\n")
118+
);
119+
}
120+
121+
Ok(())
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use super::*;
127+
128+
#[test]
129+
fn test_network_isolation_valid() {
130+
let dockerfile = r#"
131+
FROM base
132+
RUN echo "before cutoff, no network restriction needed"
133+
# external dependency cutoff point
134+
RUN --network=none echo "good"
135+
RUN --network=none --mount=type=bind,from=foo,target=/bar some-command
136+
"#;
137+
verify_dockerfile_network_isolation(dockerfile).unwrap();
138+
}
139+
140+
#[test]
141+
fn test_network_isolation_missing_flag() {
142+
let dockerfile = r#"
143+
FROM base
144+
# external dependency cutoff point
145+
RUN --network=none echo "good"
146+
RUN echo "bad - missing network flag"
147+
"#;
148+
let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err();
149+
let msg = err.to_string();
150+
assert!(msg.contains("line 5"), "error should mention line 5: {msg}");
151+
}
152+
153+
#[test]
154+
fn test_network_isolation_wrong_position() {
155+
// --network=none must come immediately after RUN
156+
let dockerfile = r#"
157+
FROM base
158+
# external dependency cutoff point
159+
RUN --mount=type=bind,from=foo,target=/bar --network=none echo "bad"
160+
"#;
161+
let err = verify_dockerfile_network_isolation(dockerfile).unwrap_err();
162+
let msg = err.to_string();
163+
assert!(msg.contains("line 4"), "error should mention line 4: {msg}");
164+
}
165+
}

crates/xtask/src/xtask.rs

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use clap::{Args, Parser, Subcommand};
1414
use fn_error_context::context;
1515
use xshell::{cmd, Shell};
1616

17+
mod buildsys;
1718
mod man;
1819
mod tmt;
1920

@@ -137,7 +138,7 @@ fn try_main() -> Result<()> {
137138
Commands::Spec => spec(&sh),
138139
Commands::RunTmt(args) => tmt::run_tmt(&sh, &args),
139140
Commands::TmtProvision(args) => tmt::tmt_provision(&sh, &args),
140-
Commands::CheckBuildsys => check_buildsys(&sh),
141+
Commands::CheckBuildsys => buildsys::check_buildsys(&sh, "Dockerfile".into()),
141142
}
142143
}
143144

@@ -405,48 +406,3 @@ fn update_generated(sh: &Shell) -> Result<()> {
405406

406407
Ok(())
407408
}
408-
409-
/// Check build system properties
410-
///
411-
/// - Reproducible builds for the RPM
412-
#[context("Checking build system")]
413-
fn check_buildsys(sh: &Shell) -> Result<()> {
414-
use std::collections::BTreeMap;
415-
416-
println!("Checking reproducible builds...");
417-
// Helper to compute SHA256 of bootc RPMs in target/packages/
418-
fn get_rpm_checksums(sh: &Shell) -> Result<BTreeMap<String, String>> {
419-
// Find bootc*.rpm files in target/packages/
420-
let packages_dir = Utf8Path::new("target/packages");
421-
let mut rpm_files: Vec<Utf8PathBuf> = Vec::new();
422-
for entry in std::fs::read_dir(packages_dir).context("Reading target/packages")? {
423-
let entry = entry?;
424-
let path = Utf8PathBuf::try_from(entry.path())?;
425-
if path.extension() == Some("rpm") {
426-
rpm_files.push(path);
427-
}
428-
}
429-
430-
assert!(!rpm_files.is_empty());
431-
432-
let mut checksums = BTreeMap::new();
433-
for rpm_path in &rpm_files {
434-
let output = cmd!(sh, "sha256sum {rpm_path}").read()?;
435-
let (hash, filename) = output
436-
.split_once(" ")
437-
.with_context(|| format!("failed to parse sha256sum output: '{}'", output))?;
438-
checksums.insert(filename.to_owned(), hash.to_owned());
439-
}
440-
Ok(checksums)
441-
}
442-
443-
cmd!(sh, "just package").run()?;
444-
let first_checksums = get_rpm_checksums(sh)?;
445-
cmd!(sh, "just package").run()?;
446-
let second_checksums = get_rpm_checksums(sh)?;
447-
448-
itertools::assert_equal(first_checksums, second_checksums);
449-
println!("ok package reproducibility");
450-
451-
Ok(())
452-
}

0 commit comments

Comments
 (0)