diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index ec32ad2c2..555dcf991 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -1797,6 +1797,33 @@ func testAcceptance( }) }) + when("--cache with options for build cache as bind", func() { + var bindCacheDir, cacheFlags string + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.Cache), "") + cacheBindName := fmt.Sprintf("%s-bind", repoName) + bindCacheDir, err := ioutil.TempDir("", cacheBindName) + assert.Nil(err) + cacheFlags = fmt.Sprintf("type=build;format=bind;name=%s", bindCacheDir) + }) + + it("creates image and cache image on the registry", func() { + buildArgs := []string{ + repoName, + "-p", filepath.Join("testdata", "mock_app"), + "--cache", + cacheFlags, + } + + output := pack.RunSuccessfully("build", buildArgs...) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulImageBuild(repoName) + + t.Log("checking that bind mount has cache contents") + assert.FileExists(fmt.Sprintf("%s/committed", bindCacheDir)) + defer os.RemoveAll(bindCacheDir) + }) + }) + when("ctrl+c", func() { it("stops the execution", func() { var buf = new(bytes.Buffer) diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index b04eb22ae..314fa9cf6 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -135,10 +135,16 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF } buildCache = cache.NewImageCache(cacheImage, l.docker) } else { - buildCache = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Build, "build", l.docker) + switch l.opts.Cache.Build.Format { + case cache.CacheVolume: + buildCache = cache.NewVolumeCache(l.opts.Image, l.opts.Cache.Build, "build", l.docker) + l.logger.Debugf("Using build cache volume %s", style.Symbol(buildCache.Name())) + case cache.CacheBind: + buildCache = cache.NewBindCache(l.opts.Cache.Build, l.docker) + l.logger.Debugf("Using build cache dir %s", style.Symbol(buildCache.Name())) + } } - l.logger.Debugf("Using build cache volume %s", style.Symbol(buildCache.Name())) if l.opts.ClearCache { if err := buildCache.Clear(ctx); err != nil { return errors.Wrap(err, "clearing build cache") @@ -251,7 +257,7 @@ func (l *LifecycleExecution) Create(ctx context.Context, publish bool, dockerHos case cache.Image: flags = append(flags, "-cache-image", buildCache.Name()) cacheOpts = WithBinds(volumes...) - case cache.Volume: + case cache.Volume, cache.Bind: cacheOpts = WithBinds(append(volumes, fmt.Sprintf("%s:%s", buildCache.Name(), l.mountPaths.cacheDir()))...) } diff --git a/internal/cache/bind_cache.go b/internal/cache/bind_cache.go new file mode 100644 index 000000000..757db613e --- /dev/null +++ b/internal/cache/bind_cache.go @@ -0,0 +1,36 @@ +package cache + +import ( + "context" + "os" + + "github.com/docker/docker/client" +) + +type BindCache struct { + docker client.CommonAPIClient + bind string +} + +func NewBindCache(cacheType CacheInfo, dockerClient client.CommonAPIClient) *BindCache { + return &BindCache{ + bind: cacheType.Source, + docker: dockerClient, + } +} + +func (c *BindCache) Name() string { + return c.bind +} + +func (c *BindCache) Clear(ctx context.Context) error { + err := os.RemoveAll(c.bind) + if err != nil { + return err + } + return nil +} + +func (c *BindCache) Type() Type { + return Bind +} diff --git a/internal/cache/cache_opts.go b/internal/cache/cache_opts.go index 4f2c7ef99..9c1b59e39 100644 --- a/internal/cache/cache_opts.go +++ b/internal/cache/cache_opts.go @@ -3,6 +3,7 @@ package cache import ( "encoding/csv" "fmt" + "path/filepath" "strings" "github.com/pkg/errors" @@ -21,6 +22,7 @@ type CacheOpts struct { const ( CacheVolume Format = iota CacheImage + CacheBind ) func (f Format) String() string { @@ -29,6 +31,8 @@ func (f Format) String() string { return "image" case CacheVolume: return "volume" + case CacheBind: + return "bind" } return "" } @@ -76,6 +80,8 @@ func (c *CacheOpts) Set(value string) error { cache.Format = CacheImage case "volume": cache.Format = CacheVolume + case "bind": + cache.Format = CacheBind default: return errors.Errorf("invalid cache format '%s'", value) } @@ -84,7 +90,7 @@ func (c *CacheOpts) Set(value string) error { } } - err = populateMissing(c) + err = sanitize(c) if err != nil { return err } @@ -102,9 +108,30 @@ func (c *CacheOpts) Type() string { return "cache" } -func populateMissing(c *CacheOpts) error { - if (c.Build.Source == "" && c.Build.Format == CacheImage) || (c.Launch.Source == "" && c.Launch.Format == CacheImage) { +func sanitize(c *CacheOpts) error { + if (c.Build.Source == "" && c.Build.Format == CacheImage) || + (c.Build.Source == "" && c.Build.Format == CacheBind) || + (c.Launch.Source == "" && c.Launch.Format == CacheImage) || + (c.Launch.Source == "" && c.Launch.Format == CacheBind) { return errors.Errorf("cache 'name' is required") } + + if c.Build.Format == CacheBind || c.Launch.Format == CacheBind { + var ( + resolvedPath string + err error + ) + if c.Build.Format == CacheBind { + if resolvedPath, err = filepath.Abs(c.Build.Source); err != nil { + return errors.Wrap(err, "resolve absolute path") + } + c.Build.Source = filepath.Join(resolvedPath, "build-cache") + } else { + if resolvedPath, err = filepath.Abs(c.Launch.Source); err != nil { + return errors.Wrap(err, "resolve absolute path") + } + c.Launch.Source = filepath.Join(resolvedPath, "launch-cache") + } + } return nil } diff --git a/internal/cache/cache_opts_test.go b/internal/cache/cache_opts_test.go index 13499eed1..99ae92312 100644 --- a/internal/cache/cache_opts_test.go +++ b/internal/cache/cache_opts_test.go @@ -1,6 +1,10 @@ package cache import ( + "fmt" + "os" + "runtime" + "strings" "testing" "github.com/heroku/color" @@ -93,9 +97,6 @@ func testCacheOpts(t *testing.T, when spec.G, it spec.S) { for _, testcase := range successTestCases { var cacheFlags CacheOpts t.Logf("Testing cache type: %s", testcase.name) - if testcase.name == "Everything missing" { - print("i am here") - } err := cacheFlags.Set(testcase.input) if testcase.shouldFail { @@ -208,4 +209,91 @@ func testCacheOpts(t *testing.T, when spec.G, it spec.S) { } }) }) + + when("bind cache format options are passed", func() { + it("with complete options", func() { + var testcases []CacheOptTestCase + homeDir, err := os.UserHomeDir() + h.AssertNil(t, err) + cwd, err := os.Getwd() + h.AssertNil(t, err) + + if runtime.GOOS != "windows" { + testcases = []CacheOptTestCase{ + { + name: "Build cache as bind", + input: fmt.Sprintf("type=build;format=bind;name=%s/test-bind-build-cache", homeDir), + output: fmt.Sprintf("type=build;format=bind;name=%s/test-bind-build-cache/build-cache;type=launch;format=volume;name=;", homeDir), + }, + { + name: "Build cache as bind with relative path", + input: "type=build;format=bind;name=./test-bind-build-cache-relative", + output: fmt.Sprintf("type=build;format=bind;name=%s/test-bind-build-cache-relative/build-cache;type=launch;format=volume;name=;", cwd), + }, + { + name: "Launch cache as bind", + input: fmt.Sprintf("type=launch;format=bind;name=%s/test-bind-volume-cache", homeDir), + output: fmt.Sprintf("type=build;format=volume;name=;type=launch;format=bind;name=%s/test-bind-volume-cache/launch-cache;", homeDir), + }, + } + } else { + testcases = []CacheOptTestCase{ + { + name: "Build cache as bind", + input: fmt.Sprintf("type=build;format=bind;name=%s\\test-bind-build-cache", homeDir), + output: fmt.Sprintf("type=build;format=bind;name=%s\\test-bind-build-cache\\build-cache;type=launch;format=volume;name=;", homeDir), + }, + { + name: "Build cache as bind with relative path", + input: "type=build;format=bind;name=.\\test-bind-build-cache-relative", + output: fmt.Sprintf("type=build;format=bind;name=%s\\test-bind-build-cache-relative\\build-cache;type=launch;format=volume;name=;", cwd), + }, + { + name: "Launch cache as bind", + input: fmt.Sprintf("type=launch;format=bind;name=%s\\test-bind-volume-cache", homeDir), + output: fmt.Sprintf("type=build;format=volume;name=;type=launch;format=bind;name=%s\\test-bind-volume-cache\\launch-cache;", homeDir), + }, + } + } + + 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, strings.ToLower(testcase.output), strings.ToLower(cacheFlags.String())) + } + }) + + it("with missing options", func() { + successTestCases := []CacheOptTestCase{ + { + name: "Launch cache as bind missing: name", + input: "type=launch;format=bind", + output: "cache 'name' is required", + shouldFail: true, + }, + { + name: "Launch cache as Volume missing: type, name", + input: "format=bind", + 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) + } + } + }) + }) } diff --git a/internal/cache/consts.go b/internal/cache/consts.go index 80ae0ef1c..8a1098519 100644 --- a/internal/cache/consts.go +++ b/internal/cache/consts.go @@ -3,6 +3,7 @@ package cache const ( Image Type = iota Volume + Bind ) type Type int