Rework cache key generation a bit. (#375)

* Rework cache key generation a bit.

Cache keys are now based on the previous commands, rather than the previous state
of the filesystem.

* Refactor command interface a bit, only cache the context for commands that use it.
This commit is contained in:
dlorenc 2018-10-03 16:16:12 -05:00 committed by GitHub
parent 919df3949b
commit 734ffe65ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 169 additions and 163 deletions

View File

@ -29,6 +29,7 @@ import (
) )
type AddCommand struct { type AddCommand struct {
BaseCommand
cmd *instructions.AddCommand cmd *instructions.AddCommand
buildcontext string buildcontext string
snapshotFiles []string snapshotFiles []string
@ -110,7 +111,6 @@ func (a *AddCommand) String() string {
return a.cmd.String() return a.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached func (a *AddCommand) UsesContext() bool {
func (a *AddCommand) CacheCommand() bool { return true
return false
} }

View File

@ -24,6 +24,7 @@ import (
) )
type ArgCommand struct { type ArgCommand struct {
BaseCommand
cmd *instructions.ArgCommand cmd *instructions.ArgCommand
} }
@ -46,17 +47,7 @@ func (r *ArgCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
return nil return nil
} }
// FilesToSnapshot returns an empty array since this command only touches metadata.
func (r *ArgCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (r *ArgCommand) String() string { func (r *ArgCommand) String() string {
return r.cmd.String() return r.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (r *ArgCommand) CacheCommand() bool {
return false
}

View File

@ -0,0 +1,34 @@
/*
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package commands
type BaseCommand struct {
cache bool
usesContext bool
}
func (b *BaseCommand) CacheCommand() bool {
return b.cache
}
func (b *BaseCommand) UsesContext() bool {
return b.usesContext
}
func (b *BaseCommand) FilesToSnapshot() []string {
return []string{}
}

View File

@ -26,6 +26,7 @@ import (
) )
type CmdCommand struct { type CmdCommand struct {
BaseCommand
cmd *instructions.CmdCommand cmd *instructions.CmdCommand
} }
@ -52,17 +53,7 @@ func (c *CmdCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
return nil return nil
} }
// FilesToSnapshot returns an empty array since this is a metadata command
func (c *CmdCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (c *CmdCommand) String() string { func (c *CmdCommand) String() string {
return c.cmd.String() return c.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (c *CmdCommand) CacheCommand() bool {
return false
}

View File

@ -48,7 +48,7 @@ func TestExecuteCmd(t *testing.T) {
for _, test := range cmdTests { for _, test := range cmdTests {
cmd := CmdCommand{ cmd := CmdCommand{
&instructions.CmdCommand{ cmd: &instructions.CmdCommand{
ShellDependantCmdLine: instructions.ShellDependantCmdLine{ ShellDependantCmdLine: instructions.ShellDependantCmdLine{
PrependShell: test.prependShell, PrependShell: test.prependShell,
CmdLine: test.cmdLine, CmdLine: test.cmdLine,

View File

@ -37,6 +37,9 @@ type DockerCommand interface {
// Return true if this command should be true // Return true if this command should be true
// Currently only true for RUN // Currently only true for RUN
CacheCommand() bool CacheCommand() bool
// Return true if this command depends on the build context.
UsesContext() bool
} }
func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) {

View File

@ -29,6 +29,7 @@ import (
) )
type CopyCommand struct { type CopyCommand struct {
BaseCommand
cmd *instructions.CopyCommand cmd *instructions.CopyCommand
buildcontext string buildcontext string
snapshotFiles []string snapshotFiles []string
@ -103,7 +104,6 @@ func (c *CopyCommand) String() string {
return c.cmd.String() return c.cmd.String()
} }
// CacheCommand returns true since this command should be cached func (c *CopyCommand) UsesContext() bool {
func (c *CopyCommand) CacheCommand() bool { return true
return false
} }

View File

@ -26,6 +26,7 @@ import (
) )
type EntrypointCommand struct { type EntrypointCommand struct {
BaseCommand
cmd *instructions.EntrypointCommand cmd *instructions.EntrypointCommand
} }
@ -50,17 +51,7 @@ func (e *EntrypointCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
return nil return nil
} }
// FilesToSnapshot returns an empty array since this is a metadata command
func (e *EntrypointCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (e *EntrypointCommand) String() string { func (e *EntrypointCommand) String() string {
return e.cmd.String() return e.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (e *EntrypointCommand) CacheCommand() bool {
return false
}

View File

@ -48,7 +48,7 @@ func TestEntrypointExecuteCmd(t *testing.T) {
for _, test := range entrypointTests { for _, test := range entrypointTests {
cmd := EntrypointCommand{ cmd := EntrypointCommand{
&instructions.EntrypointCommand{ cmd: &instructions.EntrypointCommand{
ShellDependantCmdLine: instructions.ShellDependantCmdLine{ ShellDependantCmdLine: instructions.ShellDependantCmdLine{
PrependShell: test.prependShell, PrependShell: test.prependShell,
CmdLine: test.cmdLine, CmdLine: test.cmdLine,

View File

@ -25,6 +25,7 @@ import (
) )
type EnvCommand struct { type EnvCommand struct {
BaseCommand
cmd *instructions.EnvCommand cmd *instructions.EnvCommand
} }
@ -34,17 +35,7 @@ func (e *EnvCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
return util.UpdateConfigEnv(newEnvs, config, replacementEnvs) return util.UpdateConfigEnv(newEnvs, config, replacementEnvs)
} }
// We know that no files have changed, so return an empty array
func (e *EnvCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (e *EnvCommand) String() string { func (e *EnvCommand) String() string {
return e.cmd.String() return e.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (e *EnvCommand) CacheCommand() bool {
return false
}

View File

@ -33,7 +33,7 @@ func Test_EnvExecute(t *testing.T) {
} }
envCmd := &EnvCommand{ envCmd := &EnvCommand{
&instructions.EnvCommand{ cmd: &instructions.EnvCommand{
Env: []instructions.KeyValuePair{ Env: []instructions.KeyValuePair{
{ {
Key: "path", Key: "path",

View File

@ -29,6 +29,7 @@ import (
) )
type ExposeCommand struct { type ExposeCommand struct {
BaseCommand
cmd *instructions.ExposeCommand cmd *instructions.ExposeCommand
} }
@ -72,15 +73,6 @@ func validProtocol(protocol string) bool {
return false return false
} }
func (r *ExposeCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *ExposeCommand) String() string { func (r *ExposeCommand) String() string {
return r.cmd.String() return r.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (r *ExposeCommand) CacheCommand() bool {
return false
}

View File

@ -48,7 +48,7 @@ func TestUpdateExposedPorts(t *testing.T) {
} }
exposeCmd := &ExposeCommand{ exposeCmd := &ExposeCommand{
&instructions.ExposeCommand{ cmd: &instructions.ExposeCommand{
Ports: ports, Ports: ports,
}, },
} }
@ -77,7 +77,7 @@ func TestInvalidProtocol(t *testing.T) {
} }
exposeCmd := &ExposeCommand{ exposeCmd := &ExposeCommand{
&instructions.ExposeCommand{ cmd: &instructions.ExposeCommand{
Ports: ports, Ports: ports,
}, },
} }

View File

@ -23,6 +23,7 @@ import (
) )
type HealthCheckCommand struct { type HealthCheckCommand struct {
BaseCommand
cmd *instructions.HealthCheckCommand cmd *instructions.HealthCheckCommand
} }
@ -34,17 +35,7 @@ func (h *HealthCheckCommand) ExecuteCommand(config *v1.Config, buildArgs *docker
return nil return nil
} }
// FilesToSnapshot returns an empty array since this is a metadata command
func (h *HealthCheckCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (h *HealthCheckCommand) String() string { func (h *HealthCheckCommand) String() string {
return h.cmd.String() return h.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (h *HealthCheckCommand) CacheCommand() bool {
return false
}

View File

@ -26,6 +26,7 @@ import (
) )
type LabelCommand struct { type LabelCommand struct {
BaseCommand
cmd *instructions.LabelCommand cmd *instructions.LabelCommand
} }
@ -64,17 +65,7 @@ func updateLabels(labels []instructions.KeyValuePair, config *v1.Config, buildAr
} }
// No files have changed, this command only touches metadata.
func (r *LabelCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (r *LabelCommand) String() string { func (r *LabelCommand) String() string {
return r.cmd.String() return r.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (r *LabelCommand) CacheCommand() bool {
return false
}

View File

@ -24,6 +24,7 @@ import (
) )
type OnBuildCommand struct { type OnBuildCommand struct {
BaseCommand
cmd *instructions.OnbuildCommand cmd *instructions.OnbuildCommand
} }
@ -39,17 +40,7 @@ func (o *OnBuildCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile
return nil return nil
} }
// FilesToSnapshot returns that no files have changed, this command only touches metadata.
func (o *OnBuildCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (o *OnBuildCommand) String() string { func (o *OnBuildCommand) String() string {
return o.cmd.String() return o.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (o *OnBuildCommand) CacheCommand() bool {
return false
}

View File

@ -60,7 +60,7 @@ func TestExecuteOnbuild(t *testing.T) {
} }
onbuildCmd := &OnBuildCommand{ onbuildCmd := &OnBuildCommand{
&instructions.OnbuildCommand{ cmd: &instructions.OnbuildCommand{
Expression: test.expression, Expression: test.expression,
}, },
} }

View File

@ -35,6 +35,7 @@ import (
) )
type RunCommand struct { type RunCommand struct {
BaseCommand
cmd *instructions.RunCommand cmd *instructions.RunCommand
} }
@ -142,17 +143,15 @@ func addDefaultHOME(u string, envs []string) []string {
return append(envs, home) return append(envs, home)
} }
// FilesToSnapshot returns nil for this command because we don't know which files
// have changed, so we snapshot the entire system.
func (r *RunCommand) FilesToSnapshot() []string {
return nil
}
// String returns some information about the command for the image config // String returns some information about the command for the image config
func (r *RunCommand) String() string { func (r *RunCommand) String() string {
return r.cmd.String() return r.cmd.String()
} }
func (r *RunCommand) FilesToSnapshot() []string {
return nil
}
// CacheCommand returns true since this command should be cached // CacheCommand returns true since this command should be cached
func (r *RunCommand) CacheCommand() bool { func (r *RunCommand) CacheCommand() bool {
return true return true

View File

@ -23,6 +23,7 @@ import (
) )
type ShellCommand struct { type ShellCommand struct {
BaseCommand
cmd *instructions.ShellCommand cmd *instructions.ShellCommand
} }
@ -32,17 +33,7 @@ func (s *ShellCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.B
return nil return nil
} }
// FilesToSnapshot returns an empty array since this is a metadata command
func (s *ShellCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (s *ShellCommand) String() string { func (s *ShellCommand) String() string {
return s.cmd.String() return s.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (s *ShellCommand) CacheCommand() bool {
return false
}

View File

@ -45,7 +45,7 @@ func TestShellExecuteCmd(t *testing.T) {
for _, test := range shellTests { for _, test := range shellTests {
cmd := ShellCommand{ cmd := ShellCommand{
&instructions.ShellCommand{ cmd: &instructions.ShellCommand{
Shell: test.cmdLine, Shell: test.cmdLine,
}, },
} }

View File

@ -26,6 +26,7 @@ import (
) )
type StopSignalCommand struct { type StopSignalCommand struct {
BaseCommand
cmd *instructions.StopSignalCommand cmd *instructions.StopSignalCommand
} }
@ -52,17 +53,7 @@ func (s *StopSignalCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
return nil return nil
} }
// FilesToSnapshot returns an empty array since this is a metadata command
func (s *StopSignalCommand) FilesToSnapshot() []string {
return []string{}
}
// String returns some information about the command for the image config history // String returns some information about the command for the image config history
func (s *StopSignalCommand) String() string { func (s *StopSignalCommand) String() string {
return s.cmd.String() return s.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (s *StopSignalCommand) CacheCommand() bool {
return false
}

View File

@ -51,7 +51,7 @@ func TestStopsignalExecuteCmd(t *testing.T) {
for _, test := range stopsignalTests { for _, test := range stopsignalTests {
cmd := StopSignalCommand{ cmd := StopSignalCommand{
&instructions.StopSignalCommand{ cmd: &instructions.StopSignalCommand{
Signal: test.signal, Signal: test.signal,
}, },
} }

View File

@ -27,6 +27,7 @@ import (
) )
type UserCommand struct { type UserCommand struct {
BaseCommand
cmd *instructions.UserCommand cmd *instructions.UserCommand
} }
@ -59,15 +60,6 @@ func (r *UserCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
return nil return nil
} }
func (r *UserCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *UserCommand) String() string { func (r *UserCommand) String() string {
return r.cmd.String() return r.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (r *UserCommand) CacheCommand() bool {
return false
}

View File

@ -91,7 +91,7 @@ func TestUpdateUser(t *testing.T) {
}, },
} }
cmd := UserCommand{ cmd := UserCommand{
&instructions.UserCommand{ cmd: &instructions.UserCommand{
User: test.user, User: test.user,
}, },
} }

View File

@ -29,6 +29,7 @@ import (
) )
type VolumeCommand struct { type VolumeCommand struct {
BaseCommand
cmd *instructions.VolumeCommand cmd *instructions.VolumeCommand
snapshotFiles []string snapshotFiles []string
} }
@ -74,8 +75,3 @@ func (v *VolumeCommand) FilesToSnapshot() []string {
func (v *VolumeCommand) String() string { func (v *VolumeCommand) String() string {
return v.cmd.String() return v.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (v *VolumeCommand) CacheCommand() bool {
return false
}

View File

@ -29,6 +29,7 @@ import (
) )
type WorkdirCommand struct { type WorkdirCommand struct {
BaseCommand
cmd *instructions.WorkdirCommand cmd *instructions.WorkdirCommand
snapshotFiles []string snapshotFiles []string
} }
@ -66,8 +67,3 @@ func (w *WorkdirCommand) FilesToSnapshot() []string {
func (w *WorkdirCommand) String() string { func (w *WorkdirCommand) String() string {
return w.cmd.String() return w.cmd.String()
} }
// CacheCommand returns false since this command shouldn't be cached
func (w *WorkdirCommand) CacheCommand() bool {
return false
}

View File

@ -18,7 +18,6 @@ package executor
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -85,23 +84,6 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta
}, nil }, nil
} }
// key will return a string representation of the build at the cmd
func (s *stageBuilder) key(cmd string) (string, error) {
fsKey, err := s.snapshotter.Key()
if err != nil {
return "", err
}
c := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(c)
enc.Encode(s.cf)
cf, err := util.SHA256(c)
if err != nil {
return "", err
}
logrus.Debugf("%s\n%s\n%s\n%s\n", s.baseImageDigest, fsKey, cf, cmd)
return util.SHA256(bytes.NewReader([]byte(s.baseImageDigest + fsKey + cf + cmd)))
}
// extractCachedLayer will extract the cached layer and append it to the config file // extractCachedLayer will extract the cached layer and append it to the config file
func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) error { func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) error {
logrus.Infof("Found cached layer, extracting to filesystem") logrus.Infof("Found cached layer, extracting to filesystem")
@ -139,6 +121,15 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error {
return err return err
} }
var volumes []string var volumes []string
// Set the initial cache key to be the base image digest, the build args and the SrcContext.
compositeKey := NewCompositeCache(s.baseImageDigest)
contextHash, err := HashDir(opts.SrcContext)
if err != nil {
return err
}
compositeKey.AddKey(opts.BuildArgs...)
args := dockerfile.NewBuildArgs(opts.BuildArgs) args := dockerfile.NewBuildArgs(opts.BuildArgs)
for index, cmd := range s.stage.Commands { for index, cmd := range s.stage.Commands {
finalCmd := index == len(s.stage.Commands)-1 finalCmd := index == len(s.stage.Commands)-1
@ -149,13 +140,21 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error {
if command == nil { if command == nil {
continue continue
} }
logrus.Info(command.String())
cacheKey, err := s.key(command.String()) // Add the next command to the cache key.
if err != nil { compositeKey.AddKey(command.String())
return errors.Wrap(err, "getting key") if command.UsesContext() {
compositeKey.AddKey(contextHash)
} }
logrus.Info(command.String())
ck, err := compositeKey.Hash()
if err != nil {
return err
}
if command.CacheCommand() && opts.Cache { if command.CacheCommand() && opts.Cache {
image, err := cache.RetrieveLayer(opts, cacheKey) image, err := cache.RetrieveLayer(opts, ck)
if err == nil { if err == nil {
if err := s.extractCachedLayer(image, command.String()); err != nil { if err := s.extractCachedLayer(image, command.String()); err != nil {
return errors.Wrap(err, "extracting cached layer") return errors.Wrap(err, "extracting cached layer")
@ -222,7 +221,7 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error {
} }
// Push layer to cache now along with new config file // Push layer to cache now along with new config file
if command.CacheCommand() && opts.Cache { if command.CacheCommand() && opts.Cache {
if err := pushLayerToCache(opts, cacheKey, layer, command.String()); err != nil { if err := pushLayerToCache(opts, ck, layer, command.String()); err != nil {
return err return err
} }
} }

View File

@ -0,0 +1,76 @@
/*
Copyright 2018 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package executor
import (
"crypto/sha256"
"os"
"path/filepath"
"strings"
"github.com/GoogleContainerTools/kaniko/pkg/util"
)
// NewCompositeCache returns an initialized composite cache object.
func NewCompositeCache(initial ...string) *CompositeCache {
c := CompositeCache{
keys: initial,
}
return &c
}
// CompositeCache is a type that generates a cache key from a series of keys.
type CompositeCache struct {
keys []string
}
// AddKey adds the specified key to the sequence.
func (s *CompositeCache) AddKey(k ...string) {
s.keys = append(s.keys, k...)
}
// Key returns the human readable composite key as a string.
func (s *CompositeCache) Key() string {
return strings.Join(s.keys, "-")
}
// Hash returns the composite key in a string SHA256 format.
func (s *CompositeCache) Hash() (string, error) {
return util.SHA256(strings.NewReader(s.Key()))
}
// HashDir returns a hash of the directory.
func HashDir(p string) (string, error) {
sha := sha256.New()
if err := filepath.Walk(p, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
fileHash, err := util.CacheHasher()(path)
if err != nil {
return err
}
if _, err := sha.Write([]byte(fileHash)); err != nil {
return err
}
return nil
}); err != nil {
return "", err
}
return string(sha.Sum(nil)), nil
}