Skip to content

Commit

Permalink
flags: Add --cache flag for build cache as image
Browse files Browse the repository at this point in the history
- Add `flag` definition
- Integrate flag options with image cache
- Add tests for flag options

Signed-off-by: Nitish Gupta <imnitish.ng@gmail.com>
  • Loading branch information
imnitishng committed Jul 6, 2022
1 parent 21a3f40 commit 6504e87
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 2 deletions.
8 changes: 6 additions & 2 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ func (l *LifecycleExecution) PrevImageName() string {
func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error {
phaseFactory := phaseFactoryCreator(l)
var buildCache Cache
if l.opts.CacheImage != "" {
cacheImage, err := name.ParseReference(l.opts.CacheImage, name.WeakValidation)
if l.opts.CacheImage != "" || l.opts.Cache.CacheType == "image" {
cacheImageName := l.opts.CacheImage
if cacheImageName == "" {
cacheImageName = l.opts.Cache.Source
}
cacheImage, err := name.ParseReference(cacheImageName, name.WeakValidation)
if err != nil {
return fmt.Errorf("invalid cache image name: %s", err)
}
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 @@ -77,6 +77,7 @@ type LifecycleOptions struct {
Interactive bool
Termui Termui
DockerHost string
Cache cache.CacheOpts
CacheImage string
HTTPProxy string
HTTPSProxy string
Expand Down
88 changes: 88 additions & 0 deletions internal/cache/cache_opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package cache

import (
"encoding/csv"
"fmt"
"strings"

"github.com/pkg/errors"
)

type CacheOpts struct {
CacheType string
Format string
Source string
}

func (c *CacheOpts) Set(value string) error {
csvReader := csv.NewReader(strings.NewReader(value))
csvReader.Comma = ';'
fields, err := csvReader.Read()
if err != nil {
return err
}

for _, field := range fields {
parts := strings.SplitN(field, "=", 2)

if len(parts) == 2 {
key := strings.ToLower(parts[0])
value := strings.ToLower(parts[1])
switch key {
case "type":
if value != "build" {
return errors.Errorf("invalid cache type '%s'", value)
}
c.CacheType = value
case "format":
if value != "image" {
return errors.Errorf("invalid cache format '%s'", value)
}
c.Format = value
case "name":
c.Source = value
}
}

if len(parts) != 2 {
return errors.Errorf("invalid field '%s' must be a key=value pair", field)
}
}

err = populateMissing(c)
if err != nil {
return err
}
return nil
}

func (c *CacheOpts) String() string {
var cacheFlag string
if c.CacheType != "" {
cacheFlag += fmt.Sprintf("type=%s;", c.CacheType)
}
if c.Format != "" {
cacheFlag += fmt.Sprintf("format=%s;", c.Format)
}
if c.Source != "" {
cacheFlag += fmt.Sprintf("name=%s", c.Source)
}
return cacheFlag
}

func (c *CacheOpts) Type() string {
return "cache"
}

func populateMissing(c *CacheOpts) error {
if c.CacheType == "" {
c.CacheType = "build"
}
if c.Format == "" {
c.Format = "volume"
}
if c.Source == "" {
return errors.Errorf("cache 'name' is required")
}
return nil
}
98 changes: 98 additions & 0 deletions internal/cache/cache_opts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package cache

import (
"testing"

"github.com/heroku/color"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"

h "github.com/buildpacks/pack/testhelpers"
)

type CacheOptTestCase struct {
name string
input string
output string
shouldFail bool
}

func TestMetadata(t *testing.T) {
color.Disable(true)
defer color.Disable(false)
spec.Run(t, "Metadata", testCacheOpts, spec.Sequential(), spec.Report(report.Terminal{}))
}

func testCacheOpts(t *testing.T, when spec.G, it spec.S) {
when("cache options are passed", func() {
it("image cache format with complete options", func() {
testcases := []CacheOptTestCase{
{
name: "Build cache as Image",
input: "type=build;format=image;name=io.test.io/myorg/my-cache:build",
output: "type=build;format=image;name=io.test.io/myorg/my-cache:build",
},
}

for _, testcase := range testcases {
var cacheFlags CacheOpts
t.Logf("Testing cache type: %s", testcase.name)
err := cacheFlags.Set(testcase.input)
h.AssertNil(t, err)
h.AssertEq(t, testcase.output, cacheFlags.String())
}
})

it("image cache format with missing options", func() {
successTestCases := []CacheOptTestCase{
{
name: "Build cache as Image missing: type",
input: "format=image;name=io.test.io/myorg/my-cache:build",
output: "type=build;format=image;name=io.test.io/myorg/my-cache:build",
},
{
name: "Build cache as Image missing: format",
input: "type=build;name=io.test.io/myorg/my-cache:build",
output: "type=build;format=volume;name=io.test.io/myorg/my-cache:build",
},
{
name: "Build cache as Image missing: type, format",
input: "name=io.test.io/myorg/my-cache:build",
output: "type=build;format=volume;name=io.test.io/myorg/my-cache:build",
},
{
name: "Build cache as Image missing: name",
input: "type=build;format=image",
output: "cache 'name' is required",
shouldFail: true,
},
{
name: "Build cache as Image missing: format, name",
input: "type=build",
output: "cache 'name' is required",
shouldFail: true,
},
{
name: "Build cache as Image missing: type, name",
input: "format=image",
output: "cache 'name' is required",
shouldFail: true,
},
}

for _, testcase := range successTestCases {
var cacheFlags CacheOpts
t.Logf("Testing cache type: %s", testcase.name)
err := cacheFlags.Set(testcase.input)

if testcase.shouldFail {
h.AssertError(t, err, testcase.output)
} else {
h.AssertNil(t, err)
output := cacheFlags.String()
h.AssertEq(t, testcase.output, output)
}
}
})
})
}
12 changes: 12 additions & 0 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"

"github.com/buildpacks/pack/internal/cache"
"github.com/buildpacks/pack/internal/config"
"github.com/buildpacks/pack/internal/style"
"github.com/buildpacks/pack/pkg/client"
Expand All @@ -28,6 +29,7 @@ type BuildFlags struct {
Interactive bool
DockerHost string
CacheImage string
Cache cache.CacheOpts
AppPath string
Builder string
Registry string
Expand Down Expand Up @@ -159,6 +161,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
DefaultProcessType: flags.DefaultProcessType,
ProjectDescriptorBaseDir: filepath.Dir(actualDescriptorPath),
ProjectDescriptor: descriptor,
Cache: flags.Cache,
CacheImage: flags.CacheImage,
Workspace: flags.Workspace,
LifecycleImage: lifecycleImage,
Expand Down Expand Up @@ -200,6 +203,7 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co
cmd.Flags().StringVarP(&buildFlags.AppPath, "path", "p", "", "Path to app dir or zip-formatted file (defaults to current working directory)")
cmd.Flags().StringSliceVarP(&buildFlags.Buildpacks, "buildpack", "b", nil, "Buildpack to use. One of:\n a buildpack by id and version in the form of '<buildpack>@<version>',\n path to a buildpack directory (not supported on Windows),\n path/URL to a buildpack .tar or .tgz file, or\n a packaged buildpack image name in the form of '<hostname>/<repo>[:<tag>]'"+stringSliceHelp("buildpack"))
cmd.Flags().StringVarP(&buildFlags.Builder, "builder", "B", cfg.DefaultBuilder, "Builder image")
cmd.Flags().Var(&buildFlags.Cache, "cache", "Cache used to define different cache options.")
cmd.Flags().StringVar(&buildFlags.CacheImage, "cache-image", "", `Cache build layers in remote registry. Requires --publish`)
cmd.Flags().BoolVar(&buildFlags.ClearCache, "clear-cache", false, "Clear image's associated cache before building")
cmd.Flags().StringVar(&buildFlags.DateTime, "creation-time", "", "Desired create time in the output image config. Accepted values are Unix timestamps (e.g., '1641013200'), or 'now'. Platform API version must be at least 0.9 to use this feature.")
Expand Down Expand Up @@ -237,6 +241,14 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackCli
return client.NewExperimentError("Support for buildpack registries is currently experimental.")
}

if flags.Cache.String() != "" && flags.CacheImage != "" {
return errors.New("'cache' flag cannot be used with 'cache-image' flag.")
}

if flags.Cache.Format == "image" && !flags.Publish {
return errors.New("image cache format requires the 'publish' flag.")
}

if flags.CacheImage != "" && !flags.Publish {
return errors.New("cache-image flag requires the publish flag")
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/buildpacks/pack/internal/build"
"github.com/buildpacks/pack/internal/builder"
"github.com/buildpacks/pack/internal/cache"
internalConfig "github.com/buildpacks/pack/internal/config"
pname "github.com/buildpacks/pack/internal/name"
"github.com/buildpacks/pack/internal/stack"
Expand Down Expand Up @@ -106,6 +107,9 @@ type BuildOptions struct {
// Buildpacks may both read and overwrite these values.
Env map[string]string

// Used to configure various cache available options
Cache cache.CacheOpts

// Option only valid if Publish is true
// Create an additional image that contains cache=true layers and push it to the registry.
CacheImage string
Expand Down Expand Up @@ -345,6 +349,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
TrustBuilder: opts.TrustBuilder(opts.Builder),
UseCreator: false,
DockerHost: opts.DockerHost,
Cache: opts.Cache,
CacheImage: opts.CacheImage,
HTTPProxy: proxyConfig.HTTPProxy,
HTTPSProxy: proxyConfig.HTTPSProxy,
Expand Down

0 comments on commit 6504e87

Please sign in to comment.