Skip to content

fix(build, stapel, import): stop imports failing with sbom enabled - #470

Draft
reyreavman wants to merge 1 commit into
mainfrom
fix/build/sbom-import-network
Draft

reyreavman wants to merge 1 commit into
mainfrom
fix/build/sbom-import-network

Conversation

@reyreavman

@reyreavman reyreavman commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A stapel image with import: fails to build under build.sbom.enable: true with the Docker backend: the dependencies* stage dies with rsync: failed to connect to <import-server ip>: Network unreachable (101), is retried once "in a clean container after a transport failure" and fails again with container run failed: exit code 10. Repro from a clean checkout:

project: demo
configVersion: 1
build:
  sbom:
    enable: true
    standard: cyclonedx@1.6
---
image: donor
from: alpine:3.20
---
image: app
from: alpine:3.20
import:
- image: donor
  add: /etc/os-release
  to: /donor/os-release
  before: install
WERF_BUILDAH_MODE=docker werf build

What

  • Under build.sbom.enable: true with the Docker backend the dependencies* (import) stages run with the backend's default network again and the import completes; previously they ran with --network=none.
  • Shell stages (beforeInstall, install, beforeSetup, setup) still run with --network=none under build.sbom.enable: true, and --backend-network / werf.yaml network: still cannot open them.
  • The packages stage is unchanged: it keeps the network it had.
  • build.sbom.enable: false is unchanged: no stage gets a network override.
  • The "Network is disabled for shell stages" warning is unchanged.
  • Stage digests are unchanged, so already built stages are reused.

Why

With build.sbom.enable: true the isolation code marks every stage except packages as network-less, dependencies* included. Until 09f22e7 that mark never reached the import container: only BaseStage.PrepareImage applied it and DependenciesStage.PrepareImage bypasses that path, so the write was dead and imports worked. 09f22e7 made the build phase forward a stage's own network override into the builder for every stage — to keep the sign stages closed against --backend-network — which turned the dead write live. Under the Docker backend an import is an rsync from the import-server container over the bridge network, so a network-less import stage cannot succeed.

Leaving the import stage network-less buys nothing for the SBOM: the stage carries no shell: commands and copies files out of an image that import: already declares and whose BOM the importing image already inherits. Marking the dependencies* stages as needing network, the way packages already is, keeps shell stages offline and lets imports work.

Rejected: special-casing the import stages inside the build phase's option merge — the exemption belongs where packages is exempted, so one rule decides which stages the SBOM isolation leaves alone.

Fixes: 09f22e7 ("feat(build, stapel): sign files inside the image with a key the build container never sees")

@reyreavman

reyreavman commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verification

  • task test:e2e paths=./test/e2e/sbom labelFilter=pip parallel=1 -- -focus="marks a package of the donor" (Docker backend, Linux host with kind registry): fails on main with the rsync Network unreachable (101) transport error, passes with this branch — 1 of 75 specs, 98 s.
  • task test:e2e paths=./test/e2e/build labelFilter=managed-deps parallel=1 (Docker backend, same host): both specs pass — sbom.enable=true still enforces --network=none on shell stages, sbom.enable=false still does not.
  • Mutation: reverted newDependenciesStage to pass the base options through unchanged → DependenciesStage needs network regardless of the base options fails for all four stage names, and all four entries of network isolation under build.sbom.enable fail on the import stage's network expecting empty, getting none.

Review focus

  • The exemption is decided by NeedsNetwork(), the same predicate the SBOM isolation already uses for packages; confirm no other consumer of that predicate gives the import stage something beyond skipping the override.

Follow-up

  • pkg/build/import_server/rsync_server.go interpolates import.to, owner and group unquoted into the bash script the Docker import container evaluates, so a to: '/x/$(cmd)' runs cmd there. Pre-existing and independent of SBOM, but with this PR that container has network again under build.sbom.enable: true (as it did before 09f22e7). Quote those values (strconv.Quote-style shell escaping) and add a metacharacter regression test — separate PR.

