diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index d43c09852..09e4ed885 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -1914,6 +1914,55 @@ func testAcceptance( }) }) + when("--cache-image", func() { + var cacheImageName string + var cacheImage string + it.Before(func() { + cacheImageName = fmt.Sprintf("%s-cache", repoName) + cacheImage = fmt.Sprintf("%s-cache", repo) + }) + + it("creates image and cache image on the registry", func() { + h.SkipUnless(t, + pack.Supports("build --cache-image"), + "pack does not support 'package-buildpack'", + ) + + buildArgs := []string{ + repoName, + "-p", filepath.Join("testdata", "mock_app"), + "--publish", + "--cache-image", + cacheImageName, + } + if dockerHostOS() != "windows" { + buildArgs = append(buildArgs, "--network", "host") + } + + output := pack.RunSuccessfully("build", buildArgs...) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulImageBuild(repoName) + + cacheImageRef, err := name.ParseReference(cacheImageName, name.WeakValidation) + assert.Nil(err) + + t.Log("checking that registry has contents") + contents, err := registryConfig.RegistryCatalog() + assert.Nil(err) + if !strings.Contains(contents, repo) { + t.Fatalf("Expected to see image %s in %s", repo, contents) + } + + if !strings.Contains(contents, cacheImage) { + t.Fatalf("Expected to see image %s in %s", cacheImage, contents) + } + + assert.Succeeds(h.PullImageWithAuth(dockerCli, repoName, registryConfig.RegistryAuth())) + assert.Succeeds(h.PullImageWithAuth(dockerCli, cacheImageRef.Name(), registryConfig.RegistryAuth())) + defer h.DockerRmi(dockerCli, repoName) + defer h.DockerRmi(dockerCli, cacheImageRef.Name()) + }) + }) + when("ctrl+c", func() { it("stops the execution", func() { var buf = new(bytes.Buffer) diff --git a/build.go b/build.go index 8921e4a71..826af7fbd 100644 --- a/build.go +++ b/build.go @@ -103,6 +103,10 @@ type BuildOptions struct { // Buildpacks may both read and overwrite these values. Env map[string]string + // Option only valid if Publish is true + // Create an additional image that contains cache=true layers and push it to the registry. + CacheImage string + // Option passed directly to the lifecycle. // If true, publishes Image directly to a registry. // Assumes Image contains a valid registry with credentials @@ -291,6 +295,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { Volumes: processedVolumes, DefaultProcessType: opts.DefaultProcessType, FileFilter: fileFilter, + CacheImage: opts.CacheImage, } lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version diff --git a/build_test.go b/build_test.go index de85b631e..bd73c1bc7 100644 --- a/build_test.go +++ b/build_test.go @@ -638,6 +638,25 @@ func testBuild(t *testing.T, when spec.G, it spec.S) { }) }) + when("ImageCache option", func() { + it("passes it through to lifecycle", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + CacheImage: "some-cache-image", + })) + h.AssertEq(t, fakeLifecycle.Opts.CacheImage, "some-cache-image") + }) + + it("defaults to false", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + })) + h.AssertEq(t, fakeLifecycle.Opts.CacheImage, "") + }) + }) + when("Buildpacks option", func() { assertOrderEquals := func(content string) { t.Helper() diff --git a/internal/build/fakes/cache.go b/internal/build/fakes/cache.go new file mode 100644 index 000000000..911506071 --- /dev/null +++ b/internal/build/fakes/cache.go @@ -0,0 +1,35 @@ +package fakes + +import ( + "context" + + "github.com/buildpacks/pack/internal/cache" +) + +type FakeCache struct { + ReturnForType cache.Type + ReturnForClear error + ReturnForName string + + TypeCallCount int + ClearCallCount int + NameCallCount int +} + +func NewFakeCache() *FakeCache { + return &FakeCache{} +} + +func (f *FakeCache) Type() cache.Type { + f.TypeCallCount++ + return f.ReturnForType +} + +func (f *FakeCache) Clear(ctx context.Context) error { + f.ClearCallCount++ + return f.ReturnForClear +} +func (f *FakeCache) Name() string { + f.NameCallCount++ + return f.ReturnForName +} diff --git a/internal/build/lifecycle_execution.go b/internal/build/lifecycle_execution.go index a55638560..e307dd204 100644 --- a/internal/build/lifecycle_execution.go +++ b/internal/build/lifecycle_execution.go @@ -9,6 +9,7 @@ import ( "github.com/buildpacks/lifecycle/auth" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "github.com/buildpacks/pack/internal/builder" @@ -103,8 +104,16 @@ func (l *LifecycleExecution) PlatformAPI() *api.Version { func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error { phaseFactory := phaseFactoryCreator(l) - - buildCache := cache.NewVolumeCache(l.opts.Image, "build", l.docker) + var buildCache Cache + if l.opts.CacheImage != "" { + cacheImage, err := name.ParseReference(l.opts.CacheImage, name.WeakValidation) + if err != nil { + return fmt.Errorf("invalid cache image name: %s", err) + } + buildCache = cache.NewImageCache(cacheImage, l.docker) + } else { + buildCache = cache.NewVolumeCache(l.opts.Image, "build", l.docker) + } l.logger.Debugf("Using build cache volume %s", style.Symbol(buildCache.Name())) if l.opts.ClearCache { @@ -123,14 +132,14 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF } l.logger.Info(style.Step("ANALYZING")) - if err := l.Analyze(ctx, l.opts.Image.String(), buildCache.Name(), l.opts.Network, l.opts.Publish, l.opts.ClearCache, phaseFactory); err != nil { + if err := l.Analyze(ctx, l.opts.Image.String(), l.opts.Network, l.opts.Publish, l.opts.ClearCache, buildCache, phaseFactory); err != nil { return err } l.logger.Info(style.Step("RESTORING")) if l.opts.ClearCache { l.logger.Info("Skipping 'restore' due to clearing cache") - } else if err := l.Restore(ctx, buildCache.Name(), l.opts.Network, phaseFactory); err != nil { + } else if err := l.Restore(ctx, l.opts.Network, buildCache, phaseFactory); err != nil { return err } @@ -141,7 +150,7 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF } l.logger.Info(style.Step("EXPORTING")) - return l.Export(ctx, l.opts.Image.String(), l.opts.AdditionalTags, l.opts.RunImage, l.opts.Publish, launchCache.Name(), buildCache.Name(), l.opts.Network, phaseFactory) + return l.Export(ctx, l.opts.Image.String(), l.opts.RunImage, l.opts.Publish, l.opts.Network, buildCache, launchCache, l.opts.AdditionalTags, phaseFactory) } return l.Create( @@ -149,10 +158,10 @@ func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseF l.opts.Publish, l.opts.ClearCache, l.opts.RunImage, - launchCache.Name(), - buildCache.Name(), l.opts.Image.String(), l.opts.Network, + buildCache, + launchCache, l.opts.AdditionalTags, l.opts.Volumes, phaseFactory, @@ -173,7 +182,8 @@ func (l *LifecycleExecution) Cleanup() error { func (l *LifecycleExecution) Create( ctx context.Context, publish, clearCache bool, - runImage, launchCacheName, cacheName, repoName, networkMode string, + runImage, repoName, networkMode string, + buildCache, launchCache Cache, additionalTags []string, volumes []string, phaseFactory PhaseFactory, @@ -192,11 +202,20 @@ func (l *LifecycleExecution) Create( flags = append(flags, "-process-type", processType) } + var cacheOpts PhaseConfigProviderOperation + switch buildCache.Type() { + case cache.Image: + flags = append(flags, "-cache-image", buildCache.Name()) + cacheOpts = WithBinds(volumes...) + case cache.Volume: + cacheOpts = WithBinds(append(volumes, fmt.Sprintf("%s:%s", buildCache.Name(), l.mountPaths.cacheDir()))...) + } + opts := []PhaseConfigProviderOperation{ WithFlags(l.withLogLevel(flags...)...), WithArgs(repoName), WithNetwork(networkMode), - WithBinds(append(volumes, fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir()))...), + cacheOpts, WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.opts.FileFilter)), } @@ -211,7 +230,7 @@ func (l *LifecycleExecution) Create( opts = append(opts, WithDaemonAccess(), WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()), - WithBinds(fmt.Sprintf("%s:%s", launchCacheName, l.mountPaths.launchCacheDir())), + WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())), ) } @@ -244,7 +263,16 @@ func (l *LifecycleExecution) Detect(ctx context.Context, networkMode string, vol return detect.Run(ctx) } -func (l *LifecycleExecution) Restore(ctx context.Context, cacheName, networkMode string, phaseFactory PhaseFactory) error { +func (l *LifecycleExecution) Restore(ctx context.Context, networkMode string, buildCache Cache, phaseFactory PhaseFactory) error { + flagsOpt := NullOp() + cacheOpt := NullOp() + switch buildCache.Type() { + case cache.Image: + flagsOpt = WithFlags("-cache-image", buildCache.Name()) + case cache.Volume: + cacheOpt = WithBinds(fmt.Sprintf("%s:%s", buildCache.Name(), l.mountPaths.cacheDir())) + } + configProvider := NewPhaseConfigProvider( "restorer", l, @@ -259,7 +287,8 @@ func (l *LifecycleExecution) Restore(ctx context.Context, cacheName, networkMode )..., ), WithNetwork(networkMode), - WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), + flagsOpt, + cacheOpt, ) restore := phaseFactory.New(configProvider) @@ -267,8 +296,8 @@ func (l *LifecycleExecution) Restore(ctx context.Context, cacheName, networkMode return restore.Run(ctx) } -func (l *LifecycleExecution) Analyze(ctx context.Context, repoName, cacheName, networkMode string, publish, clearCache bool, phaseFactory PhaseFactory) error { - analyze, err := l.newAnalyze(repoName, cacheName, networkMode, publish, clearCache, phaseFactory) +func (l *LifecycleExecution) Analyze(ctx context.Context, repoName, networkMode string, publish, clearCache bool, cache Cache, phaseFactory PhaseFactory) error { + analyze, err := l.newAnalyze(repoName, networkMode, publish, clearCache, cache, phaseFactory) if err != nil { return err } @@ -276,7 +305,7 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, repoName, cacheName, n return analyze.Run(ctx) } -func (l *LifecycleExecution) newAnalyze(repoName, cacheName, networkMode string, publish, clearCache bool, phaseFactory PhaseFactory) (RunnerCleaner, error) { +func (l *LifecycleExecution) newAnalyze(repoName, networkMode string, publish, clearCache bool, buildCache Cache, phaseFactory PhaseFactory) (RunnerCleaner, error) { args := []string{ "-layers", l.mountPaths.layersDir(), repoName, @@ -287,6 +316,17 @@ func (l *LifecycleExecution) newAnalyze(repoName, cacheName, networkMode string, args = append([]string{"-cache-dir", l.mountPaths.cacheDir()}, args...) } + cacheOpt := NullOp() + flagsOpt := NullOp() + switch buildCache.Type() { + case cache.Image: + if !clearCache { + flagsOpt = WithFlags("-cache-image", buildCache.Name()) + } + case cache.Volume: + cacheOpt = WithBinds(fmt.Sprintf("%s:%s", buildCache.Name(), l.mountPaths.cacheDir())) + } + if publish { authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, repoName) if err != nil { @@ -303,7 +343,8 @@ func (l *LifecycleExecution) newAnalyze(repoName, cacheName, networkMode string, WithRoot(), WithArgs(l.withLogLevel(args...)...), WithNetwork(networkMode), - WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), + flagsOpt, + cacheOpt, ) return phaseFactory.New(configProvider), nil @@ -328,8 +369,9 @@ func (l *LifecycleExecution) newAnalyze(repoName, cacheName, networkMode string, )..., )..., ), + flagsOpt, WithNetwork(networkMode), - WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), + cacheOpt, ) return phaseFactory.New(configProvider), nil @@ -365,7 +407,7 @@ func determineDefaultProcessType(platformAPI *api.Version, providedValue string) return providedValue } -func (l *LifecycleExecution) newExport(repoName string, additionalTags []string, runImage string, publish bool, launchCacheName, cacheName, networkMode string, phaseFactory PhaseFactory) (RunnerCleaner, error) { +func (l *LifecycleExecution) newExport(repoName, runImage string, publish bool, networkMode string, buildCache, launchCache Cache, additionalTags []string, phaseFactory PhaseFactory) (RunnerCleaner, error) { flags := []string{ "-cache-dir", l.mountPaths.cacheDir(), "-layers", l.mountPaths.layersDir(), @@ -379,6 +421,14 @@ func (l *LifecycleExecution) newExport(repoName string, additionalTags []string, flags = append(flags, "-process-type", processType) } + cacheOpt := NullOp() + switch buildCache.Type() { + case cache.Image: + flags = append(flags, "-cache-image", buildCache.Name()) + case cache.Volume: + cacheOpt = WithBinds(fmt.Sprintf("%s:%s", buildCache.Name(), l.mountPaths.cacheDir())) + } + opts := []PhaseConfigProviderOperation{ WithLogPrefix("exporter"), WithImage(l.opts.LifecycleImage), @@ -392,7 +442,7 @@ func (l *LifecycleExecution) newExport(repoName string, additionalTags []string, WithArgs(append([]string{repoName}, additionalTags...)...), WithRoot(), WithNetwork(networkMode), - WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), + cacheOpt, WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)), } @@ -412,15 +462,15 @@ func (l *LifecycleExecution) newExport(repoName string, additionalTags []string, opts, WithDaemonAccess(), WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()), - WithBinds(fmt.Sprintf("%s:%s", launchCacheName, l.mountPaths.launchCacheDir())), + WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())), ) } return phaseFactory.New(NewPhaseConfigProvider("exporter", l, opts...)), nil } -func (l *LifecycleExecution) Export(ctx context.Context, repoName string, additionalTags []string, runImage string, publish bool, launchCacheName, cacheName, networkMode string, phaseFactory PhaseFactory) error { - export, err := l.newExport(repoName, additionalTags, runImage, publish, launchCacheName, cacheName, networkMode, phaseFactory) +func (l *LifecycleExecution) Export(ctx context.Context, repoName string, runImage string, publish bool, networkMode string, buildCache, launchCache Cache, additionalTags []string, phaseFactory PhaseFactory) error { + export, err := l.newExport(repoName, runImage, publish, networkMode, buildCache, launchCache, additionalTags, phaseFactory) if err != nil { return err } diff --git a/internal/build/lifecycle_execution_test.go b/internal/build/lifecycle_execution_test.go index 72491ddf9..2ec26cb3b 100644 --- a/internal/build/lifecycle_execution_test.go +++ b/internal/build/lifecycle_execution_test.go @@ -3,12 +3,15 @@ package build_test import ( "bytes" "context" + "fmt" "io/ioutil" "math/rand" "os" "testing" "time" + "github.com/buildpacks/pack/internal/cache" + "github.com/google/go-containerregistry/pkg/name" "github.com/apex/log" @@ -179,9 +182,49 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { } }) }) + + when("Error cases", func() { + when("passed invalid cache-image", func() { + it("fails", func() { + opts := build.LifecycleOptions{ + Publish: false, + ClearCache: false, + RunImage: "test", + Image: imageName, + Builder: fakeBuilder, + TrustBuilder: false, + UseCreator: false, + CacheImage: "%%%", + } + + lifecycle, err := build.NewLifecycleExecution(logger, docker, opts) + h.AssertNil(t, err) + + err = lifecycle.Run(context.Background(), func(execution *build.LifecycleExecution) build.PhaseFactory { + return fakePhaseFactory + }) + + h.AssertError(t, err, fmt.Sprintf("invalid cache image name: %s", "could not parse reference: %%!(NOVERB)")) + }) + }) + }) }) when("#Create", func() { + var ( + fakeBuildCache *fakes.FakeCache + fakeLaunchCache *fakes.FakeCache + ) + it.Before(func() { + fakeBuildCache = fakes.NewFakeCache() + fakeBuildCache.ReturnForType = cache.Volume + fakeBuildCache.ReturnForName = "some-cache" + + fakeLaunchCache = fakes.NewFakeCache() + fakeLaunchCache.ReturnForType = cache.Volume + fakeLaunchCache.ReturnForName = "some-launch-cache" + }) + it("creates a phase and then run it", func() { lifecycle := newTestLifecycleExec(t, false) fakePhase := &fakes.FakePhase{} @@ -194,8 +237,8 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", - "test", - "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -217,10 +260,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { false, false, expectedRunImage, - "test", - "test", expectedRepoName, "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -251,9 +294,9 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { false, "test", "test", - "test", - "test", expectedNetworkMode, + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -279,8 +322,8 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", - "test", - "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -311,8 +354,44 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", + fakeBuildCache, + fakeLaunchCache, + []string{}, + []string{}, + fakePhaseFactory, + ) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "creator") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-dir", "/cache"}, + ) + }) + }) + + when("using a cache image", func() { + it.Before(func() { + fakeBuildCache.ReturnForType = cache.Image + fakeBuildCache.ReturnForName = "some-cache-image" + }) + it("configures the phase with the expected arguments", func() { + verboseLifecycle := newTestLifecycleExec(t, true) + fakePhaseFactory := fakes.NewFakePhaseFactory() + + err := verboseLifecycle.Create( + context.Background(), + false, + true, + "test", "test", "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -326,8 +405,11 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, configProvider.Name(), "creator") h.AssertIncludeAllExpectedPatterns(t, configProvider.ContainerConfig().Cmd, - []string{"-cache-dir", "/cache"}, + []string{"-skip-restore"}, + []string{"-cache-image", "some-cache-image"}, ) + + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") }) }) @@ -344,8 +426,8 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", - "test", - "test", + fakes.NewFakeCache(), + fakes.NewFakeCache(), additionalTags, []string{}, fakePhaseFactory, @@ -364,6 +446,20 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) when("publish", func() { + var ( + fakeBuildCache *fakes.FakeCache + fakeLaunchCache *fakes.FakeCache + ) + it.Before(func() { + fakeBuildCache = fakes.NewFakeCache() + fakeBuildCache.ReturnForName = "some-cache" + fakeBuildCache.ReturnForType = cache.Volume + + fakeLaunchCache = fakes.NewFakeCache() + fakeLaunchCache.ReturnForType = cache.Volume + fakeLaunchCache.ReturnForName = "some-launch-cache" + }) + it("configures the phase with binds", func() { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() @@ -376,9 +472,9 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { false, "test", "test", - "some-cache", - "test", "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{volumeMount}, fakePhaseFactory, @@ -403,8 +499,8 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", - "test", - "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -428,10 +524,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { true, false, "test", - "test", - "test", expectedRepos, "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -445,14 +541,68 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { h.AssertSliceContains(t, configProvider.ContainerConfig().Env, "CNB_REGISTRY_AUTH={}") }) + when("using a cache image", func() { + it.Before(func() { + fakeBuildCache.ReturnForType = cache.Image + fakeBuildCache.ReturnForName = "some-cache-image" + }) + + it("configures the phase with the expected arguments", func() { + verboseLifecycle := newTestLifecycleExec(t, true) + fakePhaseFactory := fakes.NewFakePhaseFactory() + + err := verboseLifecycle.Create( + context.Background(), + true, + true, + "test", + "test", + "test", + fakeBuildCache, + fakeLaunchCache, + []string{}, + []string{}, + fakePhaseFactory, + ) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "creator") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-skip-restore"}, + []string{"-cache-image", "some-cache-image"}, + ) + + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + }) + }) + when("platform 0.3", func() { + var ( + fakeBuildCache *fakes.FakeCache + fakeLaunchCache *fakes.FakeCache + ) + it.Before(func() { + fakeBuildCache = fakes.NewFakeCache() + fakeBuildCache.ReturnForName = "some-cache" + fakeBuildCache.ReturnForType = cache.Volume + + fakeLaunchCache = fakes.NewFakeCache() + fakeLaunchCache.ReturnForType = cache.Volume + fakeLaunchCache.ReturnForName = "some-launch-cache" + }) + it("doesn't hint at default process type", func() { fakeBuilder, err := fakes.NewFakeBuilder(fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.3")})) h.AssertNil(t, err) lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -470,7 +620,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -483,6 +633,19 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) when("publish is false", func() { + var ( + fakeBuildCache *fakes.FakeCache + fakeLaunchCache *fakes.FakeCache + ) + it.Before(func() { + fakeBuildCache = fakes.NewFakeCache() + fakeBuildCache.ReturnForName = "some-cache" + fakeBuildCache.ReturnForType = cache.Volume + + fakeLaunchCache = fakes.NewFakeCache() + fakeLaunchCache.ReturnForType = cache.Volume + fakeLaunchCache.ReturnForName = "some-launch-cache" + }) it("configures the phase with the expected arguments", func() { verboseLifecycle := newTestLifecycleExec(t, true) fakePhaseFactory := fakes.NewFakePhaseFactory() @@ -494,8 +657,8 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { "test", "test", "test", - "test", - "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -523,10 +686,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { false, false, "test", - "some-launch-cache", - "some-cache", "test", "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{}, fakePhaseFactory, @@ -552,10 +715,10 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { false, false, "test", - "some-launch-cache", - "some-cache", "test", "test", + fakeBuildCache, + fakeLaunchCache, []string{}, []string{volumeMount}, fakePhaseFactory, @@ -576,7 +739,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -594,7 +757,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -676,12 +839,17 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) when("#Analyze", func() { + var fakeCache *fakes.FakeCache + it.Before(func() { + fakeCache = fakes.NewFakeCache() + fakeCache.ReturnForType = cache.Volume + }) it("creates a phase and then runs it", func() { lifecycle := newTestLifecycleExec(t, false) fakePhase := &fakes.FakePhase{} fakePhaseFactory := fakes.NewFakePhaseFactory(fakes.WhichReturnsForNew(fakePhase)) - err := lifecycle.Analyze(context.Background(), "test", "test", "test", false, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) h.AssertEq(t, fakePhase.CleanupCallCount, 1) @@ -694,7 +862,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedRepoName := "some-repo-name" - err := lifecycle.Analyze(context.Background(), expectedRepoName, "test", "test", false, true, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), expectedRepoName, "test", false, true, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -712,7 +880,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedRepoName := "some-repo-name" - err := lifecycle.Analyze(context.Background(), expectedRepoName, "test", "test", false, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), expectedRepoName, "test", false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -727,6 +895,53 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) }) + when("using a cache image", func() { + var ( + lifecycle *build.LifecycleExecution + fakePhaseFactory *fakes.FakePhaseFactory + expectedRepoName = "some-repo-name" + ) + it.Before(func() { + fakeCache.ReturnForType = cache.Image + fakeCache.ReturnForName = "some-cache-image" + + lifecycle = newTestLifecycleExec(t, false) + fakePhaseFactory = fakes.NewFakePhaseFactory() + }) + it("configures the phase with a build cache images", func() { + err := lifecycle.Analyze(context.Background(), expectedRepoName, "", false, false, fakeCache, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "analyzer") + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-image", "some-cache-image"}, + ) + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-dir", "/cache"}, + ) + }) + when("clear-cache", func() { + it("cache is omitted from Analyze", func() { + err := lifecycle.Analyze(context.Background(), expectedRepoName, "", false, true, fakeCache, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "analyzer") + h.AssertSliceNotContains(t, configProvider.ContainerConfig().Cmd, "-cache-image") + }) + }) + }) + when("publish", func() { it("runs the phase with the lifecycle image", func() { lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) { @@ -734,7 +949,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Analyze(context.Background(), "test", "test", "test", true, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -750,7 +965,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Analyze(context.Background(), "test", "test", "test", true, false, fakePhaseFactory) + err = lifecycle.Analyze(context.Background(), "test", "test", true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -767,7 +982,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { expectedRepos := "some-repo-name" expectedNetworkMode := "some-network-mode" - err := lifecycle.Analyze(context.Background(), expectedRepos, "test", expectedNetworkMode, true, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), expectedRepos, expectedNetworkMode, true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -782,7 +997,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Analyze(context.Background(), "test", "test", "test", true, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -797,7 +1012,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedRepoName := "some-repo-name" - err := verboseLifecycle.Analyze(context.Background(), expectedRepoName, "test", "test", true, false, fakePhaseFactory) + err := verboseLifecycle.Analyze(context.Background(), expectedRepoName, "test", true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -814,11 +1029,12 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) it("configures the phase with binds", func() { + fakeCache.ReturnForName = "some-cache" lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBind := "some-cache:/cache" - err := lifecycle.Analyze(context.Background(), "test", "some-cache", "test", true, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", true, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -827,6 +1043,36 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBind) }) + + when("using a cache image", func() { + it.Before(func() { + fakeCache.ReturnForName = "some-cache-image" + fakeCache.ReturnForType = cache.Image + }) + + it("configures the phase with a build cache images", func() { + lifecycle := newTestLifecycleExec(t, false) + fakePhaseFactory := fakes.NewFakePhaseFactory() + expectedRepoName := "some-repo-name" + + err := lifecycle.Analyze(context.Background(), expectedRepoName, "test", true, false, fakeCache, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-image", "some-cache-image"}, + ) + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-dir", "/cache"}, + ) + }) + }) }) when("publish is false", func() { @@ -836,7 +1082,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Analyze(context.Background(), "test", "test", "test", false, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -852,7 +1098,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Analyze(context.Background(), "test", "test", "test", false, false, fakePhaseFactory) + err = lifecycle.Analyze(context.Background(), "test", "test", false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -867,7 +1113,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Analyze(context.Background(), "test", "test", "test", false, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -883,7 +1129,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedRepoName := "some-repo-name" - err := verboseLifecycle.Analyze(context.Background(), expectedRepoName, "test", "test", false, true, fakePhaseFactory) + err := verboseLifecycle.Analyze(context.Background(), expectedRepoName, "test", false, true, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -905,7 +1151,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedNetworkMode := "some-network-mode" - err := lifecycle.Analyze(context.Background(), "test", "test", expectedNetworkMode, false, false, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", expectedNetworkMode, false, false, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -916,11 +1162,12 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) it("configures the phase with binds", func() { + fakeCache.ReturnForName = "some-cache" lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBind := "some-cache:/cache" - err := lifecycle.Analyze(context.Background(), "test", "some-cache", "test", false, true, fakePhaseFactory) + err := lifecycle.Analyze(context.Background(), "test", "test", false, true, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -933,13 +1180,19 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) when("#Restore", func() { + var fakeCache *fakes.FakeCache + it.Before(func() { + fakeCache = fakes.NewFakeCache() + fakeCache.ReturnForName = "some-cache" + fakeCache.ReturnForType = cache.Volume + }) it("runs the phase with the lifecycle image", func() { lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) { options.LifecycleImage = "some-lifecycle-image" }) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Restore(context.Background(), "test", "test", fakePhaseFactory) + err := lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -955,7 +1208,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Restore(context.Background(), "test", "test", fakePhaseFactory) + err = lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -971,7 +1224,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhase := &fakes.FakePhase{} fakePhaseFactory := fakes.NewFakePhaseFactory(fakes.WhichReturnsForNew(fakePhase)) - err := lifecycle.Restore(context.Background(), "test", "test", fakePhaseFactory) + err := lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) h.AssertEq(t, fakePhase.CleanupCallCount, 1) @@ -982,7 +1235,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Restore(context.Background(), "test", "test", fakePhaseFactory) + err := lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -996,7 +1249,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { verboseLifecycle := newTestLifecycleExec(t, true) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := verboseLifecycle.Restore(context.Background(), "test", "test", fakePhaseFactory) + err := verboseLifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1017,7 +1270,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedNetworkMode := "some-network-mode" - err := lifecycle.Restore(context.Background(), "test", expectedNetworkMode, fakePhaseFactory) + err := lifecycle.Restore(context.Background(), expectedNetworkMode, fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1032,7 +1285,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBind := "some-cache:/cache" - err := lifecycle.Restore(context.Background(), "some-cache", "test", fakePhaseFactory) + err := lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1041,6 +1294,34 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] h.AssertSliceContains(t, configProvider.HostConfig().Binds, expectedBind) }) + when("using cache image", func() { + var ( + lifecycle *build.LifecycleExecution + fakePhaseFactory *fakes.FakePhaseFactory + ) + + it.Before(func() { + fakeCache.ReturnForType = cache.Image + fakeCache.ReturnForName = "some-cache-image" + + lifecycle = newTestLifecycleExec(t, false) + fakePhaseFactory = fakes.NewFakePhaseFactory() + }) + it("configures the phase with a cache image", func() { + err := lifecycle.Restore(context.Background(), "test", fakeCache, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-image", "some-cache-image"}, + ) + }) + }) }) when("#Build", func() { @@ -1111,12 +1392,27 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) when("#Export", func() { + var ( + fakeBuildCache *fakes.FakeCache + fakeLaunchCache *fakes.FakeCache + ) + + it.Before(func() { + fakeBuildCache = fakes.NewFakeCache() + fakeBuildCache.ReturnForType = cache.Volume + fakeBuildCache.ReturnForName = "some-cache" + + fakeLaunchCache = fakes.NewFakeCache() + fakeLaunchCache.ReturnForType = cache.Volume + fakeLaunchCache.ReturnForName = "some-launch-cache" + }) + it("creates a phase and then runs it", func() { lifecycle := newTestLifecycleExec(t, false) fakePhase := &fakes.FakePhase{} fakePhaseFactory := fakes.NewFakePhaseFactory(fakes.WhichReturnsForNew(fakePhase)) - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) h.AssertEq(t, fakePhase.CleanupCallCount, 1) @@ -1129,7 +1425,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { expectedRepoName := "some-repo-name" expectedRunImage := "some-run-image" - err := verboseLifecycle.Export(context.Background(), expectedRepoName, []string{}, expectedRunImage, false, "test", "test", "test", fakePhaseFactory) + err := verboseLifecycle.Export(context.Background(), expectedRepoName, expectedRunImage, false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1156,7 +1452,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { expectedRunImage := "some-run-image" additionalTags := []string{"additional-tag-1", "additional-tag-2"} - err := verboseLifecycle.Export(context.Background(), expectedRepoName, additionalTags, expectedRunImage, false, "test", "test", "test", fakePhaseFactory) + err := verboseLifecycle.Export(context.Background(), expectedRepoName, expectedRunImage, false, "test", fakes.NewFakeCache(), fakes.NewFakeCache(), additionalTags, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1176,6 +1472,35 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) }) + when("using cache image", func() { + it.Before(func() { + fakeBuildCache.ReturnForType = cache.Image + fakeBuildCache.ReturnForName = "some-cache-image" + }) + + it("configures phase with cache image", func() { + verboseLifecycle := newTestLifecycleExec(t, true) + fakePhaseFactory := fakes.NewFakePhaseFactory() + expectedRepoName := "some-repo-name" + expectedRunImage := "some-run-image" + + err := verboseLifecycle.Export(context.Background(), expectedRepoName, expectedRunImage, false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "exporter") + + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-image", "some-cache-image"}, + ) + }) + }) + when("publish", func() { it("runs the phase with the lifecycle image", func() { lifecycle := newTestLifecycleExec(t, true, func(options *build.LifecycleOptions) { @@ -1183,7 +1508,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1199,7 +1524,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1215,7 +1540,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedRepos := []string{"some-repo-name", "some-run-image"} - err := lifecycle.Export(context.Background(), expectedRepos[0], []string{}, expectedRepos[1], true, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), expectedRepos[0], expectedRepos[1], true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1229,7 +1554,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1244,7 +1569,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedNetworkMode := "some-network-mode" - err := lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", expectedNetworkMode, fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", true, expectedNetworkMode, fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1259,7 +1584,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBind := "some-cache:/cache" - err := lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "some-cache", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1274,7 +1599,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBinds := []string{"some-cache:/cache", "some-launch-cache:/launch-cache"} - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "some-launch-cache", "some-cache", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1294,7 +1619,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedDefaultProc := []string{"-process-type", "test-process"} - err := lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1304,6 +1629,35 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { h.AssertIncludeAllExpectedPatterns(t, configProvider.ContainerConfig().Cmd, expectedDefaultProc) }) + when("using cache image and publishing", func() { + it.Before(func() { + fakeBuildCache.ReturnForType = cache.Image + fakeBuildCache.ReturnForName = "some-cache-image" + }) + + it("configures phase with cache image", func() { + verboseLifecycle := newTestLifecycleExec(t, true) + fakePhaseFactory := fakes.NewFakePhaseFactory() + expectedRepoName := "some-repo-name" + expectedRunImage := "some-run-image" + + err := verboseLifecycle.Export(context.Background(), expectedRepoName, expectedRunImage, true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) + h.AssertNil(t, err) + + lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 + h.AssertNotEq(t, lastCallIndex, -1) + + configProvider := fakePhaseFactory.NewCalledWithProvider[lastCallIndex] + h.AssertEq(t, configProvider.Name(), "exporter") + + h.AssertSliceNotContains(t, configProvider.HostConfig().Binds, ":/cache") + h.AssertIncludeAllExpectedPatterns(t, + configProvider.ContainerConfig().Cmd, + []string{"-cache-image", "some-cache-image"}, + ) + }) + }) + when("platform 0.3", func() { it("doesn't hint at default process type", func() { fakeBuilder, err := fakes.NewFakeBuilder(fakes.WithSupportedPlatformAPIs([]*api.Version{api.MustParse("0.3")})) @@ -1311,7 +1665,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1329,7 +1683,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", true, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", true, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1348,7 +1702,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { }) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1364,7 +1718,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1379,7 +1733,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, false) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1394,7 +1748,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { verboseLifecycle := newTestLifecycleExec(t, true) fakePhaseFactory := fakes.NewFakePhaseFactory() - err := verboseLifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err := verboseLifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1414,7 +1768,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedNetworkMode := "some-network-mode" - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", expectedNetworkMode, fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, expectedNetworkMode, fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1429,7 +1783,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBinds := []string{"some-cache:/cache", "some-launch-cache:/launch-cache"} - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "some-launch-cache", "some-cache", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1444,7 +1798,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedBinds := []string{"some-cache:/cache", "some-launch-cache:/launch-cache"} - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "some-launch-cache", "some-cache", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1464,7 +1818,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { fakePhaseFactory := fakes.NewFakePhaseFactory() expectedDefaultProc := []string{"-process-type", "test-process"} - err := lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err := lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1481,7 +1835,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 @@ -1499,7 +1853,7 @@ func testLifecycleExecution(t *testing.T, when spec.G, it spec.S) { lifecycle := newTestLifecycleExec(t, true, fakes.WithBuilder(fakeBuilder)) fakePhaseFactory := fakes.NewFakePhaseFactory() - err = lifecycle.Export(context.Background(), "test", []string{}, "test", false, "test", "test", "test", fakePhaseFactory) + err = lifecycle.Export(context.Background(), "test", "test", false, "test", fakeBuildCache, fakeLaunchCache, []string{}, fakePhaseFactory) h.AssertNil(t, err) lastCallIndex := len(fakePhaseFactory.NewCalledWithProvider) - 1 diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index 92374bab8..f73a48445 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -5,6 +5,8 @@ import ( "math/rand" "time" + "github.com/buildpacks/pack/internal/cache" + "github.com/buildpacks/imgutil" "github.com/buildpacks/lifecycle/api" "github.com/docker/docker/client" @@ -39,6 +41,7 @@ type LifecycleExecutor struct { type Cache interface { Name() string Clear(context.Context) error + Type() cache.Type } func init() { @@ -55,6 +58,7 @@ type LifecycleOptions struct { Publish bool TrustBuilder bool UseCreator bool + CacheImage string HTTPProxy string HTTPSProxy string NoProxy string diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index 03338b012..6c2dfd438 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -86,6 +86,10 @@ func (p *PhaseConfigProvider) InfoWriter() io.Writer { return p.infoWriter } +func NullOp() PhaseConfigProviderOperation { + return func(provider *PhaseConfigProvider) {} +} + func WithArgs(args ...string) PhaseConfigProviderOperation { return func(provider *PhaseConfigProvider) { provider.ctrConf.Cmd = append(provider.ctrConf.Cmd, args...) diff --git a/internal/cache/consts.go b/internal/cache/consts.go new file mode 100644 index 000000000..80ae0ef1c --- /dev/null +++ b/internal/cache/consts.go @@ -0,0 +1,8 @@ +package cache + +const ( + Image Type = iota + Volume +) + +type Type int diff --git a/internal/cache/image_cache.go b/internal/cache/image_cache.go index 187ad4858..158c00a02 100644 --- a/internal/cache/image_cache.go +++ b/internal/cache/image_cache.go @@ -2,8 +2,6 @@ package cache import ( "context" - "crypto/sha256" - "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/client" @@ -16,9 +14,8 @@ type ImageCache struct { } func NewImageCache(imageRef name.Reference, dockerClient client.CommonAPIClient) *ImageCache { - sum := sha256.Sum256([]byte(imageRef.Name())) return &ImageCache{ - image: fmt.Sprintf("pack-cache-%x", sum[:6]), + image: imageRef.Name(), docker: dockerClient, } } @@ -36,3 +33,7 @@ func (c *ImageCache) Clear(ctx context.Context) error { } return nil } + +func (c *ImageCache) Type() Type { + return Image +} diff --git a/internal/cache/image_cache_test.go b/internal/cache/image_cache_test.go index a6c133f60..904f6936f 100644 --- a/internal/cache/image_cache_test.go +++ b/internal/cache/image_cache_test.go @@ -38,38 +38,17 @@ func testImageCache(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) }) - it("reusing the same cache for the same repo name", func() { - ref, err := name.ParseReference("my/repo", name.WeakValidation) - h.AssertNil(t, err) - subject := cache.NewImageCache(ref, dockerClient) - expected := cache.NewImageCache(ref, dockerClient) - if subject.Name() != expected.Name() { - t.Fatalf("The same repo name should result in the same volume") - } - }) - - it("supplies different images for different tags", func() { - ref, err := name.ParseReference("my/repo:other-tag", name.WeakValidation) - h.AssertNil(t, err) - subject := cache.NewImageCache(ref, dockerClient) - ref, err = name.ParseReference("my/repo", name.WeakValidation) - h.AssertNil(t, err) - notExpected := cache.NewImageCache(ref, dockerClient) - if subject.Name() == notExpected.Name() { - t.Fatalf("Different image tags should result in different images") - } - }) - - it("supplies different images for different registries", func() { - ref, err := name.ParseReference("registry.com/my/repo:other-tag", name.WeakValidation) - h.AssertNil(t, err) - subject := cache.NewImageCache(ref, dockerClient) - ref, err = name.ParseReference("my/repo", name.WeakValidation) - h.AssertNil(t, err) - notExpected := cache.NewImageCache(ref, dockerClient) - if subject.Name() == notExpected.Name() { - t.Fatalf("Different image registries should result in different images") - } + when("#Name", func() { + it("should return the image reference used in intialization", func() { + refName := "gcr.io/my/repo:tag" + ref, err := name.ParseReference(refName, name.WeakValidation) + h.AssertNil(t, err) + subject := cache.NewImageCache(ref, dockerClient) + actual := subject.Name() + if actual != refName { + t.Fatalf("Incorrect cache name expected %s, got %s", refName, actual) + } + }) }) it("resolves implied tag", func() { @@ -97,6 +76,26 @@ func testImageCache(t *testing.T, when spec.G, it spec.S) { }) }) + when("#Type", func() { + var ( + dockerClient client.CommonAPIClient + ) + + it.Before(func() { + var err error + dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) + h.AssertNil(t, err) + }) + + it("returns the cache type", func() { + ref, err := name.ParseReference("my/repo", name.WeakValidation) + h.AssertNil(t, err) + subject := cache.NewImageCache(ref, dockerClient) + expected := cache.Image + h.AssertEq(t, subject.Type(), expected) + }) + }) + when("#Clear", func() { var ( imageName string diff --git a/internal/cache/volume_cache.go b/internal/cache/volume_cache.go index 9d73f0fd0..25abd7f3d 100644 --- a/internal/cache/volume_cache.go +++ b/internal/cache/volume_cache.go @@ -37,3 +37,7 @@ func (c *VolumeCache) Clear(ctx context.Context) error { } return nil } + +func (c *VolumeCache) Type() Type { + return Volume +} diff --git a/internal/cache/volume_cache_test.go b/internal/cache/volume_cache_test.go index aca95d219..2f1388fe8 100644 --- a/internal/cache/volume_cache_test.go +++ b/internal/cache/volume_cache_test.go @@ -29,15 +29,14 @@ func TestVolumeCache(t *testing.T) { } func testCache(t *testing.T, when spec.G, it spec.S) { - when("#NewVolumeCache", func() { - var dockerClient client.CommonAPIClient - - it.Before(func() { - var err error - dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) - h.AssertNil(t, err) - }) + var dockerClient client.CommonAPIClient + it.Before(func() { + var err error + dockerClient, err = client.NewClientWithOpts(client.FromEnv, client.WithVersion("1.38")) + h.AssertNil(t, err) + }) + when("#NewVolumeCache", func() { it("adds suffix to calculated name", func() { ref, err := name.ParseReference("my/repo", name.WeakValidation) h.AssertNil(t, err) @@ -159,4 +158,14 @@ func testCache(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("#Type", func() { + it("returns the cache type", func() { + ref, err := name.ParseReference("my/repo", name.WeakValidation) + h.AssertNil(t, err) + subject := cache.NewVolumeCache(ref, "some-suffix", dockerClient) + expected := cache.Volume + h.AssertEq(t, subject.Type(), expected) + }) + }) } diff --git a/internal/commands/build.go b/internal/commands/build.go index a50a5bb12..af100e090 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -22,6 +22,7 @@ type BuildFlags struct { Publish bool ClearCache bool TrustBuilder bool + CacheImage string AppPath string Builder string Registry string @@ -113,6 +114,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob DefaultProcessType: flags.DefaultProcessType, ProjectDescriptorBaseDir: filepath.Dir(actualDescriptorPath), ProjectDescriptor: descriptor, + CacheImage: flags.CacheImage, }); err != nil { return errors.Wrap(err, "failed to build") } @@ -145,6 +147,7 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co cmd.Flags().StringVarP(&buildFlags.DefaultProcessType, "default-process", "D", "", `Set the default process type. (default "web")`) cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`) cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to."+multiValueHelp("tag")) + cmd.Flags().StringVar(&buildFlags.CacheImage, "cache-image", "", `Cache build layers in remote registry. Requires --publish`) } func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error { @@ -157,6 +160,10 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackCli return pack.NewExperimentError("Support for buildpack registries is currently experimental.") } + if flags.CacheImage != "" && !flags.Publish { + return errors.New("cache-image flag requires the publish flag") + } + return nil } diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 5cae8ebe3..ba4e64f28 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -287,6 +287,30 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) { }) }) + when("a cache-image passed", func() { + when("--publish is not used", func() { + it("errors", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithCacheImage("some-cache-image")). + Return(nil) + + command.SetArgs([]string{"--builder", "my-builder", "image", "--cache-image", "some-cache-image"}) + err := command.Execute() + h.AssertError(t, err, "cache-image flag requires the publish flag") + }) + }) + when("--publish is used", func() { + it("succeeds", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithCacheImage("some-cache-image")). + Return(nil) + + command.SetArgs([]string{"--builder", "my-builder", "image", "--cache-image", "some-cache-image", "--publish"}) + h.AssertNil(t, command.Execute()) + }) + }) + }) + when("env vars are passed as flags", func() { var ( tmpVar = "tmpVar" @@ -535,6 +559,15 @@ func EqBuildOptionsWithPullPolicy(policy pubcfg.PullPolicy) gomock.Matcher { } } +func EqBuildOptionsWithCacheImage(cacheImage string) gomock.Matcher { + return buildOptionsMatcher{ + description: fmt.Sprintf("CacheImage=%s", cacheImage), + equals: func(o pack.BuildOptions) bool { + return o.CacheImage == cacheImage + }, + } +} + func EqBuildOptionsWithNetwork(network string) gomock.Matcher { return buildOptionsMatcher{ description: fmt.Sprintf("Network=%s", network), diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 29f3e3f89..9af740cf5 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -509,7 +509,7 @@ func RunE(cmd *exec.Cmd) (string, error) { func PullImageWithAuth(dockerCli client.CommonAPIClient, ref, registryAuth string) error { rc, err := dockerCli.ImagePull(context.Background(), ref, dockertypes.ImagePullOptions{RegistryAuth: registryAuth}) if err != nil { - return nil + return err } if _, err := io.Copy(ioutil.Discard, rc); err != nil { return err