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

View File

@ -24,6 +24,7 @@ import (
)
type ArgCommand struct {
BaseCommand
cmd *instructions.ArgCommand
}
@ -46,17 +47,7 @@ func (r *ArgCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
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
func (r *ArgCommand) String() 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 {
BaseCommand
cmd *instructions.CmdCommand
}
@ -52,17 +53,7 @@ func (c *CmdCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
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
func (c *CmdCommand) String() 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 {
cmd := CmdCommand{
&instructions.CmdCommand{
cmd: &instructions.CmdCommand{
ShellDependantCmdLine: instructions.ShellDependantCmdLine{
PrependShell: test.prependShell,
CmdLine: test.cmdLine,

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import (
)
type EntrypointCommand struct {
BaseCommand
cmd *instructions.EntrypointCommand
}
@ -50,17 +51,7 @@ func (e *EntrypointCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
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
func (e *EntrypointCommand) String() 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 {
cmd := EntrypointCommand{
&instructions.EntrypointCommand{
cmd: &instructions.EntrypointCommand{
ShellDependantCmdLine: instructions.ShellDependantCmdLine{
PrependShell: test.prependShell,
CmdLine: test.cmdLine,

View File

@ -25,6 +25,7 @@ import (
)
type EnvCommand struct {
BaseCommand
cmd *instructions.EnvCommand
}
@ -34,17 +35,7 @@ func (e *EnvCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bui
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
func (e *EnvCommand) String() 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{
&instructions.EnvCommand{
cmd: &instructions.EnvCommand{
Env: []instructions.KeyValuePair{
{
Key: "path",

View File

@ -29,6 +29,7 @@ import (
)
type ExposeCommand struct {
BaseCommand
cmd *instructions.ExposeCommand
}
@ -72,15 +73,6 @@ func validProtocol(protocol string) bool {
return false
}
func (r *ExposeCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *ExposeCommand) String() 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{
&instructions.ExposeCommand{
cmd: &instructions.ExposeCommand{
Ports: ports,
},
}
@ -77,7 +77,7 @@ func TestInvalidProtocol(t *testing.T) {
}
exposeCmd := &ExposeCommand{
&instructions.ExposeCommand{
cmd: &instructions.ExposeCommand{
Ports: ports,
},
}

View File

@ -23,6 +23,7 @@ import (
)
type HealthCheckCommand struct {
BaseCommand
cmd *instructions.HealthCheckCommand
}
@ -34,17 +35,7 @@ func (h *HealthCheckCommand) ExecuteCommand(config *v1.Config, buildArgs *docker
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
func (h *HealthCheckCommand) String() 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 {
BaseCommand
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
func (r *LabelCommand) String() 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 {
BaseCommand
cmd *instructions.OnbuildCommand
}
@ -39,17 +40,7 @@ func (o *OnBuildCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile
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
func (o *OnBuildCommand) String() 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{
&instructions.OnbuildCommand{
cmd: &instructions.OnbuildCommand{
Expression: test.expression,
},
}

View File

@ -35,6 +35,7 @@ import (
)
type RunCommand struct {
BaseCommand
cmd *instructions.RunCommand
}
@ -142,17 +143,15 @@ func addDefaultHOME(u string, envs []string) []string {
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
func (r *RunCommand) String() string {
return r.cmd.String()
}
func (r *RunCommand) FilesToSnapshot() []string {
return nil
}
// CacheCommand returns true since this command should be cached
func (r *RunCommand) CacheCommand() bool {
return true

View File

@ -23,6 +23,7 @@ import (
)
type ShellCommand struct {
BaseCommand
cmd *instructions.ShellCommand
}
@ -32,17 +33,7 @@ func (s *ShellCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.B
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
func (s *ShellCommand) String() 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 {
cmd := ShellCommand{
&instructions.ShellCommand{
cmd: &instructions.ShellCommand{
Shell: test.cmdLine,
},
}

View File

@ -26,6 +26,7 @@ import (
)
type StopSignalCommand struct {
BaseCommand
cmd *instructions.StopSignalCommand
}
@ -52,17 +53,7 @@ func (s *StopSignalCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerf
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
func (s *StopSignalCommand) String() 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 {
cmd := StopSignalCommand{
&instructions.StopSignalCommand{
cmd: &instructions.StopSignalCommand{
Signal: test.signal,
},
}

View File

@ -27,6 +27,7 @@ import (
)
type UserCommand struct {
BaseCommand
cmd *instructions.UserCommand
}
@ -59,15 +60,6 @@ func (r *UserCommand) ExecuteCommand(config *v1.Config, buildArgs *dockerfile.Bu
return nil
}
func (r *UserCommand) FilesToSnapshot() []string {
return []string{}
}
func (r *UserCommand) String() 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{
&instructions.UserCommand{
cmd: &instructions.UserCommand{
User: test.user,
},
}

View File

@ -29,6 +29,7 @@ import (
)
type VolumeCommand struct {
BaseCommand
cmd *instructions.VolumeCommand
snapshotFiles []string
}
@ -74,8 +75,3 @@ func (v *VolumeCommand) FilesToSnapshot() []string {
func (v *VolumeCommand) String() 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 {
BaseCommand
cmd *instructions.WorkdirCommand
snapshotFiles []string
}
@ -66,8 +67,3 @@ func (w *WorkdirCommand) FilesToSnapshot() []string {
func (w *WorkdirCommand) String() 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 (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -85,23 +84,6 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta
}, 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
func (s *stageBuilder) extractCachedLayer(layer v1.Image, createdBy string) error {
logrus.Infof("Found cached layer, extracting to filesystem")
@ -139,6 +121,15 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error {
return err
}
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)
for index, cmd := range s.stage.Commands {
finalCmd := index == len(s.stage.Commands)-1
@ -149,13 +140,21 @@ func (s *stageBuilder) build(opts *config.KanikoOptions) error {
if command == nil {
continue
}
logrus.Info(command.String())
cacheKey, err := s.key(command.String())
if err != nil {
return errors.Wrap(err, "getting key")
// Add the next command to the cache key.
compositeKey.AddKey(command.String())
if command.UsesContext() {
compositeKey.AddKey(contextHash)
}
logrus.Info(command.String())
ck, err := compositeKey.Hash()
if err != nil {
return err
}
if command.CacheCommand() && opts.Cache {
image, err := cache.RetrieveLayer(opts, cacheKey)
image, err := cache.RetrieveLayer(opts, ck)
if err == nil {
if err := s.extractCachedLayer(image, command.String()); err != nil {
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
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
}
}

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
}