With build.sbom.enable: true the SBOM network isolation marks every stage
except packages as network-less, the dependencies* stages that carry
import: included. Until 09f22e7 ("feat(build, stapel): sign files
inside the image with a key the build container never sees") that mark
never reached the import container: only BaseStage.PrepareImage applied
it, and DependenciesStage.PrepareImage bypasses it. stageBuildOptions
now forwards the stage's network override into the builder for every
stage, so the import container started with --network=none. Under the
Docker backend an import is an rsync from the import-server container
over the bridge network, so the stage failed with "rsync: failed to
connect ... Network unreachable (101)" and the build stopped.

Mark the dependencies stages as needing network, exactly as the packages
stage already is, so the SBOM isolation skips them. The stage carries no
shell: commands and copies files out of an image that import: already
declares and the SBOM already accounts for, so shell stages stay offline
and the guarantee is unchanged.

Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
@reyreavman
reyreavman force-pushed the fix/build/sbom-import-network branch from 1869480 to ba8d6ef Compare October 23, 2026 10:08
@reyreavman

Copy link
Copy Markdown
Collaborator Author

Code Review Report

Base: main (PR head ba8d6efed0; every file:line below refers to that commit)
Diff: 3 files, +73/-1

Verdict

  • Technical: The fix is correct and minimal — the dependencies* stages are marked NeedsNetwork exactly the way packages already is (pkg/build/stage/packages.go:26-27 ↔ pkg/build/stage/dependencies.go:50-51), so the isolation loop in pkg/build/image/stapel.go:158-161 skips them. The claims in the description check out against the code: DependenciesStage.PrepareImage indeed never calls BaseStage.PrepareImage (dependencies.go:230-238), and stageBuildOptions (pkg/build/build_phase.go:2033-2046) forwards only NetworkOverride, which is now empty for import stages. Both new tests survived a mutation check.
  • Product: Behaviour returns to what it was before 09f22e7 — the import stage runs with the backend's network, shell stages stay --network=none, the warning is unchanged (the import stage is skipped before hasShellStages is set, stapel.go:159-165). Digests are untouched. No docs/CHANGELOG changes are needed.
  • Risk: Low. The only thing holding the PR back is a red unit job in CI for a reason unrelated to the diff, which causes the e2e jobs that actually reproduce the bug to be skipped.

DoD Criteria

Criteria Inferred? Met? Evidence
Under sbom.enable: true + Docker the import stages get the default network yes (from PR body) ✅ dependencies.go:50-51; stapel_test.go:159 (Network() empty for all 4 import positions)
Shell stages stay --network=none, --backend-network cannot open them yes ✅ stapel_test.go:157-158; build_phase.go:2039-2043
packages unchanged yes ✅ packages.go not in the diff
sbom.enable: false — no override yes ✅ (not tested) stapel.go:155-156 — whole block is gated on sbomEnabled; the new test does not cover this case
"Network is disabled for shell stages" warning unchanged yes ✅ stapel.go:159-165 — continue happens before hasShellStages = true
Stage digests unchanged yes ✅ NeedsNetwork is not an input of GetDependencies; all digest tests in the suite pass on head
Local: task build, task test:unit paths="./pkg/build/stage/... ./pkg/build/image/..." — ✅ 3 suites, Test Suite Passed

Issues

  • Major — PR CI is red: the unit job failed in two Buildah tests (buildah … avoids repeated storage lookups…, Buildah image metadata after retagging…) with lstat /run/user/998: no such file or directory. That is the runner environment (no XDG runtime dir); the diff does not touch Buildah, and the same job is green on main (run 39920117480). Because of it e2e_extra/e2e_simple were skipped on this PR — and those are the real check of the fix. Needs a rerun; if it reproduces, it is a separate infra problem, not this PR's.
  • Minor — The PR description does not mention that the regression is already reproduced by existing e2e tests on main: e2e_extra fails with exactly app/dependenciesBeforeSetup rsync: failed to connect … Network unreachable (101) in SBOM final repo with dependent images (test/e2e/sbom/final_repo_dependent_test.go) and SBOM and VEX across every repository of a build (test/e2e/sbom/final_repo_sweep_test.go). Since the description becomes the squash commit body, one line is worth adding: "existing e2e [sbom, final-repo, dependent|sweep] reproduce the failure on main and are the end-to-end check". That also explains why the PR adds no new e2e test.
  • Minor — pkg/build/image/stapel_test.go:143-147: stageNetwork type-asserts interface{ Network() string } even though stage.Interface does not declare it. Production code does the same (build_phase.go:1378), so it is acceptable; no action required.

Tests as evidence (mutation loop)

Test Mutation Result
dependencies_test.go:16-28 "needs network regardless of the base options" opts.NeedsNetwork = true → false (revert the fix) ❌ 4/4 entries failed (Expected … to be true) — as they should
stapel_test.go:149-163 "disables network for shell stages but leaves the import stage reachable" same mutation ❌ 4/4 failed on Expect(stageNetwork(image, importStage)).To(BeEmpty())
same SetNetworkOverride("none") → SetNetworkOverride("") in stapel.go:163 (drop the isolation) ❌ 4/4 failed on Install/Setup == "none"

The tree was restored from a copy after every mutation, git status clean. Mutation not tried: removing the continue in stapel.go:160 — the same test would catch it (the import stage would get "none").

Risks

№ Risk Type Likelihood Severity Location Circumstances Consequences Recommendation
1 PR merged with e2e skipped Operational Possible Low CI run 39936974080 unit fails on the runner environment, e2e never runs The fix lands on main without its only end-to-end check under the Docker backend Rerun CI and wait for green e2e_extra ([sbom, final-repo, dependent], [sbom, final-repo, sweep]) before merging

Not verified

  • A real build under WERF_BUILDAH_MODE=docker is impossible on macOS — e2e needs Linux + Docker. The only confirmation is CI (e2e_extra), which is currently skipped on this PR.
  • task lint and an unscoped task test:unit were not run locally (CI lint is green; locally only ./pkg/build/stage/... and ./pkg/build/image/... were run).
  • Buildah path: under Buildah imports do not use rsync/network, so the NeedsNetwork mark changes nothing there — verified by reading (DependenciesStage.prepareImage), not by execution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant