Skip to content

Commit

Permalink
Merge pull request #251 from puerco/image-sboms
Browse files Browse the repository at this point in the history
Fix image structure in apko SBOMs
  • Loading branch information
puerco committed Jun 29, 2022
2 parents ab57dbe + 29d9f79 commit b54c185
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 192 deletions.
5 changes: 3 additions & 2 deletions internal/cli/build.go
Expand Up @@ -125,6 +125,7 @@ func BuildCmd(ctx context.Context, imageRef, outputTarGZ string, opts ...build.O
if err != nil {
return fmt.Errorf("failed to build layer image: %w", err)
}

defer os.Remove(layerTarGZ)

if err := bc.GenerateSBOM(); err != nil {
Expand All @@ -134,14 +135,14 @@ func BuildCmd(ctx context.Context, imageRef, outputTarGZ string, opts ...build.O
if bc.Options.UseDockerMediaTypes {
if err := oci.BuildDockerImageTarballFromLayer(
imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, bc.Options.Arch,
bc.Logger(),
bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats,
); err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
} else {
if err := oci.BuildImageTarballFromLayer(
imageRef, layerTarGZ, outputTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, bc.Options.Arch,
bc.Logger(),
bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats,
); err != nil {
return fmt.Errorf("failed to build OCI image: %w", err)
}
Expand Down
167 changes: 99 additions & 68 deletions internal/cli/publish.go
Expand Up @@ -16,7 +16,6 @@ package cli

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -44,6 +43,7 @@ func publish() *cobra.Command {
var extraKeys []string
var extraRepos []string
var debugEnabled bool
var writeSBOM bool

cmd := &cobra.Command{
Use: "publish",
Expand All @@ -55,6 +55,9 @@ in a keychain.`,
Example: ` apko publish <config.yaml> <tag...>`,
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if !writeSBOM {
sbomFormats = []string{}
}
archs := types.ParseArchitectures(archstrs)
if err := PublishCmd(cmd.Context(), imageRefs, archs,
build.WithConfig(args[0]),
Expand All @@ -80,7 +83,8 @@ in a keychain.`,
cmd.Flags().BoolVar(&useDockerMediaTypes, "use-docker-mediatypes", false, "use Docker mediatypes for image layers/manifest")
cmd.Flags().BoolVar(&debugEnabled, "debug", false, "enable debug logging")
cmd.Flags().StringVar(&buildDate, "build-date", "", "date used for the timestamps of the files inside the image")
cmd.Flags().StringVar(&sbomPath, "sbom-path", "", "generate an SBOM")
cmd.Flags().BoolVar(&writeSBOM, "sbom", true, "generate an SBOM")
cmd.Flags().StringVar(&sbomPath, "sbom-path", "", "path to write the SBOMs")
cmd.Flags().StringSliceVar(&archstrs, "arch", nil, "architectures to build for (e.g., x86_64,ppc64le,arm64) -- default is all, unless specified in config.")
cmd.Flags().StringSliceVarP(&extraKeys, "keyring-append", "k", []string{}, "path to extra keys to include in the keyring")
cmd.Flags().StringSliceVar(&sbomFormats, "sbom-formats", sbom.DefaultOptions.Formats, "SBOM formats to output")
Expand All @@ -101,113 +105,140 @@ func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architectu
return err
}

if len(bc.Options.SBOMFormats) > 0 {
bc.Options.WantSBOM = true
} else {
bc.Options.WantSBOM = false
}

if len(archs) == 0 {
archs = types.AllArchs
}
if len(bc.ImageConfiguration.Archs) == 0 {
bc.ImageConfiguration.Archs = archs
}
bc.Logger().Infof(
"Publishing images for %d architectures: %+v",
len(bc.ImageConfiguration.Archs),
bc.ImageConfiguration.Archs,
)

bc.Logger().Printf("building tags %v", bc.Options.Tags)
// The build context options is sometimes copied in the next functions. Ensure
// we have the directory defined and created by invoking the function early.
bc.Options.TempDir()
defer os.RemoveAll(bc.Options.TempDir())

var digest name.Digest
switch len(bc.ImageConfiguration.Archs) {
case 0:
return errors.New("no archs requested")
case 1:
bc.Options.Arch = bc.ImageConfiguration.Archs[0]
bc.Logger().Printf("building tags %v", bc.Options.Tags)

if err := bc.Refresh(); err != nil {
return fmt.Errorf("failed to update build context for %q: %w", bc.Options.Arch, err)
}
var errg errgroup.Group
workDir := bc.Options.WorkDir
imgs := map[types.Architecture]coci.SignedImage{}

layerTarGZ, err := bc.BuildLayer()
if err != nil {
return fmt.Errorf("failed to build layer image: %w", err)
}
defer os.Remove(layerTarGZ)
// This is a hack to skip the SBOM generation during
// image build. Will be removed when global options are a thing.
formats := bc.Options.SBOMFormats
wantSBOM := bc.Options.WantSBOM
bc.Options.SBOMFormats = []string{}
bc.Options.WantSBOM = false

if bc.Options.UseDockerMediaTypes {
digest, _, err = oci.PublishDockerImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, bc.Options.Arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats, bc.Options.Tags...)
if err != nil {
return fmt.Errorf("failed to build Docker image: %w", err)
}
} else {
digest, _, err = oci.PublishImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, bc.Options.Arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats, bc.Options.Tags...)
if err != nil {
return fmt.Errorf("failed to build OCI image: %w", err)
}
}
imageTags := []string{}
if len(bc.ImageConfiguration.Archs) == 1 {
imageTags = bc.Options.Tags
}

default:
var errg errgroup.Group
workDir := bc.Options.WorkDir
imgs := map[types.Architecture]coci.SignedImage{}
var finalDigest name.Digest
for _, arch := range bc.ImageConfiguration.Archs {
arch := arch
bc := *bc

for _, arch := range bc.ImageConfiguration.Archs {
arch := arch
bc := *bc
errg.Go(func() error {
bc.Options.Arch = arch
bc.Options.WorkDir = filepath.Join(workDir, arch.ToAPK())

errg.Go(func() error {
bc.Options.Arch = arch
bc.Options.WorkDir = filepath.Join(workDir, arch.ToAPK())
if err := bc.Refresh(); err != nil {
return fmt.Errorf("failed to update build context for %q: %w", arch, err)
}

if err := bc.Refresh(); err != nil {
return fmt.Errorf("failed to update build context for %q: %w", arch, err)
}
layerTarGZ, err := bc.BuildLayer()
if err != nil {
return fmt.Errorf("failed to build layer image for %q: %w", arch, err)
}
// TODO(kaniini): clean up everything correctly for multitag scenario
// defer os.Remove(layerTarGZ)

layerTarGZ, err := bc.BuildLayer()
var img coci.SignedImage
if bc.Options.UseDockerMediaTypes {
finalDigest, img, err = oci.PublishDockerImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats, imageTags...)
if err != nil {
return fmt.Errorf("failed to build layer image for %q: %w", arch, err)
return fmt.Errorf("failed to build Docker image for %q: %w", arch, err)
}
// TODO(kaniini): clean up everything correctly for multitag scenario
// defer os.Remove(layerTarGZ)

var img coci.SignedImage
if bc.Options.UseDockerMediaTypes {
_, img, err = oci.PublishDockerImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats)
if err != nil {
return fmt.Errorf("failed to build Docker image for %q: %w", arch, err)
}
} else {
_, img, err = oci.PublishImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats)
if err != nil {
return fmt.Errorf("failed to build OCI image for %q: %w", arch, err)
}
} else {
finalDigest, img, err = oci.PublishImageFromLayer(layerTarGZ, bc.ImageConfiguration, bc.Options.SourceDateEpoch, arch, bc.Logger(), bc.Options.SBOMPath, bc.Options.SBOMFormats, imageTags...)
if err != nil {
return fmt.Errorf("failed to build OCI image for %q: %w", arch, err)
}
imgs[arch] = img
return nil
})
}
}

if err := errg.Wait(); err != nil {
return err
}
imgs[arch] = img
return nil
})
}

if err := errg.Wait(); err != nil {
return err
}

if len(archs) > 1 {
if bc.Options.UseDockerMediaTypes {
digest, err = oci.PublishDockerIndex(imgs, logrus.NewEntry(bc.Options.Log), bc.Options.Tags...)
finalDigest, err = oci.PublishDockerIndex(imgs, logrus.NewEntry(bc.Options.Log), bc.Options.Tags...)
if err != nil {
return fmt.Errorf("failed to build Docker index: %w", err)
}
} else {
digest, err = oci.PublishIndex(imgs, logrus.NewEntry(bc.Options.Log), bc.Options.Tags...)
finalDigest, err = oci.PublishIndex(imgs, logrus.NewEntry(bc.Options.Log), bc.Options.Tags...)
if err != nil {
return fmt.Errorf("failed to build OCI index: %w", err)
}
}
}

bc.Options.SBOMFormats = formats
sbompath := bc.Options.SBOMPath
if bc.Options.SBOMPath == "" {
sbompath = bc.Options.TempDir()
}

if wantSBOM {
logrus.Info("Generating arch image SBOMs")
for arch, img := range imgs {
bc.Options.WantSBOM = true
bc.Options.Arch = arch
bc.Options.TarballPath = filepath.Join(bc.Options.TempDir(), bc.Options.TarballFileName())
bc.Options.WorkDir = filepath.Join(workDir, arch.ToAPK())

if err := bc.GenerateImageSBOM(arch, img); err != nil {
return fmt.Errorf("generating sbom for %s: %w", arch, err)
}

if _, err := oci.PostAttachSBOM(
img, sbompath, bc.Options.SBOMFormats, arch, bc.Logger(), bc.Options.Tags...,
); err != nil {
return fmt.Errorf("attaching sboms to %s image: %w", arch, err)
}
}
}

// If provided, this is the name of the file to write digest referenced into
if outputRefs != "" {
//nolint:gosec // Make image ref file readable by non-root
if err := os.WriteFile(outputRefs, []byte(digest.String()), 0666); err != nil {
if err := os.WriteFile(outputRefs, []byte(finalDigest.String()), 0666); err != nil {
return fmt.Errorf("failed to write digest: %w", err)
}
}

// Write the image digest to STDOUT in order to enable command
// composition e.g. kn service create --image=$(apko publish ...)
fmt.Println(digest)
fmt.Println(finalDigest)

return nil
}
23 changes: 21 additions & 2 deletions pkg/build/build.go
Expand Up @@ -24,6 +24,7 @@ import (
"time"

"github.com/hashicorp/go-multierror"
coci "github.com/sigstore/cosign/pkg/oci"
"github.com/sirupsen/logrus"

"chainguard.dev/apko/pkg/build/types"
Expand Down Expand Up @@ -51,6 +52,20 @@ func (bc *Context) BuildTarball() (string, error) {
return bc.impl.BuildTarball(&bc.Options)
}

func (bc *Context) GenerateImageSBOM(arch types.Architecture, img coci.SignedImage) error {
opts := bc.Options

h, err := img.Digest()
if err != nil {
return fmt.Errorf("getting %s image digest: %w", arch, err)
}

opts.Arch = arch
opts.ImageDigest = h.String()

return bc.impl.GenerateSBOM(&opts)
}

func (bc *Context) GenerateSBOM() error {
return bc.impl.GenerateSBOM(&bc.Options)
}
Expand Down Expand Up @@ -84,8 +99,12 @@ func (bc *Context) BuildLayer() (string, error) {
}

// generate SBOM
if err := bc.GenerateSBOM(); err != nil {
return "", fmt.Errorf("generating SBOMs: %w", err)
if bc.Options.WantSBOM {
if err := bc.GenerateSBOM(); err != nil {
return "", fmt.Errorf("generating SBOMs: %w", err)
}
} else {
bc.Logger().Debug("Not generating SBOMs (WantSBOM = false)")
}

return layerTarGZ, nil
Expand Down
18 changes: 11 additions & 7 deletions pkg/build/build_implementation.go
Expand Up @@ -17,8 +17,8 @@ package build
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/google/go-containerregistry/pkg/name"
v1tar "github.com/google/go-containerregistry/pkg/v1/tarball"
Expand Down Expand Up @@ -52,9 +52,7 @@ type buildImplementation interface {
type defaultBuildImplementation struct{}

func (di *defaultBuildImplementation) Refresh(o *options.Options) (*s6.Context, *exec.Executor, error) {
if strings.HasPrefix(o.TarballPath, "/tmp/apko") {
o.TarballPath = ""
}
o.TarballPath = ""

hostArch := types.ParseArchitecture(runtime.GOARCH)

Expand All @@ -79,7 +77,7 @@ func (di *defaultBuildImplementation) BuildTarball(o *options.Options) (string,
if o.TarballPath != "" {
outfile, err = os.Create(o.TarballPath)
} else {
outfile, err = os.CreateTemp("", "apko-*.tar.gz")
outfile, err = os.Create(filepath.Join(o.TempDir(), o.TarballFileName()))
}
if err != nil {
return "", fmt.Errorf("opening the build context tarball path failed: %w", err)
Expand Down Expand Up @@ -131,19 +129,25 @@ func (di *defaultBuildImplementation) GenerateSBOM(o *options.Options) error {
s.Options.ImageInfo.Name = tag.String()
}

s.Options.ImageInfo.ImageDigest = o.ImageDigest

// Generate the packages externally as we may
// move the package reader somewhere else
packages, err := s.ReadPackageIndex()
if err != nil {
return fmt.Errorf("getting installed packages from sbom: %w", err)
}
s.Options.ImageInfo.Arch = o.Arch
s.Options.ImageInfo.Digest = digest.String()
s.Options.ImageInfo.LayerDigest = digest.String()
s.Options.ImageInfo.SourceDateEpoch = o.SourceDateEpoch
s.Options.OutputDir = o.SBOMPath
s.Options.Packages = packages
s.Options.Formats = o.SBOMFormats

s.Options.OutputDir = o.TempDir()
if o.SBOMPath != "" {
s.Options.OutputDir = o.SBOMPath
}

if _, err := s.Generate(); err != nil {
return fmt.Errorf("generating SBOMs: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/build/build_test.go
Expand Up @@ -64,6 +64,7 @@ func TestBuildLayer(t *testing.T) {
mock := buildfakes.FakeBuildImplementation{}
tc.prepare(&mock)
sut, err := build.New("/mock")
sut.Options.WantSBOM = true
require.NoError(t, err)
sut.SetImplementation(&mock)
_, err = sut.BuildLayer()
Expand Down

0 comments on commit b54c185

Please sign in to comment.