Skip to content

Commit

Permalink
Merge pull request #970 from dwillist/491_caching_layers_remote_registry
Browse files Browse the repository at this point in the history
Enable caching layers in image registry via `cache-image` flag
  • Loading branch information
jromero authored Jan 7, 2021
2 parents 297b981 + d45ad8e commit e9d285d
Show file tree
Hide file tree
Showing 16 changed files with 720 additions and 139 deletions.
49 changes: 49 additions & 0 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 35 additions & 0 deletions internal/build/fakes/cache.go
Original file line number Diff line number Diff line change
@@ -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
}
94 changes: 72 additions & 22 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -141,18 +150,18 @@ 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(
ctx,
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,
Expand All @@ -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,
Expand All @@ -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)),
}

Expand All @@ -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())),
)
}

Expand Down Expand Up @@ -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,
Expand All @@ -259,24 +287,25 @@ 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)
defer restore.Cleanup()
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
}
defer analyze.Cleanup()
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,
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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),
Expand All @@ -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)),
}

Expand All @@ -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
}
Expand Down
Loading

0 comments on commit e9d285d

Please sign in to comment.