Skip to content

Commit 696ae8d

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

File tree

6 files changed

+326
-5
lines changed

6 files changed

+326
-5
lines changed

pkg/commands/cache.go

+8-1
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

+23
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

+23
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

+230-2
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,12 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta
111111
return nil, err
112112
}
113113
l := snapshot.NewLayeredMap(hasher)
114-
snapshotter := snapshot.NewSnapshotter(l, config.RootDir)
114+
var snapshotter snapShotter
115+
if !opts.Reproducible {
116+
snapshotter = snapshot.NewSnapshotter(l, config.RootDir)
117+
} else {
118+
snapshotter = snapshot.NewCanonicalSnapshotter(l, config.RootDir)
119+
}
115120

116121
digest, err := sourceImage.Digest()
117122
if err != nil {
@@ -444,6 +449,92 @@ func (s *stageBuilder) build() error {
444449
return nil
445450
}
446451

452+
// fakeBuild is like build(), but does not actually execute the commands or
453+
// extract files.
454+
func (s *stageBuilder) fakeBuild() error {
455+
// Set the initial cache key to be the base image digest, the build args and the SrcContext.
456+
var compositeKey *CompositeCache
457+
if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok {
458+
compositeKey = NewCompositeCache(cacheKey)
459+
} else {
460+
compositeKey = NewCompositeCache(s.baseImageDigest)
461+
}
462+
463+
// Apply optimizations to the instructions.
464+
if err := s.optimize(*compositeKey, s.cf.Config); err != nil {
465+
return errors.Wrap(err, "failed to optimize instructions")
466+
}
467+
468+
for index, command := range s.cmds {
469+
if command == nil {
470+
continue
471+
}
472+
473+
// If the command uses files from the context, add them.
474+
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args)
475+
if err != nil {
476+
return errors.Wrap(err, "failed to get files used from context")
477+
}
478+
479+
if s.opts.Cache {
480+
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env)
481+
if err != nil && s.opts.Cache {
482+
return err
483+
}
484+
}
485+
486+
logrus.Info(command.String())
487+
488+
isCacheCommand := func() bool {
489+
switch command.(type) {
490+
case commands.Cached:
491+
return true
492+
default:
493+
return false
494+
}
495+
}()
496+
497+
if c, ok := command.(commands.FakeExecuteCommand); ok {
498+
if err := c.FakeExecuteCommand(&s.cf.Config, s.args); err != nil {
499+
return errors.Wrap(err, "failed to execute fake command")
500+
}
501+
} else {
502+
switch command.(type) {
503+
case *commands.UserCommand:
504+
default:
505+
return errors.Errorf("uncached command %T is not supported in fake build", command)
506+
}
507+
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil {
508+
return errors.Wrap(err, "failed to execute command")
509+
}
510+
}
511+
files = command.FilesToSnapshot()
512+
513+
if !s.shouldTakeSnapshot(index, command.MetadataOnly()) && !s.opts.ForceBuildMetadata {
514+
logrus.Debugf("fakeBuild: skipping snapshot for [%v]", command.String())
515+
continue
516+
}
517+
if isCacheCommand {
518+
v := command.(commands.Cached)
519+
layer := v.Layer()
520+
if err := s.saveLayerToImage(layer, command.String()); err != nil {
521+
return errors.Wrap(err, "failed to save layer")
522+
}
523+
} else {
524+
tarPath, err := s.takeSnapshot(files, command.ShouldDetectDeletedFiles())
525+
if err != nil {
526+
return errors.Wrap(err, "failed to take snapshot")
527+
}
528+
529+
if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil {
530+
return errors.Wrap(err, "failed to save snapshot to image")
531+
}
532+
}
533+
}
534+
535+
return nil
536+
}
537+
447538
func (s *stageBuilder) takeSnapshot(files []string, shdDelete bool) (string, error) {
448539
var snapshot string
449540
var err error
@@ -787,7 +878,9 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
787878
return nil, err
788879
}
789880
if opts.Reproducible {
790-
sourceImage, err = mutate.Canonical(sourceImage)
881+
// If this option is enabled, we will use the canonical
882+
// snapshotter to avoid having to modify the layers here.
883+
sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage)
791884
if err != nil {
792885
return nil, err
793886
}
@@ -797,6 +890,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
797890
return nil, err
798891
}
799892
}
893+
800894
timing.DefaultRun.Stop(t)
801895
return sourceImage, nil
802896
}
@@ -833,6 +927,140 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
833927
return nil, err
834928
}
835929

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

pkg/snapshot/snapshot.go

+19-2
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,20 @@ type Snapshotter struct {
4242
l *LayeredMap
4343
directory string
4444
ignorelist []util.IgnoreListEntry
45+
canonical bool
4546
}
4647

4748
// NewSnapshotter creates a new snapshotter rooted at d
4849
func NewSnapshotter(l *LayeredMap, d string) *Snapshotter {
4950
return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList()}
5051
}
5152

53+
// NewCanonicalSnapshotter creates a new snapshotter rooted at d that produces
54+
// reproducible snapshots.
55+
func NewCanonicalSnapshotter(l *LayeredMap, d string) *Snapshotter {
56+
return &Snapshotter{l: l, directory: d, ignorelist: util.IgnoreList(), canonical: true}
57+
}
58+
5259
// Init initializes a new snapshotter
5360
func (s *Snapshotter) Init() error {
5461
logrus.Info("Initializing snapshotter ...")
@@ -112,7 +119,12 @@ func (s *Snapshotter) TakeSnapshot(files []string, shdCheckDelete bool, forceBui
112119
sort.Strings(filesToWhiteout)
113120
}
114121

115-
t := util.NewTar(f)
122+
var t util.Tar
123+
if !s.canonical {
124+
t = util.NewTar(f)
125+
} else {
126+
t = util.NewCanonicalTar(f)
127+
}
116128
defer t.Close()
117129
if err := writeToTar(t, filesToAdd, filesToWhiteout); err != nil {
118130
return "", err
@@ -128,7 +140,12 @@ func (s *Snapshotter) TakeSnapshotFS() (string, error) {
128140
return "", err
129141
}
130142
defer f.Close()
131-
t := util.NewTar(f)
143+
var t util.Tar
144+
if !s.canonical {
145+
t = util.NewTar(f)
146+
} else {
147+
t = util.NewCanonicalTar(f)
148+
}
132149
defer t.Close()
133150

134151
filesToAdd, filesToWhiteOut, err := s.scanFullFilesystem()

0 commit comments

Comments
 (0)