Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pack build command to export to OCI layout format on disk #1596

Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4d6d39d
WIP - first version
jjbustamante Dec 16, 2022
c9e1e0c
WIP - running code formatting
jjbustamante Jan 17, 2023
99ad545
WIP - fixing lint error
jjbustamante Jan 17, 2023
c6cef8a
WIP - procesing previous image
jjbustamante Jan 18, 2023
a204c11
WIP - fixing linter error
jjbustamante Jan 18, 2023
edc2bde
WIP - I removed the layout-repo volume and now every input (run-image…
jjbustamante Feb 3, 2023
7e5829e
WIP - Pointing to the imgutil version with name.ref annotation
jjbustamante Feb 3, 2023
abc51fe
WIP - fixing linting error
jjbustamante Feb 3, 2023
114e8b1
WIP - Pointing to the imgutil version with name.ref annotation
jjbustamante Feb 3, 2023
87cc8d3
WIP - tagging the image
jjbustamante Feb 6, 2023
256b0dc
root path for win or linux
jjbustamante Feb 17, 2023
4fe9225
adding layout-dir flag according to latest change in the lifecycle
jjbustamante Feb 20, 2023
8f83efc
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Feb 20, 2023
1185676
fixing lint error
jjbustamante Feb 20, 2023
79c7e54
adding unit test coverage for lifecycle_execution.go
jjbustamante Feb 21, 2023
64ea6b7
adding test coverage for build.go
jjbustamante Feb 21, 2023
70c5416
adding test coverage to the configuration files
jjbustamante Feb 22, 2023
d21e0c8
adding test coverage for input image reference
jjbustamante Feb 22, 2023
d3a9532
adding coverage for fetcher
jjbustamante Feb 22, 2023
fe63a00
adding test coverage for build
jjbustamante Feb 23, 2023
ead6d12
renaming some variables as it was before
jjbustamante Feb 23, 2023
2c636a7
fixing validation on previous image
jjbustamante Feb 23, 2023
6918797
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jkutner Feb 25, 2023
8cfab73
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Feb 27, 2023
3c1a801
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Mar 1, 2023
a3aa7fd
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Mar 4, 2023
7761d79
Fixing broken tests
jjbustamante Mar 6, 2023
4bca03f
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Mar 7, 2023
d2f1615
adding support for platform 0.11 and 0.12, fixing acceptance tests
jjbustamante Mar 7, 2023
18e758c
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Mar 7, 2023
5a02b07
Fixing test on windows
jjbustamante Mar 7, 2023
e038d46
Format issue
jjbustamante Mar 7, 2023
4f8cc8a
Merge branch 'main' into enhancement/issue-1548-imagee-in-oci-layout-…
jjbustamante Mar 8, 2023
12c370a
fixing windows tests, some of the tests are skip for now
jjbustamante Mar 8, 2023
fd9dc9e
fixing issue on windows
jjbustamante Mar 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require (
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.6.0
github.com/apex/log v1.9.0
github.com/buildpacks/imgutil v0.0.0-20230120191822-4d50b9a7e215
github.com/buildpacks/imgutil v0.0.0-20230221152838-4cf98dd677d2
github.com/buildpacks/lifecycle v0.16.0
github.com/docker/cli v23.0.1+incompatible
github.com/docker/docker v20.10.23+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/buildpacks/imgutil v0.0.0-20230120191822-4d50b9a7e215 h1:V/fmMFCX0jA73zqnKxmHnYq2dsWefpdTkytsorowbG0=
github.com/buildpacks/imgutil v0.0.0-20230120191822-4d50b9a7e215/go.mod h1:zL5lZzgFuv9l36n52FjomVrUHpyuZf6r1UHKaZ4LeSQ=
github.com/buildpacks/imgutil v0.0.0-20230221152838-4cf98dd677d2 h1:UjLEI78jFKLQwpFI2rpgKOZyXKW1cQdy7Wf+8Z6Lu1M=
github.com/buildpacks/imgutil v0.0.0-20230221152838-4cf98dd677d2/go.mod h1:zL5lZzgFuv9l36n52FjomVrUHpyuZf6r1UHKaZ4LeSQ=
github.com/buildpacks/lifecycle v0.16.0 h1:Q80RNP1JImJbkOXY/z/rWD9spqgEkTe/5/JypkOxJZ8=
github.com/buildpacks/lifecycle v0.16.0/go.mod h1:fiM5EwiDImyWA5kZ2fTNy0+bC4izwiCMR9rNsXbQnFc=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
Expand Down
17 changes: 16 additions & 1 deletion internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"path/filepath"
"strconv"

"github.com/buildpacks/lifecycle/api"
Expand Down Expand Up @@ -333,7 +334,15 @@ func (l *LifecycleExecution) Create(ctx context.Context, buildCache, launchCache
withEnv,
}

if l.opts.Publish {
if l.opts.Layout {
var err error
opts, err = l.appendLayoutOperations(opts)
if err != nil {
return err
}
}

if l.opts.Publish || l.opts.Layout {
authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, l.opts.Image.String(), l.opts.RunImage, l.opts.CacheImage, l.opts.PreviousImage)
if err != nil {
return err
Expand Down Expand Up @@ -737,6 +746,12 @@ func (l *LifecycleExecution) hasExtensions() bool {
return len(l.opts.Builder.OrderExtensions()) > 0
}

func (l *LifecycleExecution) appendLayoutOperations(opts []PhaseConfigProviderOperation) ([]PhaseConfigProviderOperation, error) {
layoutDir := filepath.Join(paths.RootDir, "layout-repo")
opts = append(opts, WithEnv("CNB_USE_LAYOUT=true", "CNB_LAYOUT_DIR="+layoutDir, "CNB_EXPERIMENTAL_MODE=warn"))
return opts, nil
}

func prependArg(arg string, args []string) []string {
return append([]string{arg}, args...)
}
Expand Down
13 changes: 13 additions & 0 deletions internal/build/lifecycle_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
providedClearCache bool
providedPublish bool
providedUseCreator bool
providedLayout bool
providedDockerHost string
providedNetworkMode = "some-network-mode"
providedRunImage = "some-run-image"
Expand Down Expand Up @@ -81,6 +82,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
opts.RunImage = providedRunImage
opts.UseCreator = providedUseCreator
opts.Volumes = providedVolumes
opts.Layout = providedLayout

targetImageRef, err := name.ParseReference(providedTargetImage)
h.AssertNil(t, err)
Expand Down Expand Up @@ -1106,6 +1108,17 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) {
})
})
})

