Use cachekey not digest for COPY --from src

* use the cachekey of the src stage rather than the digest
for COPY --from commands as they are reproducible unlike digests
* track digest to cache keys and stage indexes to digest
* add extra debug logging for troubleshooting cachekey building issues
* convert Sha256 hashes to hex encoded strings rather than plain strings
for easier human reading
This commit is contained in:
Cole Wippern 2019-11-27 16:35:52 -08:00
parent 7b4b768edf
commit b19214ad1e
3 changed files with 63 additions and 100 deletions

View File

@ -61,23 +61,24 @@ type snapShotter interface {
// stageBuilder contains all fields necessary to build one stage of a Dockerfile // stageBuilder contains all fields necessary to build one stage of a Dockerfile
type stageBuilder struct { type stageBuilder struct {
stage config.KanikoStage stage config.KanikoStage
image v1.Image image v1.Image
cf *v1.ConfigFile cf *v1.ConfigFile
baseImageDigest string baseImageDigest string
finalCacheKey string finalCacheKey string
opts *config.KanikoOptions opts *config.KanikoOptions
cmds []commands.DockerCommand cmds []commands.DockerCommand
args *dockerfile.BuildArgs args *dockerfile.BuildArgs
crossStageDeps map[int][]string crossStageDeps map[int][]string
digestToCacheKeyMap map[string]string digestToCacheKey map[string]string
snapshotter snapShotter stageIdxToDigest map[string]string
layerCache cache.LayerCache snapshotter snapShotter
pushCache cachePusher layerCache cache.LayerCache
pushCache cachePusher
} }
// newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage // newStageBuilder returns a new type stageBuilder which contains all the information required to build the stage
func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string) (*stageBuilder, error) { func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, crossStageDeps map[int][]string, dcm map[string]string, sid map[string]string) (*stageBuilder, error) {
sourceImage, err := util.RetrieveSourceImage(stage, opts) sourceImage, err := util.RetrieveSourceImage(stage, opts)
if err != nil { if err != nil {
return nil, err return nil, err
@ -104,14 +105,15 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage, cross
return nil, err return nil, err
} }
s := &stageBuilder{ s := &stageBuilder{
stage: stage, stage: stage,
image: sourceImage, image: sourceImage,
cf: imageConfig, cf: imageConfig,
snapshotter: snapshotter, snapshotter: snapshotter,
baseImageDigest: digest.String(), baseImageDigest: digest.String(),
opts: opts, opts: opts,
crossStageDeps: crossStageDeps, crossStageDeps: crossStageDeps,
digestToCacheKeyMap: dcm, digestToCacheKey: dcm,
stageIdxToDigest: sid,
layerCache: &cache.RegistryCache{ layerCache: &cache.RegistryCache{
Opts: opts, Opts: opts,
}, },
@ -146,28 +148,14 @@ func initializeConfig(img partial.WithConfigFile) (*v1.ConfigFile, error) {
return imageConfig, nil return imageConfig, nil
} }
func (s *stageBuilder) populateCompositeKey(command commands.DockerCommand, files []string, compositeKey CompositeCache) (CompositeCache, error) { func (s *stageBuilder) populateCompositeKey(command fmt.Stringer, files []string, compositeKey CompositeCache) (CompositeCache, error) {
// Add the next command to the cache key. // Add the next command to the cache key.
compositeKey.AddKey(command.String()) compositeKey.AddKey(command.String())
switch v := command.(type) { switch v := command.(type) {
case *commands.CopyCommand: case *commands.CopyCommand:
if v.From() != "" { compositeKey = s.populateCopyCmdCompositeKey(command, v.From(), compositeKey)
digest, ok := s.digestMap[v.From()]
if ok {
ds := digest.String()
logrus.Debugf("adding digest %v from previous stage to composite key for %v", ds, command.String())
compositeKey.AddKey(ds)
}
}
case *commands.CachingCopyCommand: case *commands.CachingCopyCommand:
if v.From() != "" { compositeKey = s.populateCopyCmdCompositeKey(command, v.From(), compositeKey)
digest, ok := s.digestMap[v.From()]
if ok {
ds := digest.String()
logrus.Debugf("adding digest %v from previous stage to composite key for %v", ds, command.String())
compositeKey.AddKey(ds)
}
}
} }
for _, f := range files { for _, f := range files {
@ -178,6 +166,22 @@ func (s *stageBuilder) populateCompositeKey(command commands.DockerCommand, file
return compositeKey, nil return compositeKey, nil
} }
func (s *stageBuilder) populateCopyCmdCompositeKey(command fmt.Stringer, from string, compositeKey CompositeCache) CompositeCache {
if from != "" {
digest, ok := s.stageIdxToDigest[from]
if ok {
ds := digest
cacheKey, ok := s.digestToCacheKey[ds]
if ok {
logrus.Debugf("adding digest %v from previous stage to composite key for %v", ds, command.String())
compositeKey.AddKey(cacheKey)
}
}
}
return compositeKey
}
func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) error { func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) error {
if !s.opts.Cache { if !s.opts.Cache {
return nil return nil
@ -202,10 +206,12 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
return err return err
} }
logrus.Debugf("optimize: composite key for command %v %v", command.String(), compositeKey)
ck, err := compositeKey.Hash() ck, err := compositeKey.Hash()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to hash composite key") return errors.Wrap(err, "failed to hash composite key")
} }
logrus.Debugf("optimize: cache key for command %v %v", command.String(), ck)
s.finalCacheKey = ck s.finalCacheKey = ck
if command.ShouldCacheOutput() && !stopCache { if command.ShouldCacheOutput() && !stopCache {
img, err := s.layerCache.RetrieveLayer(ck) img, err := s.layerCache.RetrieveLayer(ck)
@ -236,7 +242,7 @@ func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config) erro
func (s *stageBuilder) build() error { func (s *stageBuilder) build() error {
// Set the initial cache key to be the base image digest, the build args and the SrcContext. // Set the initial cache key to be the base image digest, the build args and the SrcContext.
var compositeKey *CompositeCache var compositeKey *CompositeCache
if cacheKey, ok := s.digestToCacheKeyMap[s.baseImageDigest]; ok { if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok {
compositeKey = NewCompositeCache(cacheKey) compositeKey = NewCompositeCache(cacheKey)
} else { } else {
compositeKey = NewCompositeCache(s.baseImageDigest) compositeKey = NewCompositeCache(s.baseImageDigest)
@ -316,6 +322,7 @@ func (s *stageBuilder) build() error {
return errors.Wrap(err, "failed to take snapshot") return errors.Wrap(err, "failed to take snapshot")
} }
logrus.Debugf("build: composite key for command %v %v", command.String(), compositeKey)
ck, err := compositeKey.Hash() ck, err := compositeKey.Hash()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to hash composite key") return errors.Wrap(err, "failed to hash composite key")
@ -472,7 +479,8 @@ func CalculateDependencies(opts *config.KanikoOptions) (map[int][]string, error)
// DoBuild executes building the Dockerfile // DoBuild executes building the Dockerfile
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
t := timing.Start("Total Build Time") t := timing.Start("Total Build Time")
digestToCacheKeyMap := make(map[string]string) digestToCacheKey := make(map[string]string)
stageIdxToDigest := make(map[string]string)
// Parse dockerfile and unpack base image to root // Parse dockerfile and unpack base image to root
stages, err := dockerfile.Stages(opts) stages, err := dockerfile.Stages(opts)
@ -494,7 +502,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
logrus.Infof("Built cross stage deps: %v", crossStageDependencies) logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
for index, stage := range stages { for index, stage := range stages {
sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKeyMap) sb, err := newStageBuilder(opts, stage, crossStageDependencies, digestToCacheKey, stageIdxToDigest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -502,23 +510,24 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
return nil, errors.Wrap(err, "error building stage") return nil, errors.Wrap(err, "error building stage")
} }
d, err := sb.image.Digest()
if err != nil {
return nil, err
}
digestMap[fmt.Sprintf("%d", sb.stage.Index)] = d
reviewConfig(stage, &sb.cf.Config) reviewConfig(stage, &sb.cf.Config)
sourceImage, err := mutate.Config(sb.image, sb.cf.Config) sourceImage, err := mutate.Config(sb.image, sb.cf.Config)
if err != nil { if err != nil {
return nil, err return nil, err
} }
d, err := sourceImage.Digest() d, err := sourceImage.Digest()
if err != nil { if err != nil {
return nil, err return nil, err
} }
digestToCacheKeyMap[d.String()] = sb.finalCacheKey
stageIdxToDigest[fmt.Sprintf("%d", sb.stage.Index)] = d.String()
logrus.Debugf("mapping stage idx %v to digest %v", sb.stage.Index, d.String())
digestToCacheKey[d.String()] = sb.finalCacheKey
logrus.Debugf("mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey)
if stage.Final { if stage.Final {
sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()}) sourceImage, err = mutate.CreatedAt(sourceImage, v1.Time{Time: time.Now()})
if err != nil { if err != nil {

View File

@ -888,6 +888,7 @@ func tempDirAndFile(t *testing.T) (string, []string) {
return dir, filenames return dir, filenames
} }
func generateTar(t *testing.T, dir string, fileNames ...string) []byte { func generateTar(t *testing.T, dir string, fileNames ...string) []byte {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
writer := tar.NewWriter(buf) writer := tar.NewWriter(buf)
@ -919,51 +920,3 @@ func generateTar(t *testing.T, dir string, fileNames ...string) []byte {
} }
return buf.Bytes() return buf.Bytes()
} }
//=======
// testCases := []struct {
// opts *config.KanikoOptions
// retrieve bool
// }{
// {
// opts: &config.KanikoOptions{Cache: true},
// },
// {
// opts: &config.KanikoOptions{Cache: true},
// retrieve: true,
// },
// {
// opts: &config.KanikoOptions{Cache: false},
// },
// {
// opts: &config.KanikoOptions{Cache: false},
// retrieve: true,
// },
// }
// for i, tc := range testCases {
// t.Run(fmt.Sprintf("Case %d", i), func(t *testing.T) {
// file, err := ioutil.TempFile("", "foo")
// if err != nil {
// t.Error(err)
// }
// cf := &v1.ConfigFile{}
// snap := fakeSnapShotter{file: file.Name()}
// lc := fakeLayerCache{retrieve: tc.retrieve}
// sb := &stageBuilder{opts: tc.opts, cf: cf, snapshotter: snap, layerCache: lc, pushCache: fakeCachePush}
// command := MockDockerCommand{
// contextFiles: []string{file.Name()},
// cacheCommand: MockCachedDockerCommand{
// contextFiles: []string{file.Name()},
// },
// }
// sb.cmds = []commands.DockerCommand{command}
// err = sb.build()
// if err != nil {
// t.Errorf("Expected error to be nil but was %v", err)
// }
// })
// }
//}
//>>>>>>> Add unit tests for stagebuilder build and optimize

View File

@ -18,6 +18,7 @@ package executor
import ( import (
"crypto/sha256" "crypto/sha256"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -75,7 +76,7 @@ func (s *CompositeCache) AddPath(p string) error {
return err return err
} }
s.keys = append(s.keys, string(sha.Sum(nil))) s.keys = append(s.keys, fmt.Sprintf("%x", sha.Sum(nil)))
return nil return nil
} }
@ -98,5 +99,5 @@ func HashDir(p string) (string, error) {
return "", err return "", err
} }
return string(sha.Sum(nil)), nil return fmt.Sprintf("%x", sha.Sum(nil)), nil
} }