Skip to content

Commit

Permalink
fix: allow bom build and verification for build_only layers (#609)
Browse files Browse the repository at this point in the history
* fix: allow bom build and verification for `build_only` layers

From our experience, package information may be removed in such layers
making it much harder to discover and auto-construct BOMs.

So allow this for `build_only` layers also.

Signed-off-by: Ramkumar Chinchani <[email protected]>

* fix: import a bom only if available from built layers

It is possible that a build_only layer doesn't generate a BOM, so it
cannot be imported in a derived layer.

Signed-off-by: Ramkumar Chinchani <[email protected]>

---------

Signed-off-by: Ramkumar Chinchani <[email protected]>
  • Loading branch information
rchincha authored Apr 4, 2024
1 parent 86ba851 commit 6d069c7
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 23 deletions.
7 changes: 7 additions & 0 deletions pkg/stacker/bom.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package stacker
import (
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"

"github.com/pkg/errors"
"stackerbuild.io/stacker/pkg/container"
"stackerbuild.io/stacker/pkg/log"
"stackerbuild.io/stacker/pkg/types"
Expand Down Expand Up @@ -109,6 +111,11 @@ func ImportArtifacts(sc types.StackerConfig, src types.ImageSource, name string)
// if a bom is available, add it here so it can be merged
srcpath := path.Join(sc.StackerDir, "artifacts", src.Tag, fmt.Sprintf("%s.json", src.Tag))

_, err := os.Lstat(srcpath)
if err != nil && errors.Is(err, fs.ErrNotExist) {
return nil
}

dstfp, err := os.CreateTemp(path.Join(sc.StackerDir, "artifacts", name), fmt.Sprintf("%s-*.json", name))
if err != nil {
return err
Expand Down
47 changes: 24 additions & 23 deletions pkg/stacker/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,30 @@ func (b *Builder) build(s types.Storage, file string) error {
}
}

// build artifacts such as BOMs, etc
if l.Bom != nil && l.Bom.Generate {
log.Debugf("generating layer artifacts for %s", name)

if err := ImportArtifacts(opts.Config, l.From, name); err != nil {
log.Errorf("unable to import previously built artifacts, err:%v", err)
return err
}

for _, pkg := range l.Bom.Packages {
if err := BuildLayerArtifacts(opts.Config, s, l, name, pkg); err != nil {
log.Errorf("failed to generate layer artifacts for %s - %v", name, err)
return err
}
}

if err := VerifyLayerArtifacts(opts.Config, s, l, name); err != nil {
log.Errorf("failed to validate layer artifacts for %s - %v", name, err)
// the generated bom is invalid, remove it so we don't publish it and also get a cache miss for rebuilds
_ = os.Remove(path.Join(opts.Config.StackerDir, "artifacts", name, fmt.Sprintf("%s.json", name)))
return err
}
}

// This is a build only layer, meaning we don't need to include
// it in the final image, as outputs from it are going to be
// imported into future images. Let's just snapshot it and add
Expand Down Expand Up @@ -554,29 +578,6 @@ func (b *Builder) build(s types.Storage, file string) error {

log.Infof("filesystem %s built successfully", name)

// build artifacts such as BOMs, etc
if l.Bom != nil && l.Bom.Generate {
log.Debugf("generating layer artifacts for %s", name)

if err := ImportArtifacts(opts.Config, l.From, name); err != nil {
log.Errorf("unable to import previously built artifacts, err:%v", err)
return err
}

for _, pkg := range l.Bom.Packages {
if err := BuildLayerArtifacts(opts.Config, s, l, name, pkg); err != nil {
log.Errorf("failed to generate layer artifacts for %s - %v", name, err)
return err
}
}

if err := VerifyLayerArtifacts(opts.Config, s, l, name); err != nil {
log.Errorf("failed to validate layer artifacts for %s - %v", name, err)
// the generated bom is invalid, remove it so we don't publish it and also get a cache miss for rebuilds
_ = os.Remove(path.Join(opts.Config.StackerDir, "artifacts", name, fmt.Sprintf("%s.json", name)))
return err
}
}
}

return oci.GC(context.Background())
Expand Down
146 changes: 146 additions & 0 deletions test/bom.bats
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,149 @@ EOF
fi
stacker clean
}

@test "all container contents must be accounted for even for build_only layers" {
skip_slow_test
cat > stacker.yaml <<"EOF"
bom-parent:
build_only: true
from:
type: oci
url: ${{CENTOS_OCI}}
bom:
generate: true
namespace: "https://test.io/artifacts"
packages:
- name: pkg1
version: 1.0.0
license: Apache-2.0
paths: [/pkg1]
- name: pkg2
version: 1.0.0
license: Apache-2.0
paths: [/pkg2]
run: |
# discover installed pkgs
/stacker/bin/stacker bom discover
# our own custom packages
mkdir -p /pkg1
touch /pkg1/file
mkdir -p /pkg2
touch /pkg2/file
# should cause build to fail!
mkdir -p /orphan-without-a-package
touch /orphan-without-a-package/file
# cleanup
rm -rf /var/lib/alternatives /tmp/* \
/etc/passwd- /etc/group- /etc/shadow- /etc/gshadow- \
/etc/sysconfig/network /etc/nsswitch.conf.bak \
/etc/rpm/macros.image-language-conf /var/lib/rpm/.dbenv.lock \
/var/lib/rpm/Enhancename /var/lib/rpm/Filetriggername \
/var/lib/rpm/Recommendname /var/lib/rpm/Suggestname \
/var/lib/rpm/Supplementname /var/lib/rpm/Transfiletriggername \
/var/log/anaconda \
/etc/sysconfig/anaconda /etc/sysconfig/network-scripts/ifcfg-* \
/etc/sysconfig/sshd-permitrootlogin /root/anaconda-* /root/original-* /run/nologin \
/var/lib/rpm/.rpm.lock /etc/.pwd.lock /etc/BUILDTIME
annotations:
org.opencontainers.image.authors: bom-test
org.opencontainers.image.vendor: bom-test
org.opencontainers.image.licenses: MIT
EOF
run stacker build --substitute CENTOS_OCI=${CENTOS_OCI}
[ "$status" -ne 0 ]
# a full inventory for this image
[ -f .stacker/artifacts/bom-parent/inventory.json ]
# sbom for this image shouldn't be generated
[ ! -a .stacker/artifacts/bom-parent/first.json ]
# building a second time also fails due to missed cache
run stacker build
[ "$status" -ne 0 ]
stacker clean
}

@test "skip bom generation for built layer" {
skip_slow_test
cat > stacker.yaml <<"EOF"
first:
from:
type: oci
url: ${{CENTOS_OCI}}
bom:
generate: true
namespace: "https://test.io/artifacts"
packages:
- name: pkg1
version: 1.0.0
license: Apache-2.0
paths: [/pkg1]
- name: pkg2
version: 1.0.0
license: Apache-2.0
paths: [/pkg2]
run: |
# our own custom packages
mkdir -p /pkg1
touch /pkg1/file
mkdir -p /pkg2
touch /pkg2/file
second:
from:
type: built
tag: first
bom:
generate: true
namespace: "https://test.io/artifacts"
packages:
- name: pkg1
version: 1.0.0
license: Apache-2.0
paths: [/pkg1]
- name: pkg2
version: 1.0.0
license: Apache-2.0
paths: [/pkg2]
- name: pkg3
version: 1.0.0
license: Apache-2.0
paths: [/pkg3]
run: |
# discover installed pkgs
/stacker/bin/stacker bom discover
# our own custom packages
mkdir -p /pkg3
touch /pkg3/file
# cleanup
rm -rf /var/lib/alternatives /tmp/* \
/etc/passwd- /etc/group- /etc/shadow- /etc/gshadow- \
/etc/sysconfig/network /etc/nsswitch.conf.bak \
/etc/rpm/macros.image-language-conf /var/lib/rpm/.dbenv.lock \
/var/lib/rpm/Enhancename /var/lib/rpm/Filetriggername \
/var/lib/rpm/Recommendname /var/lib/rpm/Suggestname \
/var/lib/rpm/Supplementname /var/lib/rpm/Transfiletriggername \
/var/log/anaconda \
/etc/sysconfig/anaconda /etc/sysconfig/network-scripts/ifcfg-* \
/etc/sysconfig/sshd-permitrootlogin /root/anaconda-* /root/original-* /run/nologin \
/var/lib/rpm/.rpm.lock /etc/.pwd.lock /etc/BUILDTIME
annotations:
org.opencontainers.image.authors: bom-test
org.opencontainers.image.vendor: bom-test
org.opencontainers.image.licenses: MIT
EOF
stacker build --substitute CENTOS_OCI=${CENTOS_OCI}
# a full inventory for this image
[ -f .stacker/artifacts/second/inventory.json ]
# sbom for this image
[ -f .stacker/artifacts/second/second.json ]
if [ -n "${ZOT_HOST}:${ZOT_PORT}" ]; then
zot_setup
stacker publish --skip-tls --url docker://${ZOT_HOST}:${ZOT_PORT} --tag latest --substitute CENTOS_OCI=${CENTOS_OCI}
refs=$(regctl artifact tree ${ZOT_HOST}:${ZOT_PORT}/second:latest --format "{{json .}}" | jq '.referrer | length')
[ $refs -eq 2 ]
refs=$(regctl artifact get --subject ${ZOT_HOST}:${ZOT_PORT}/second:latest --filter-artifact-type "application/spdx+json" | jq '.SPDXID')
[ $refs == \"SPDXRef-DOCUMENT\" ]
zot_teardown
fi
stacker clean
}

0 comments on commit 6d069c7

Please sign in to comment.