when("layout", func() {
providedLayout = true
platformAPI = api.MustParse("0.12")

it("configures the phase with oci layout environment variables", func() {
h.AssertSliceContains(t, configProvider.ContainerConfig().Env, "CNB_USE_LAYOUT=true")
h.AssertSliceContains(t, configProvider.ContainerConfig().Env, "CNB_LAYOUT_DIR=/layout-repo")
h.AssertSliceContains(t, configProvider.ContainerConfig().Env, "CNB_EXPERIMENTAL_MODE=warn")
})
})
})

when("#Detect", func() {
Expand Down
3 changes: 3 additions & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var (
api.MustParse("0.8"),
api.MustParse("0.9"),
api.MustParse("0.10"),
api.MustParse("0.11"),
api.MustParse("0.12"),
}
)

Expand Down Expand Up @@ -79,6 +81,7 @@ type LifecycleOptions struct {
TrustBuilder bool
UseCreator bool
Interactive bool
Layout bool
Termui Termui
DockerHost string
Cache cache.CacheOpts
Expand Down
26 changes: 20 additions & 6 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type BuildFlags struct {
ClearCache bool
TrustBuilder bool
Interactive bool
Sparse bool
DockerHost string
CacheImage string
Cache cache.CacheOpts
Expand Down Expand Up @@ -68,11 +69,12 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
"be provided directly to build using `--builder`, or can be set using the `set-default-builder` command. For more " +
"on how to use `pack build`, see: https://buildpacks.io/docs/app-developer-guide/build-an-app/.",
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
if err := validateBuildFlags(&flags, cfg, packClient, logger); err != nil {
inputImageName := client.ParseInputImageReference(args[0])
if err := validateBuildFlags(&flags, cfg, inputImageName, logger); err != nil {
return err
}

imageName := args[0]
inputPreviousImage := client.ParseInputImageReference(flags.PreviousImage)

descriptor, actualDescriptorPath, err := parseProjectToml(flags.AppPath, flags.DescriptorPath)
if err != nil {
Expand Down Expand Up @@ -150,7 +152,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
AdditionalTags: flags.AdditionalTags,
RunImage: flags.RunImage,
Env: env,
Image: imageName,
Image: inputImageName.Name(),
Publish: flags.Publish,
DockerHost: flags.DockerHost,
PullPolicy: pullPolicy,
Expand All @@ -171,17 +173,23 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
Workspace: flags.Workspace,
LifecycleImage: lifecycleImage,
GroupID: gid,
PreviousImage: flags.PreviousImage,
PreviousImage: inputPreviousImage.Name(),
Interactive: flags.Interactive,
SBOMDestinationDir: flags.SBOMDestinationDir,
ReportDestinationDir: flags.ReportDestinationDir,
CreationTime: dateTime,
PreBuildpacks: flags.PreBuildpacks,
PostBuildpacks: flags.PostBuildpacks,
LayoutConfig: &client.LayoutConfig{
Sparse: flags.Sparse,
InputImage: inputImageName,
PreviousInputImage: inputPreviousImage,
LayoutRepoDir: cfg.LayoutRepositoryDir,
},
}); err != nil {
return errors.Wrap(err, "failed to build")
}
logger.Infof("Successfully built image %s", style.Symbol(imageName))
logger.Infof("Successfully built image %s", style.Symbol(inputImageName.Name()))
return nil
}),
}
Expand Down Expand Up @@ -248,12 +256,14 @@ This option may set DOCKER_HOST environment variable for the build container if
cmd.Flags().StringVar(&buildFlags.SBOMDestinationDir, "sbom-output-dir", "", "Path to export SBoM contents.\nOmitting the flag will yield no SBoM content.")
cmd.Flags().StringVar(&buildFlags.ReportDestinationDir, "report-output-dir", "", "Path to export build report.toml.\nOmitting the flag yield no report file.")
cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process")
cmd.Flags().BoolVar(&buildFlags.Sparse, "sparse", false, "Use this flag to avoid saving on disk the run-image layers when the application image is exported to OCI layout format")
if !cfg.Experimental {
cmd.Flags().MarkHidden("interactive")
cmd.Flags().MarkHidden("sparse")
}
}

