-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for fake, and speed up reproducible builds #12
Changes from 1 commit
696ae8d
0dddc0a
c03819e
2bff75a
3a6d655
61cdf80
9d76d2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -111,7 +111,12 @@ func newStageBuilder(args *dockerfile.BuildArgs, opts *config.KanikoOptions, sta | |
return nil, err | ||
} | ||
l := snapshot.NewLayeredMap(hasher) | ||
snapshotter := snapshot.NewSnapshotter(l, config.RootDir) | ||
var snapshotter snapShotter | ||
if !opts.Reproducible { | ||
snapshotter = snapshot.NewSnapshotter(l, config.RootDir) | ||
} else { | ||
snapshotter = snapshot.NewCanonicalSnapshotter(l, config.RootDir) | ||
} | ||
|
||
digest, err := sourceImage.Digest() | ||
if err != nil { | ||
|
@@ -444,6 +449,92 @@ func (s *stageBuilder) build() error { | |
return nil | ||
} | ||
|
||
// fakeBuild is like build(), but does not actually execute the commands or | ||
// extract files. | ||
func (s *stageBuilder) fakeBuild() error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What would you think about making a separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you also thinking we'd merge the logic in I'll think about it, definitely doable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. non-blocking, just a suggestion There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think what could solve both my concern (about lock-step with I haven't looked too closely at the implementation so maybe this is im{practical,possible}, but thought I'd mention it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ditto 👍 |
||
// Set the initial cache key to be the base image digest, the build args and the SrcContext. | ||
var compositeKey *CompositeCache | ||
if cacheKey, ok := s.digestToCacheKey[s.baseImageDigest]; ok { | ||
compositeKey = NewCompositeCache(cacheKey) | ||
} else { | ||
compositeKey = NewCompositeCache(s.baseImageDigest) | ||
} | ||
|
||
// Apply optimizations to the instructions. | ||
if err := s.optimize(*compositeKey, s.cf.Config); err != nil { | ||
return errors.Wrap(err, "failed to optimize instructions") | ||
} | ||
|
||
for index, command := range s.cmds { | ||
if command == nil { | ||
continue | ||
} | ||
|
||
// If the command uses files from the context, add them. | ||
files, err := command.FilesUsedFromContext(&s.cf.Config, s.args) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to get files used from context") | ||
} | ||
|
||
if s.opts.Cache { | ||
*compositeKey, err = s.populateCompositeKey(command, files, *compositeKey, s.args, s.cf.Config.Env) | ||
if err != nil && s.opts.Cache { | ||
return err | ||
} | ||
} | ||
|
||
logrus.Info(command.String()) | ||
|
||
isCacheCommand := func() bool { | ||
switch command.(type) { | ||
case commands.Cached: | ||
return true | ||
default: | ||
return false | ||
} | ||
}() | ||
|
||
if c, ok := command.(commands.FakeExecuteCommand); ok { | ||
if err := c.FakeExecuteCommand(&s.cf.Config, s.args); err != nil { | ||
return errors.Wrap(err, "failed to execute fake command") | ||
} | ||
} else { | ||
switch command.(type) { | ||
case *commands.UserCommand: | ||
default: | ||
return errors.Errorf("uncached command %T is not supported in fake build", command) | ||
} | ||
if err := command.ExecuteCommand(&s.cf.Config, s.args); err != nil { | ||
return errors.Wrap(err, "failed to execute command") | ||
} | ||
} | ||
files = command.FilesToSnapshot() | ||
|
||
if !s.shouldTakeSnapshot(index, command.MetadataOnly()) && !s.opts.ForceBuildMetadata { | ||
logrus.Debugf("fakeBuild: skipping snapshot for [%v]", command.String()) | ||
continue | ||
} | ||
if isCacheCommand { | ||
v := command.(commands.Cached) | ||
layer := v.Layer() | ||
if err := s.saveLayerToImage(layer, command.String()); err != nil { | ||
return errors.Wrap(err, "failed to save layer") | ||
} | ||
} else { | ||
tarPath, err := s.takeSnapshot(files, command.ShouldDetectDeletedFiles()) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to take snapshot") | ||
} | ||
|
||
if err := s.saveSnapshotToImage(command.String(), tarPath); err != nil { | ||
return errors.Wrap(err, "failed to save snapshot to image") | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (s *stageBuilder) takeSnapshot(files []string, shdDelete bool) (string, error) { | ||
var snapshot string | ||
var err error | ||
|
@@ -787,7 +878,9 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { | |
return nil, err | ||
} | ||
if opts.Reproducible { | ||
sourceImage, err = mutate.Canonical(sourceImage) | ||
// If this option is enabled, we will use the canonical | ||
// snapshotter to avoid having to modify the layers here. | ||
sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
@@ -797,6 +890,7 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { | |
return nil, err | ||
} | ||
} | ||
|
||
timing.DefaultRun.Stop(t) | ||
return sourceImage, nil | ||
} | ||
|
@@ -833,6 +927,140 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) { | |
return nil, err | ||
} | ||
|
||
// DoFakeBuild executes building the Dockerfile without modifying the | ||
// filesystem, returns an error if build cache is not available. | ||
func DoFakeBuild(opts *config.KanikoOptions) (v1.Image, error) { | ||
digestToCacheKey := make(map[string]string) | ||
stageIdxToDigest := make(map[string]string) | ||
|
||
stages, metaArgs, err := dockerfile.ParseStages(opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
kanikoStages, err := dockerfile.MakeKanikoStages(opts, stages, metaArgs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
stageNameToIdx := ResolveCrossStageInstructions(kanikoStages) | ||
|
||
fileContext, err := util.NewFileContextFromDockerfile(opts.DockerfilePath, opts.SrcContext) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Some stages may refer to other random images, not previous stages | ||
if err := fetchExtraStages(kanikoStages, opts); err != nil { | ||
return nil, err | ||
} | ||
crossStageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
logrus.Infof("Built cross stage deps: %v", crossStageDependencies) | ||
|
||
var args *dockerfile.BuildArgs | ||
|
||
for _, stage := range kanikoStages { | ||
sb, err := newStageBuilder( | ||
args, opts, stage, | ||
crossStageDependencies, | ||
digestToCacheKey, | ||
stageIdxToDigest, | ||
stageNameToIdx, | ||
fileContext) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
args = sb.args | ||
if err := sb.fakeBuild(); err != nil { | ||
return nil, errors.Wrap(err, "error fake building stage") | ||
} | ||
|
||
reviewConfig(stage, &sb.cf.Config) | ||
|
||
sourceImage, err := mutate.Config(sb.image, sb.cf.Config) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
configFile, err := sourceImage.ConfigFile() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if opts.CustomPlatform == "" { | ||
configFile.OS = runtime.GOOS | ||
configFile.Architecture = runtime.GOARCH | ||
} else { | ||
configFile.OS = strings.Split(opts.CustomPlatform, "/")[0] | ||
configFile.Architecture = strings.Split(opts.CustomPlatform, "/")[1] | ||
} | ||
sourceImage, err = mutate.ConfigFile(sourceImage, configFile) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
d, err := sourceImage.Digest() | ||
if err != nil { | ||
return nil, err | ||
} | ||
stageIdxToDigest[fmt.Sprintf("%d", sb.stage.Index)] = d.String() | ||
logrus.Infof("Mapping stage idx %v to digest %v", sb.stage.Index, d.String()) | ||
|
||
digestToCacheKey[d.String()] = sb.finalCacheKey | ||
logrus.Infof("Mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey) | ||
|
||
if stage.Final { | ||
sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return sourceImage, nil | ||
} | ||
} | ||
|
||
return nil, err | ||
} | ||
|
||
// From mutate.Canonical with layer de/compress stripped out. | ||
func mutateCanonicalWithoutLayerEdit(image v1.Image) (v1.Image, error) { | ||
t := time.Time{} | ||
|
||
ocf, err := image.ConfigFile() | ||
if err != nil { | ||
return nil, fmt.Errorf("setting config file: %w", err) | ||
} | ||
|
||
cfg := ocf.DeepCopy() | ||
|
||
// Copy basic config over | ||
cfg.Architecture = ocf.Architecture | ||
cfg.OS = ocf.OS | ||
cfg.OSVersion = ocf.OSVersion | ||
cfg.Config = ocf.Config | ||
|
||
// Strip away timestamps from the config file | ||
cfg.Created = v1.Time{Time: t} | ||
|
||
for i, h := range cfg.History { | ||
h.Created = v1.Time{Time: t} | ||
h.CreatedBy = ocf.History[i].CreatedBy | ||
h.Comment = ocf.History[i].Comment | ||
h.EmptyLayer = ocf.History[i].EmptyLayer | ||
// Explicitly ignore Author field; which hinders reproducibility | ||
h.Author = "" | ||
cfg.History[i] = h | ||
} | ||
|
||
cfg.Container = "" | ||
cfg.Config.Hostname = "" | ||
cfg.DockerVersion = "" | ||
|
||
return mutate.ConfigFile(image, cfg) | ||
} | ||
|
||
// filesToSave returns all the files matching the given pattern in deps. | ||
// If a file is a symlink, it also returns the target file. | ||
func filesToSave(deps []string) ([]string, error) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS this aligns with the implementation of
build()
. I'm concerned that this will drift; how do you plan to keep these two in lock-step?As an aside: I think the term "fake" is a problematic one.
Perhaps renaming it to something (admittedly less pithy) like "cacheProbeBuild" or "preemptiveBuild" might be more clear?
The term "fake" is quite overloaded and I don't think it expresses the intent well here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a fair concern, one I have as well. I don't really have a plan for ensuring they're kept in sync other than adding tests to verify a build and "fakeBuild" produce the same (or not) hash in the end.
I also agree with you on "fake", and "cacheProbe" is actually a pretty good one, thanks for the suggestions.