Skip to content

Commit

Permalink
Add support for pack build --sbom-output-dir
Browse files Browse the repository at this point in the history
--sbom-output-dir <providied-dir> creates <provided-dir> and exports SBOM created during the build process to <provided-dir>

Signed-off-by: Anthony Emengo <aemengo@vmware.com>
  • Loading branch information
Anthony Emengo committed Jan 21, 2022
1 parent 23034af commit 9174071
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 2 deletions.
13 changes: 13 additions & 0 deletions internal/build/container_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/docker/docker/api/types"
dcontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
darchive "github.com/docker/docker/pkg/archive"
"github.com/pkg/errors"

"github.com/buildpacks/pack/internal/builder"
Expand Down Expand Up @@ -43,6 +44,18 @@ func CopyOut(handler func(closer io.ReadCloser) error, srcs ...string) Container
}
}

func CopyOutTo(src, dest string) ContainerOperation {
return CopyOut(func(reader io.ReadCloser) error {
info := darchive.CopyInfo{
Path: src,
IsDir: true,
}

defer reader.Close()
return darchive.CopyTo(reader, info, dest)
}, src)
}

// CopyDir copies a local directory (src) to the destination on the container while filtering files and changing it's UID/GID.
// if includeRoot is set the UID/GID will be set on the dst directory.
func CopyDir(src, dst string, uid, gid int, os string, includeRoot bool, fileFilter func(string) bool) ContainerOperation {
Expand Down
6 changes: 6 additions & 0 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ func (l *LifecycleExecution) Create(ctx context.Context, publish bool, dockerHos
cacheOpts,
WithContainerOperations(WriteProjectMetadata(l.mountPaths.projectPath(), l.opts.ProjectMetadata, l.os)),
WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, true, l.opts.FileFilter)),
If(l.opts.SBOMDestinationDir != "", WithPostContainerRunOperations(
EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume),
CopyOutTo(l.mountPaths.sbomDir(), l.opts.SBOMDestinationDir))),
If(l.opts.Interactive, WithPostContainerRunOperations(
EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume),
CopyOut(l.opts.Termui.ReadLayers, l.mountPaths.layersDir(), l.mountPaths.appDir()))),
Expand Down Expand Up @@ -541,6 +544,9 @@ func (l *LifecycleExecution) newExport(repoName, runImage string, publish bool,
cacheOpt,
WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)),
WithContainerOperations(WriteProjectMetadata(l.mountPaths.projectPath(), l.opts.ProjectMetadata, l.os)),
If(l.opts.SBOMDestinationDir != "", WithPostContainerRunOperations(
EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume),
CopyOutTo(l.mountPaths.sbomDir(), l.opts.SBOMDestinationDir))),
If(l.opts.Interactive, WithPostContainerRunOperations(
EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume),
CopyOut(l.opts.Termui.ReadLayers, l.mountPaths.layersDir(), l.mountPaths.appDir()))),
Expand Down
54 changes: 52 additions & 2 deletions internal/build/lifecycle_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,32 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {

h.AssertEq(t, fakePhase.CleanupCallCount, 1)
h.AssertEq(t, fakePhase.RunCallCount, 1)
h.AssertEq(t, len(fakePhaseFactory.NewCalledWithProvider[0].PostContainerRunOps()), 2)

provider := fakePhaseFactory.NewCalledWithProvider[0]
h.AssertEq(t, len(provider.PostContainerRunOps()), 2)
h.AssertFunctionName(t, provider.PostContainerRunOps()[0], "EnsureVolumeAccess")
h.AssertFunctionName(t, provider.PostContainerRunOps()[1], "CopyOut")
})
})

when("sbom destination directory is provided", func() {
it("provides copy-sbom-func as a post container operation", func() {
lifecycle := newTestLifecycleExec(t, false, func(opts *build.LifecycleOptions) {
opts.SBOMDestinationDir = "some-destination-dir"
})
fakePhase := &fakes.FakePhase{}
fakePhaseFactory := fakes.NewFakePhaseFactory(fakes.WhichReturnsForNew(fakePhase))

err := lifecycle.Create(context.Background(), false, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
h.AssertNil(t, err)

h.AssertEq(t, fakePhase.CleanupCallCount, 1)
h.AssertEq(t, fakePhase.RunCallCount, 1)

provider := fakePhaseFactory.NewCalledWithProvider[0]
h.AssertEq(t, len(provider.PostContainerRunOps()), 2)
h.AssertFunctionName(t, provider.PostContainerRunOps()[0], "EnsureVolumeAccess")
h.AssertFunctionName(t, provider.PostContainerRunOps()[1], "CopyOut")
})
})
})
Expand Down Expand Up @@ -2565,7 +2590,32 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {

h.AssertEq(t, fakePhase.CleanupCallCount, 1)
h.AssertEq(t, fakePhase.RunCallCount, 1)
h.AssertEq(t, len(fakePhaseFactory.NewCalledWithProvider[0].PostContainerRunOps()), 2)

provider := fakePhaseFactory.NewCalledWithProvider[0]
h.AssertEq(t, len(provider.PostContainerRunOps()), 2)
h.AssertFunctionName(t, provider.PostContainerRunOps()[0], "EnsureVolumeAccess")
h.AssertFunctionName(t, provider.PostContainerRunOps()[1], "CopyOut")
})
})