func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error {
func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef client.InputImageReference, logger logging.Logger) error {
if flags.Registry != "" && !cfg.Experimental {
return client.NewExperimentError("Support for buildpack registries is currently experimental.")
}
Expand Down Expand Up @@ -282,6 +292,10 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackCli
return client.NewExperimentError("Interactive mode is currently experimental.")
}

if inputImageRef.Layout() && !cfg.Experimental {
return client.NewExperimentError("Exporting to OCI layout is currently experimental.")
}

return nil
}

Expand Down
85 changes: 85 additions & 0 deletions internal/commands/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/sclevine/spec/report"
"github.com/spf13/cobra"

"github.com/buildpacks/pack/internal/paths"

"github.com/buildpacks/pack/internal/commands"
"github.com/buildpacks/pack/internal/commands/testmocks"
"github.com/buildpacks/pack/internal/config"
Expand Down Expand Up @@ -866,6 +868,73 @@ builder = "my-builder"
})
})
})

when("export to OCI layout is expected but experimental isn't set in the config", func() {
it("errors with a descriptive message", func() {
command.SetArgs([]string{"oci:image", "--builder", "my-builder"})
err := command.Execute()
h.AssertNotNil(t, err)
h.AssertError(t, err, "Exporting to OCI layout is currently experimental.")
})
})
})

