@@ -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+
447538func (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.
8381066func filesToSave (deps []string ) ([]string , error ) {
0 commit comments