From ba8d6efed0addd1cd214c7ff37406436972cf473 Mon Sep 17 00:00:00 2001 From: Radmir Khurum Date: Fri, 23 Oct 2026 13:01:40 +0300 Subject: [PATCH] fix(build, stapel, import): stop imports failing with sbom enabled 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 09f22e7ea6 ("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 --- pkg/build/image/stapel_test.go | 54 ++++++++++++++++++++++++++++ pkg/build/stage/dependencies.go | 6 +++- pkg/build/stage/dependencies_test.go | 14 ++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pkg/build/image/stapel_test.go b/pkg/build/image/stapel_test.go index de7a79fb1c..fecf802450 100644 --- a/pkg/build/image/stapel_test.go +++ b/pkg/build/image/stapel_test.go @@ -113,6 +113,60 @@ packages: ) }) +var _ = Describe("network isolation under build.sbom.enable", func() { + werfYaml := func(importPosition string) string { + return fmt.Sprintf(`project: test +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 + %s +shell: + install: + - echo install + setup: + - echo setup +`, importPosition) + } + + stageNetwork := func(image *Image, name stage.StageName) string { + s := image.GetStage(name) + Expect(s).NotTo(BeNil()) + n, ok := s.(interface{ Network() string }) + Expect(ok).To(BeTrue()) + return n.Network() + } + + DescribeTable("disables network for shell stages but leaves the import stage reachable", + func(importPosition string, importStage stage.StageName) { + ctx := context.Background() + projectDir := newProjectRepo(ctx, map[string]string{"werf.yaml": werfYaml(importPosition)}) + + image, err := stapelImage(ctx, projectDir) + Expect(err).NotTo(HaveOccurred()) + + Expect(stageNetwork(image, stage.Install)).To(Equal("none")) + Expect(stageNetwork(image, stage.Setup)).To(Equal("none")) + Expect(stageNetwork(image, importStage)).To(BeEmpty()) + }, + Entry("before install", "before: install", stage.DependenciesBeforeInstall), + Entry("after install", "after: install", stage.DependenciesAfterInstall), + Entry("before setup", "before: setup", stage.DependenciesBeforeSetup), + Entry("after setup", "after: setup", stage.DependenciesAfterSetup), + ) +}) + var _ = Describe("sign files stages", func() { werfYaml := func(sbomBlock string) string { return fmt.Sprintf(`project: test diff --git a/pkg/build/stage/dependencies.go b/pkg/build/stage/dependencies.go index fe7a48177a..8d082f7481 100644 --- a/pkg/build/stage/dependencies.go +++ b/pkg/build/stage/dependencies.go @@ -44,11 +44,15 @@ func getDependencies(imageBaseConfig *config.StapelImageBase, options *getImport return dependencies } +// newDependenciesStage marks the stage as needing network: under the Docker backend imports +// are rsynced from an import-server container, and the stage carries no shell: commands. func newDependenciesStage(imports []*config.Import, dependencies []*config.Dependency, name StageName, baseStageOptions *BaseStageOptions) *DependenciesStage { + opts := *baseStageOptions + opts.NeedsNetwork = true s := &DependenciesStage{} s.imports = imports s.dependencies = dependencies - s.BaseStage = NewBaseStage(name, baseStageOptions) + s.BaseStage = NewBaseStage(name, &opts) return s } diff --git a/pkg/build/stage/dependencies_test.go b/pkg/build/stage/dependencies_test.go index b56120a483..58d7e8b3c3 100644 --- a/pkg/build/stage/dependencies_test.go +++ b/pkg/build/stage/dependencies_test.go @@ -13,6 +13,20 @@ import ( ) var _ = Describe("DependenciesStage", func() { + DescribeTable("needs network regardless of the base options", + func(name StageName) { + stage := newDependenciesStage(nil, nil, name, &BaseStageOptions{ + ImageName: "example-image", + ProjectName: "example-project", + }) + Expect(stage.NeedsNetwork()).To(BeTrue()) + }, + Entry("dependenciesBeforeInstall", DependenciesBeforeInstall), + Entry("dependenciesAfterInstall", DependenciesAfterInstall), + Entry("dependenciesBeforeSetup", DependenciesBeforeSetup), + Entry("dependenciesAfterSetup", DependenciesAfterSetup), + ) + DescribeTable("configuring images dependencies for dependencies stage", func(ctx SpecContext, data TestDependencies) { conveyor := NewConveyorStubForDependencies(NewGiterminismManagerStub(NewLocalGitRepoStub("9d8059842b6fde712c58315ca0ab4713d90761c0"), NewGiterminismInspectorStub()), data.Dependencies)