when("export to OCI layout is expected", func() {
var (
sparse bool
previousImage string
layoutDir string
)

it.Before(func() {
layoutDir = filepath.Join(paths.RootDir, "local", "repo")
previousImage = ""
cfg = config.Config{
Experimental: true,
LayoutRepositoryDir: layoutDir,
}
command = commands.Build(logger, cfg, mockClient)
})

when("path to save the image is provided", func() {
it("build is called with oci layout configuration", func() {
sparse = false
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)).
Return(nil)

command.SetArgs([]string{"oci:image", "--builder", "my-builder"})
err := command.Execute()
h.AssertNil(t, err)
})
})

when("previous-image flag is provided", func() {
it("build is called with oci layout configuration", func() {
sparse = false
previousImage = "my-previous-imagegit "
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)).
Return(nil)

command.SetArgs([]string{"oci:image", "--previous-image", "oci:my-previous-image", "--builder", "my-builder"})
err := command.Execute()
h.AssertNil(t, err)
})
})

when("-sparse flag is provided", func() {
it("build is called with oci layout configuration and sparse true", func() {
sparse = true
mockClient.EXPECT().
Build(gomock.Any(), EqBuildOptionsWithLayoutConfig("image", previousImage, sparse, layoutDir)).
Return(nil)

command.SetArgs([]string{"oci:image", "--sparse", "--builder", "my-builder"})
err := command.Execute()
h.AssertNil(t, err)
})
})
})
}

Expand Down Expand Up @@ -1035,6 +1104,22 @@ func EqBuildOptionsWithDateTime(t *time.Time) interface{} {
}
}

func EqBuildOptionsWithLayoutConfig(image, previousImage string, sparse bool, layoutDir string) interface{} {
return buildOptionsMatcher{
description: fmt.Sprintf("image=%s, previous-image=%s, sparse=%t, layout-dir=%s", image, previousImage, sparse, layoutDir),
equals: func(o client.BuildOptions) bool {
if o.Layout() {
result := o.Image == image
if previousImage != "" {
result = result && previousImage == o.PreviousImage
}
return result && o.LayoutConfig.Sparse == sparse && o.LayoutConfig.LayoutRepoDir == layoutDir
}
return false
},
}
}

