Fix caching for multi-step builds. (#441)

This change fixes that by properly "replaying" the Dockerfile and mutating the config when
calculating cache keys. Previously we were looking at the wrong cache key for each command
when there was more than one.
This commit is contained in:
dlorenc 2018-11-09 12:28:18 -06:00 committed by GitHub
parent d97790119a
commit 58b607b4d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 87 additions and 26 deletions

View File

@ -17,4 +17,7 @@
# if the cache is implemented correctly # if the cache is implemented correctly
FROM gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0 FROM gcr.io/google-appengine/debian9@sha256:1d6a9a6d106bd795098f60f4abb7083626354fa6735e81743c7f8cfca11259f0
WORKDIR /foo
RUN apt-get update && apt-get install -y make RUN apt-get update && apt-get install -y make
COPY context/bar /context
RUN echo "hey" > foo

View File

@ -131,3 +131,7 @@ func (a *AddCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfi
logrus.Infof("Using files from context: %v", files) logrus.Infof("Using files from context: %v", files)
return files, nil return files, nil
} }
func (a *AddCommand) MetadataOnly() bool {
return false
}

View File

@ -35,3 +35,7 @@ func (b *BaseCommand) FilesToSnapshot() []string {
func (b *BaseCommand) FilesUsedFromContext(_ *v1.Config, _ *dockerfile.BuildArgs) ([]string, error) { func (b *BaseCommand) FilesUsedFromContext(_ *v1.Config, _ *dockerfile.BuildArgs) ([]string, error) {
return []string{}, nil return []string{}, nil
} }
func (b *BaseCommand) MetadataOnly() bool {
return true
}

View File

@ -41,6 +41,8 @@ type DockerCommand interface {
// Return true if this command depends on the build context. // Return true if this command depends on the build context.
FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error) FilesUsedFromContext(*v1.Config, *dockerfile.BuildArgs) ([]string, error)
MetadataOnly() bool
} }
func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) { func GetCommand(cmd instructions.Command, buildcontext string) (DockerCommand, error) {

View File

@ -134,3 +134,7 @@ func (c *CopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerf
logrus.Infof("Using files from context: %v", files) logrus.Infof("Using files from context: %v", files)
return files, nil return files, nil
} }
func (c *CopyCommand) MetadataOnly() bool {
return false
}

View File

@ -161,6 +161,10 @@ func (r *RunCommand) CacheCommand(img v1.Image) DockerCommand {
} }
} }
func (r *RunCommand) MetadataOnly() bool {
return false
}
type CachingRunCommand struct { type CachingRunCommand struct {
BaseCommand BaseCommand
img v1.Image img v1.Image

View File

@ -67,3 +67,7 @@ func (w *WorkdirCommand) FilesToSnapshot() []string {
func (w *WorkdirCommand) String() string { func (w *WorkdirCommand) String() string {
return w.cmd.String() return w.cmd.String()
} }
func (w *WorkdirCommand) MetadataOnly() bool {
return false
}

View File

@ -86,6 +86,63 @@ func newStageBuilder(opts *config.KanikoOptions, stage config.KanikoStage) (*sta
}, nil }, nil
} }
func (s *stageBuilder) optimize(compositeKey CompositeCache, cfg v1.Config, cmds []commands.DockerCommand) error {
if !s.opts.Cache {
return nil
}
args := dockerfile.NewBuildArgs(s.opts.BuildArgs)
args.AddMetaArgs(s.stage.MetaArgs)
layerCache := &cache.RegistryCache{
Opts: s.opts,
}
// Possibly replace commands with their cached implementations.
// We walk through all the commands, running any commands that only operate on metadata.
// We throw the metadata away after, but we need it to properly track command dependencies
// for things like COPY ${FOO} or RUN commands that use environment variables.
for i, command := range cmds {
if command == nil {
continue
}
compositeKey.AddKey(command.String())
// If the command uses files from the context, add them.
files, err := command.FilesUsedFromContext(&cfg, args)
if err != nil {
return err
}
for _, f := range files {
if err := compositeKey.AddPath(f); err != nil {
return err
}
}
ck, err := compositeKey.Hash()
if err != nil {
return err
}
img, err := layerCache.RetrieveLayer(ck)
if err != nil {
logrus.Infof("No cached layer found for cmd %s", command.String())
break
}
if cacheCmd := command.CacheCommand(img); cacheCmd != nil {
logrus.Infof("Using caching version of cmd: %s", command.String())
cmds[i] = cacheCmd
}
// Mutate the config for any commands that require it.
if command.MetadataOnly() {
if err := command.ExecuteCommand(&cfg, args); err != nil {
return err
}
}
}
return nil
}
func (s *stageBuilder) build() error { func (s *stageBuilder) build() error {
// Unpack file system to root // Unpack file system to root
if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil { if _, err := util.GetFSFromImage(constants.RootDir, s.image); err != nil {
@ -109,34 +166,13 @@ func (s *stageBuilder) build() error {
cmds = append(cmds, command) cmds = append(cmds, command)
} }
layerCache := &cache.RegistryCache{
Opts: s.opts,
}
if s.opts.Cache {
// Possibly replace commands with their cached implementations.
for i, command := range cmds {
if command == nil {
continue
}
ck, err := compositeKey.Hash()
if err != nil {
return err
}
img, err := layerCache.RetrieveLayer(ck)
if err != nil {
logrus.Infof("No cached layer found for cmd %s", command.String())
break
}
if cacheCmd := command.CacheCommand(img); cacheCmd != nil {
logrus.Infof("Using caching version of cmd: %s", command.String())
cmds[i] = cacheCmd
}
}
}
args := dockerfile.NewBuildArgs(s.opts.BuildArgs) args := dockerfile.NewBuildArgs(s.opts.BuildArgs)
args.AddMetaArgs(s.stage.MetaArgs) args.AddMetaArgs(s.stage.MetaArgs)
// Apply optimizations to the instructions.
if err := s.optimize(*compositeKey, s.cf.Config, cmds); err != nil {
return err
}
for index, command := range cmds { for index, command := range cmds {
if command == nil { if command == nil {
continue continue