Skip to content

Commit 549420b

Browse files
committed
Add support for fake, and speed up reproducible builds
1 parent 9d0d559 commit 549420b

File tree

5 files changed

+322
-3
lines changed

5 files changed

+322
-3
lines changed

pkg/commands/cache.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ limitations under the License.
1616

1717
package commands
1818

19-
import v1 "github.com/google/go-containerregistry/pkg/v1"
19+
import (
20+
"github.com/GoogleContainerTools/kaniko/pkg/dockerfile"
21+
v1 "github.com/google/go-containerregistry/pkg/v1"
22+
)
2023

2124
type Cached interface {
2225
Layer() v1.Layer
@@ -29,3 +32,7 @@ type caching struct {
2932
func (c caching) Layer() v1.Layer {
3033
return c.layer
3134
}
35+
36+
type FakeExecuteCommand interface {
37+
FakeExecuteCommand(*v1.Config, *dockerfile.BuildArgs) error
38+
}

pkg/commands/copy.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,29 @@ func (cr *CachingCopyCommand) ExecuteCommand(config *v1.Config, buildArgs *docke
206206
return nil
207207
}
208208

209+
func (cr *CachingCopyCommand) FakeExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
210+
logrus.Infof("Found cached layer, faking extraction to filesystem")
211+
var err error
212+
213+
if cr.img == nil {
214+
return errors.New(fmt.Sprintf("cached command image is nil %v", cr.String()))
215+
}
216+
217+
layers, err := cr.img.Layers()
218+
if err != nil {
219+
return errors.Wrapf(err, "retrieve image layers")
220+
}
221+
222+
if len(layers) != 1 {
223+
return errors.New(fmt.Sprintf("expected %d layers but got %d", 1, len(layers)))
224+
}
225+
226+
cr.layer = layers[0]
227+
cr.extractedFiles = []string{}
228+
229+
return nil
230+
}
231+
209232
func (cr *CachingCopyCommand) FilesUsedFromContext(config *v1.Config, buildArgs *dockerfile.BuildArgs) ([]string, error) {
210233
return copyCmdFilesUsedFromContext(config, buildArgs, cr.cmd, cr.fileContext)
211234
}

pkg/commands/run.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,29 @@ func (cr *CachingRunCommand) ExecuteCommand(config *v1.Config, buildArgs *docker
256256
return nil
257257
}
258258

259+
func (cr *CachingRunCommand) FakeExecuteCommand(config *v1.Config, buildArgs *dockerfile.BuildArgs) error {
260+
logrus.Infof("Found cached layer, faking extraction to filesystem")
261+
var err error
262+
263+
if cr.img == nil {
264+
return errors.New(fmt.Sprintf("command image is nil %v", cr.String()))
265+
}
266+
267+
layers, err := cr.img.Layers()
268+
if err != nil {
269+
return errors.Wrap(err, "retrieving image layers")
270+
}
271+
272+
if len(layers) != 1 {
273+
return errors.New(fmt.Sprintf("expected %d layers but got %d", 1, len(layers)))
274+
}
275+
276+
cr.layer = layers[0]
277+
cr.extractedFiles = []string{}
278+
279+
return nil
280+
}
281+
259282
func (cr *CachingRunCommand) FilesToSnapshot() []string {
260283
f := cr.extractedFiles
261284
logrus.Debugf("%d files extracted by caching run command", len(f))

pkg/executor/build.go

Lines changed: 243 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,92 @@ func (s *stageBuilder) build() error {
444444
return nil
445445
}
446446

447+
// fakeBuild is like build(), but does not actually execute the commands or
448+
// extract files.
449+
func (s *stageBuilder) fakeBuild() error {
450+
// Set the initial cache key to be the base image digest, the build args and the SrcContext.
451+
var compositeKey *CompositeCache
452+
if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok {
453+
compositeKey = NewCompositeCache(cacheKey)
454+
} else {
455+
compositeKey = NewCompositeCache(s.baseImageDigest)
456+
}
457+
458+
// Apply optimizations to the instructions.
459+
if err := s.optimize(*compositeKey, s.cf.Config); err != nil {
460+
return errors.Wrap(err, "failed to optimize instructions")
461+
}
462+
463+
for index, command := range s.cmds {
464+
if command == nil {
465+
continue
466+
}
467+
468+
// If the command uses files from the context, add them.
469+
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args)
470+
if err != nil {
471+
return errors.Wrap(err, "failed to get files used from context")
472+
}
473+
474+
if s.opts.Cache {
475+
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env)
476+
if err != nil && s.opts.Cache {
477+
return err
478+
}
479+
}
480+
481+
logrus.Info(command.String())
482+
483+
isCacheCommand := func() bool {
484+
switch command.(type) {
485+
case commands.Cached:
486+
return true
487+
default:
488+
return false
489+
}
490+
}()
491+
492+
if c, ok := command.(commands.FakeExecuteCommand); ok {
493+
if err := c.FakeExecuteCommand(&s.cf.Config, s.args); err != nil {
494+
return errors.Wrap(err, "failed to execute fake command")
495+
}
496+
} else {
497+
switch command.(type) {
498+
case *commands.UserCommand:
499+
default:
500+
return errors.Errorf("uncached command %T is not supported in fake build", command)
501+
}
502+
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil {
503+
return errors.Wrap(err, "failed to execute command")
504+
}
505+
}
506+
files = command.FilesToSnapshot()
507+
508+
if !s.shouldTakeSnapshot(index, command.MetadataOnly()) && !s.opts.ForceBuildMetadata {
509+
logrus.Debugf("fakeBuild: skipping snapshot for [%v]", command.String())
510+
continue
511+
}
512+
if isCacheCommand {
513+
v := command.(commands.Cached)
514+
layer := v.Layer()
515+
if err := s.saveLayerToImage(layer, command.String()); err != nil {
516+
return errors.Wrap(err, "failed to save layer")
517+
}
518+
} else {
519+
tarPath, err := s.takeSnapshot(files, command.ShouldDetectDeletedFiles())
520+
if err != nil {
521+
return errors.Wrap(err, "failed to take snapshot")
522+
}
523+
524+
if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil {
525+
return errors.Wrap(err, "failed to save snapshot to image")
526+
}
527+
}
528+
}
529+
530+
return nil
531+
}
532+
447533
func (s *stageBuilder) takeSnapshot(files []string, shdDelete bool) (string, error) {
448534
var snapshot string
449535
var err error
@@ -696,6 +782,14 @@ func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptio
696782

697783
// DoBuild executes building the Dockerfile
698784
func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
785+
// Create Tar archives as canonical to avoid unpack and recompress.
786+
if opts.Reproducible {
787+
util.NewTar = util.CanonicalNewTar(time.Time{})
788+
defer func() {
789+
util.NewTar = util.DefaultNewTar
790+
}()
791+
}
792+
699793
t := timing.Start("Total Build Time")
700794
digestToCacheKey := make(map[string]string)
701795
stageIdxToDigest := make(map[string]string)
@@ -787,7 +881,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
787881
return nil, err
788882
}
789883
if opts.Reproducible {
790-
sourceImage, err = mutate.Canonical(sourceImage)
884+
sourceImage, err = mutateCanonical(sourceImage)
791885
if err != nil {
792886
return nil, err
793887
}
@@ -797,6 +891,10 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
797891
return nil, err
798892
}
799893
}
894+
895+
digest, _ := sourceImage.Digest()
896+
logrus.Infof("Stage %d built successfully with digest %s: %T", index, digest.String(), sourceImage)
897+
800898
timing.DefaultRun.Stop(t)
801899
return sourceImage, nil
802900
}
@@ -833,6 +931,150 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
833931
return nil, err
834932
}
835933