type buildOptionsMatcher struct {
equals func(client.BuildOptions) bool
description string
Expand Down
6 changes: 6 additions & 0 deletions internal/commands/config_experimental.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"path/filepath"
"strconv"

"github.com/pkg/errors"
Expand Down Expand Up @@ -33,6 +34,11 @@ func ConfigExperimental(logger logging.Logger, cfg config.Config, cfgPath string
return errors.Wrapf(err, "invalid value %s provided", style.Symbol(args[0]))
}
cfg.Experimental = val
if cfg.Experimental {
cfg.LayoutRepositoryDir = filepath.Join(filepath.Dir(cfgPath), "layout-repo")
} else {
cfg.LayoutRepositoryDir = ""
}
Comment on lines +37 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little odd to me. Are we sure we want the default layout dir in the config dir?

Also, it looks like this isn't actually configurable right? It's either the default or nothing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for now, because the feature is experimental, I didn't want to do a lot of work giving the user a configuration option. basically, the idea is:

  • We need a place to save the run-image on disk, maybe the user doesn't care, that's why I am saving them in the pack home directory
  • I am planning to document this behavior in the docs so users can be aware of it.

The other alternative is, I could work on the user experience for configuring and customizing this kind of thing, but, I was planing to do it after shipping at least something the users can play with

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can see pack already save some data in the same folder, for example.

➜  > tree ~/.pack -L 1
/Users/jbustamante/.pack
├── completion.zsh
├── config.toml
├── download-cache
├── layout-repo
├── registry-a932275bd19c2d9e1b88fa06698fd2f5427a363d25bf87fa500691c373089381
├── registry2908354347
└── registry57772754

6 directories, 2 files

Those registry-* folders are already saving data there

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it just metadata that goes in this dir? Or does it actually store large files?

Copy link
Member Author

@jjbustamante jjbustamante Feb 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will save the run-image in OCI layout format, it could have all the blobs depending on whether the user set the sparse flag or not. But it could have large files, for sure


if err = config.Write(cfg, cfgPath); err != nil {
return errors.Wrap(err, "writing to config")
Expand Down
7 changes: 7 additions & 0 deletions internal/commands/config_experimental_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ func testConfigExperimental(t *testing.T, when spec.G, it spec.S) {
cfg, err := config.Read(configPath)
h.AssertNil(t, err)
h.AssertEq(t, cfg.Experimental, true)

// oci layout repo is configured
layoutDir := filepath.Join(filepath.Dir(configPath), "layout-repo")
h.AssertEq(t, cfg.LayoutRepositoryDir, layoutDir)
})

it("sets false if provided", func() {
Expand All @@ -87,6 +91,9 @@ func testConfigExperimental(t *testing.T, when spec.G, it spec.S) {
cfg, err := config.Read(configPath)
h.AssertNil(t, err)
h.AssertEq(t, cfg.Experimental, false)

// oci layout repo is cleaned
h.AssertEq(t, cfg.LayoutRepositoryDir, "")
})

it("returns error if invalid value provided", func() {
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Config struct {
Registries []Registry `toml:"registries,omitempty"`
LifecycleImage string `toml:"lifecycle-image,omitempty"`
RegistryMirrors map[string]string `toml:"registry-mirrors,omitempty"`
LayoutRepositoryDir string `toml:"layout-repo-dir,omitempty"`
}

type Registry struct {
Expand Down Expand Up @@ -75,7 +76,6 @@ func Read(path string) (Config, error) {
if err != nil && !os.IsNotExist(err) {
return Config{}, errors.Wrapf(err, "failed to read config file at path %s", path)
}

return cfg, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func testConfig(t *testing.T, when spec.G, it spec.S) {
h.AssertEq(t, len(subject.RunImages), 0)
h.AssertEq(t, subject.Experimental, false)
h.AssertEq(t, len(subject.RegistryMirrors), 0)
h.AssertEq(t, subject.LayoutRepositoryDir, "")
})
})
})
Expand Down
9 changes: 5 additions & 4 deletions internal/fakes/fake_image_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
)

type FetchArgs struct {
Daemon bool
PullPolicy image.PullPolicy
Platform string
Daemon bool
PullPolicy image.PullPolicy
Platform string
LayoutOption image.LayoutOption
}

type FakeImageFetcher struct {
Expand All @@ -30,7 +31,7 @@ func NewFakeImageFetcher() *FakeImageFetcher {
}

func (f *FakeImageFetcher) Fetch(ctx context.Context, name string, options image.FetchOptions) (imgutil.Image, error) {
f.FetchCalls[name] = &FetchArgs{Daemon: options.Daemon, PullPolicy: options.PullPolicy, Platform: options.Platform}
f.FetchCalls[name] = &FetchArgs{Daemon: options.Daemon, PullPolicy: options.PullPolicy, Platform: options.Platform, LayoutOption: options.LayoutOption}

ri, remoteFound := f.RemoteImages[name]

Expand Down
8 changes: 8 additions & 0 deletions internal/paths/defaults_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build linux || darwin
// +build linux darwin

package paths

const (
RootDir = `/`
)
Loading