@@ -111,7 +111,12 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta
111
111
return nil , err
112
112
}
113
113
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
+ }
115
120
116
121
digest , err := sourceImage .Digest ()
117
122
if err != nil {
@@ -444,6 +449,92 @@ func (s *stageBuilder) build() error {
444
449
return nil
445
450
}
446
451
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
+
447
538
func (s * stageBuilder ) takeSnapshot (files []string , shdDelete bool ) (string , error ) {
448
539
var snapshot string
449
540
var err error
@@ -787,7 +878,9 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
787
878
return nil , err
788
879
}
789
880
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 )
791
884
if err != nil {
792
885
return nil , err
793
886
}
@@ -797,6 +890,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
797
890
return nil , err
798
891
}
799
892
}
893
+
800
894
timing .DefaultRun .Stop (t )
801
895
return sourceImage , nil
802
896
}
@@ -833,6 +927,140 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
833
927
return nil , err
834
928
}
835
929
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
+
836
1064
// filesToSave returns all the files matching the given pattern in deps.
837
1065
// If a file is a symlink, it also returns the target file.
838
1066
func filesToSave (deps []string ) ([]string , error ) {
0 commit comments