934+
// DoFakeBuild executes building the Dockerfile
935+
func DoFakeBuild(opts *config.KanikoOptions) (v1.Image, error) {
936+
digestToCacheKey := make(map[string]string)
937+
stageIdxToDigest := make(map[string]string)
938+
939+
stages, metaArgs, err := dockerfile.ParseStages(opts)
940+
if err != nil {
941+
return nil, err
942+
}
943+
944+
kanikoStages, err := dockerfile.MakeKanikoStages(opts, stages, metaArgs)
945+
if err != nil {
946+
return nil, err
947+
}
948+
for _, stage := range kanikoStages {
949+
logrus.Infof("Stage: %#v", stage)
950+
}
951+
stageNameToIdx := ResolveCrossStageInstructions(kanikoStages)
952+
logrus.Infof("stageNameToIdx: %v", stageNameToIdx)
953+
954+
fileContext, err := util.NewFileContextFromDockerfile(opts.DockerfilePath, opts.SrcContext)
955+
if err != nil {
956+
return nil, err
957+
}
958+
logrus.Infof("File context: %#v", fileContext)
959+
960+
// Some stages may refer to other random images, not previous stages
961+
if err := fetchExtraStages(kanikoStages, opts); err != nil {
962+
return nil, err
963+
}
964+
for _, stage := range kanikoStages {
965+
logrus.Infof("Stage (post extra): %#v", stage)
966+
}
967+
crossStageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
968+
if err != nil {
969+
return nil, err
970+
}
971+
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
972+
973+
var args *dockerfile.BuildArgs
974+
975+
for index, stage := range kanikoStages {
976+
sb, err := newStageBuilder(
977+
args, opts, stage,
978+
crossStageDependencies,
979+
digestToCacheKey,
980+
stageIdxToDigest,
981+
stageNameToIdx,
982+
fileContext)
983+
if err != nil {
984+
return nil, err
985+
}
986+
987+
args = sb.args
988+
if err := sb.fakeBuild(); err != nil {
989+
return nil, errors.Wrap(err, "error fake building stage")
990+
}
991+
992+
reviewConfig(stage, &sb.cf.Config)
993+
994+
sourceImage, err := mutate.Config(sb.image, sb.cf.Config)
995+
if err != nil {
996+
return nil, err
997+
}
998+
999+
configFile, err := sourceImage.ConfigFile()
1000+
if err != nil {
1001+
return nil, err
1002+
}
1003+
if opts.CustomPlatform == "" {
1004+
configFile.OS = runtime.GOOS
1005+
configFile.Architecture = runtime.GOARCH
1006+
} else {
1007+
configFile.OS = strings.Split(opts.CustomPlatform, "/")[0]
1008+
configFile.Architecture = strings.Split(opts.CustomPlatform, "/")[1]
1009+
}
1010+
sourceImage, err = mutate.ConfigFile(sourceImage, configFile)
1011+
if err != nil {
1012+
return nil, err
1013+
}
1014+
1015+
d, err := sourceImage.Digest()
1016+
if err != nil {
1017+
return nil, err
1018+
}
1019+
stageIdxToDigest[fmt.Sprintf("%d", sb.stage.Index)] = d.String()
1020+
logrus.Infof("Mapping stage idx %v to digest %v", sb.stage.Index, d.String())
1021+
1022+
digestToCacheKey[d.String()] = sb.finalCacheKey
1023+
logrus.Infof("Mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey)
1024+
1025+
if stage.Final {
1026+
sourceImage, err = mutateCanonical(sourceImage)
1027+
if err != nil {
1028+
return nil, err
1029+
}
1030+
1031+
digest, _ := sourceImage.Digest()
1032+
logrus.Infof("fakeStage %d built successfully with digest %s", index, digest.String())
1033+
1034+
return sourceImage, nil
1035+
}
1036+
}
1037+
1038+
return nil, err
1039+
}
1040+
1041+
// From mutate.Canonical with layer de/compress stripped out.
1042+
func mutateCanonical(image v1.Image) (v1.Image, error) {
1043+
t := time.Time{}
1044+
1045+
ocf, err := image.ConfigFile()
1046+
if err != nil {
1047+
return nil, fmt.Errorf("setting config file: %w", err)
1048+
}
1049+
1050+
cfg := ocf.DeepCopy()
1051+
1052+
// Copy basic config over
1053+
cfg.Architecture = ocf.Architecture
1054+
cfg.OS = ocf.OS
1055+
cfg.OSVersion = ocf.OSVersion
1056+
cfg.Config = ocf.Config
1057+
1058+
// Strip away timestamps from the config file
1059+
cfg.Created = v1.Time{Time: t}
1060+
1061+
for i, h := range cfg.History {
1062+
h.Created = v1.Time{Time: t}
1063+
h.CreatedBy = ocf.History[i].CreatedBy
1064+
h.Comment = ocf.History[i].Comment
1065+
h.EmptyLayer = ocf.History[i].EmptyLayer
1066+
// Explicitly ignore Author field; which hinders reproducibility
1067+
h.Author = ""
1068+
cfg.History[i] = h
1069+
}
1070+
1071+
cfg.Container = ""
1072+
cfg.Config.Hostname = ""
1073+
cfg.DockerVersion = ""
1074+
1075+
return mutate.ConfigFile(image, cfg)
1076+
}
1077+
8361078
// filesToSave returns all the files matching the given pattern in deps.
8371079
// If a file is a symlink, it also returns the target file.
8381080
func filesToSave(deps []string) ([]string, error) {

0 commit comments

Comments
 (0)