when("sbom destination directory is provided", func() {
it("provides copy-sbom-func as a post container operation", func() {
lifecycle := newTestLifecycleExec(t, false, func(opts *build.LifecycleOptions) {
opts.SBOMDestinationDir = "some-destination-dir"
})
fakePhase := &fakes.FakePhase{}
fakePhaseFactory := fakes.NewFakePhaseFactory(fakes.WhichReturnsForNew(fakePhase))

err := lifecycle.Create(context.Background(), false, "", false, "test", "test", "test", fakeBuildCache, fakeLaunchCache, []string{}, []string{}, fakePhaseFactory)
h.AssertNil(t, err)

h.AssertEq(t, fakePhase.CleanupCallCount, 1)
h.AssertEq(t, fakePhase.RunCallCount, 1)

provider := fakePhaseFactory.NewCalledWithProvider[0]
h.AssertEq(t, len(provider.PostContainerRunOps()), 2)
h.AssertFunctionName(t, provider.PostContainerRunOps()[0], "EnsureVolumeAccess")
h.AssertFunctionName(t, provider.PostContainerRunOps()[1], "CopyOut")
})
})
})
Expand Down
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type LifecycleOptions struct {
Workspace string
GID int
PreviousImage string
SBOMDestinationDir string
}

func NewLifecycleExecutor(logger logging.Logger, docker client.CommonAPIClient) *LifecycleExecutor {
Expand Down
4 changes: 4 additions & 0 deletions internal/build/mount_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ func (m mountPaths) cacheDir() string {
func (m mountPaths) launchCacheDir() string {
return m.join(m.volume, "launch-cache")
}

func (m mountPaths) sbomDir() string {
return m.join(m.volume, "layers", "sbom")
}
3 changes: 3 additions & 0 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type BuildFlags struct {
Workspace string
GID int
PreviousImage string
SBOMDestinationDir string
}

// Build an image from source code
Expand Down Expand Up @@ -157,6 +158,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
GroupID: gid,
PreviousImage: flags.PreviousImage,
Interactive: flags.Interactive,
SBOMDestinationDir: flags.SBOMDestinationDir,
}); err != nil {
return errors.Wrap(err, "failed to build")
}
Expand Down Expand Up @@ -197,6 +199,7 @@ This option may set DOCKER_HOST environment variable for the build container if
cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image")
cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`)
cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID")
cmd.Flags().StringVar(&buildFlags.SBOMDestinationDir, "sbom-output-dir", "", "Path to export SBoM contents.\nOmitting the flag will yield no SBoM content.")
cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process")
if !cfg.Experimental {
cmd.Flags().MarkHidden("interactive")
Expand Down
20 changes: 20 additions & 0 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,17 @@ builder = "my-builder"
h.AssertError(t, err, "Interactive mode is currently experimental.")
})
})

when("sbom destination directory is provided", func() {
it("forwards the network onto the client", func() {
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithSBOMOutputDir("some-output-dir")).
Return(nil)

command.SetArgs([]string{"image", "--builder", "my-builder", "--sbom-output-dir", "some-output-dir"})
h.AssertNil(t, command.Execute())
})
})
})
}

Expand Down Expand Up @@ -911,6 +922,15 @@ func EqBuildOptionsWithPreviousImage(prevImage string) gomock.Matcher {
}
}

func EqBuildOptionsWithSBOMOutputDir(s string) interface{} {
return buildOptionsMatcher{
description: fmt.Sprintf("sbom-destination-dir=%s", s),
equals: func(o client.BuildOptions) bool {
return o.SBOMDestinationDir == s
},
}
}

type buildOptionsMatcher struct {
equals func(client.BuildOptions) bool
description string
Expand Down
4 changes: 4 additions & 0 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ type BuildOptions struct {
// This places registry credentials on the builder's build image.
// Only trust builders from reputable sources.
TrustBuilder IsTrustedBuilder

// Directory to output any SBOM artifacts
SBOMDestinationDir string
}

// ProxyConfig specifies proxy setting to be set as environment variables in a container.
Expand Down Expand Up @@ -352,6 +355,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
PreviousImage: opts.PreviousImage,
Interactive: opts.Interactive,
Termui: termui.NewTermui(imageRef.Name(), bldr, runImageName),
SBOMDestinationDir: opts.SBOMDestinationDir,
}

lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version
Expand Down
11 changes: 11 additions & 0 deletions pkg/client/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2471,6 +2471,17 @@ func testBuild(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, fakeLifecycle.Opts.Interactive, true)
})
})

when("sbom destination dir option", func() {
it("passthroughs to lifecycle", func() {
h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{
Builder: defaultBuilderName,
Image: "example.com/some/repo:tag",
SBOMDestinationDir: "some-destination-dir",
}))
h.AssertEq(t, fakeLifecycle.Opts.SBOMDestinationDir, "some-destination-dir")
})
})
})
}

Expand Down

0 comments on commit 9174071

Please sign in to comment.