Skip to content
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 imagefs and fix multi-stage cache probing #25

Merged
merged 13 commits into from
Aug 29, 2024
88 changes: 61 additions & 27 deletions pkg/executor/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/GoogleContainerTools/kaniko/pkg/filesystem"
image_util "github.com/GoogleContainerTools/kaniko/pkg/image"
"github.com/GoogleContainerTools/kaniko/pkg/image/remote"
"github.com/GoogleContainerTools/kaniko/pkg/imagefs"
"github.com/GoogleContainerTools/kaniko/pkg/snapshot"
"github.com/GoogleContainerTools/kaniko/pkg/timing"
"github.com/GoogleContainerTools/kaniko/pkg/util"
Expand Down Expand Up @@ -731,9 +732,12 @@ func (s *stageBuilder) saveLayerToImage(layer v1.Layer, createdBy string) error
return err
}

func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptions, stageNameToIdx map[string]string) (map[int][]string, error) {
func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptions, stageNameToIdx map[string]string) (map[int][]string, map[string][]string, error) {
images := []v1.Image{}
depGraph := map[int][]string{}
stageDepGraph := map[int][]string{}
// imageDepGraph tracks stage dependencies from non-stage
// images for use by imagefs to avoid extraction.
imageDepGraph := map[string][]string{}
for _, s := range stages {
ba := dockerfile.NewBuildArgs(opts.BuildArgs)
ba.AddMetaArgs(s.MetaArgs)
Expand All @@ -746,12 +750,12 @@ func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptio
} else {
image, err = image_util.RetrieveSourceImage(s, opts)
if err != nil {
return nil, err
return nil, nil, err
}
}
cfg, err := initializeConfig(image, opts)
if err != nil {
return nil, err
return nil, nil, err
}

cmds, err := dockerfile.GetOnBuildInstructions(&cfg.Config, stageNameToIdx)
Expand All @@ -761,37 +765,38 @@ func CalculateDependencies(stages []config.KanikoStage, opts *config.KanikoOptio
switch cmd := c.(type) {
case *instructions.CopyCommand:
if cmd.From != "" {
i, err := strconv.Atoi(cmd.From)
if err != nil {
continue
}
resolved, err := util.ResolveEnvironmentReplacementList(cmd.SourcesAndDest.SourcePaths, ba.ReplacementEnvs(cfg.Config.Env), true)
if err != nil {
return nil, err
return nil, nil, err
}
i, err := strconv.Atoi(cmd.From)
if err == nil {
stageDepGraph[i] = append(stageDepGraph[i], resolved...)
} else {
imageDepGraph[cmd.From] = append(imageDepGraph[cmd.From], resolved...)
mafredri marked this conversation as resolved.
Show resolved Hide resolved
}
depGraph[i] = append(depGraph[i], resolved...)
}
case *instructions.EnvCommand:
if err := util.UpdateConfigEnv(cmd.Env, &cfg.Config, ba.ReplacementEnvs(cfg.Config.Env)); err != nil {
return nil, err
return nil, nil, err
}
image, err = mutate.Config(image, cfg.Config)
if err != nil {
return nil, err
return nil, nil, err
}
case *instructions.ArgCommand:
for _, arg := range cmd.Args {
k, v, err := commands.ParseArg(arg.Key, arg.Value, cfg.Config.Env, ba)
if err != nil {
return nil, err
return nil, nil, err
}
ba.AddArg(k, v)
}
}
}
images = append(images, image)
}
return depGraph, nil
return stageDepGraph, imageDepGraph, nil
}

// DoBuild executes building the Dockerfile
Expand All @@ -816,15 +821,17 @@ func DoBuild(opts *config.KanikoOptions) (v1.Image, error) {
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)
crossStageDependencies, imageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
if err != nil {
return nil, err
}
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
logrus.Infof("Built image deps: %v", imageDependencies)

// Some stages may refer to other random images, not previous stages
if err := fetchExtraStages(kanikoStages, opts, false, imageDependencies); err != nil {
mafredri marked this conversation as resolved.
Show resolved Hide resolved
return nil, errors.Wrap(err, "fetch extra stages failed")
mafredri marked this conversation as resolved.
Show resolved Hide resolved
}

var args *dockerfile.BuildArgs

Expand Down Expand Up @@ -959,15 +966,16 @@ func DoCacheProbe(opts *config.KanikoOptions) (v1.Image, error) {
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)
crossStageDependencies, imageDependencies, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
if err != nil {
return nil, err
}
logrus.Infof("Built cross stage deps: %v", crossStageDependencies)
logrus.Infof("Built image deps: %v", imageDependencies)
// Some stages may refer to other random images, not previous stages
if err := fetchExtraStages(kanikoStages, opts, true, imageDependencies); err != nil {
return nil, errors.Wrap(err, "fetch extra stages failed")
mafredri marked this conversation as resolved.
Show resolved Hide resolved
}

var args *dockerfile.BuildArgs

Expand Down Expand Up @@ -1021,6 +1029,19 @@ func DoCacheProbe(opts *config.KanikoOptions) (v1.Image, error) {
digestToCacheKey[d.String()] = sb.finalCacheKey
logrus.Infof("Mapping digest %v to cachekey %v", d.String(), sb.finalCacheKey)

if filesToCache, ok := crossStageDependencies[sb.stage.Index]; ok {
ifs, err := imagefs.New(
filesystem.FS,
filepath.Join(config.KanikoDir, strconv.Itoa(sb.stage.Index)),
sourceImage,
filesToCache,
)
if err != nil {
return nil, errors.Wrap(err, "could not create image filesystem")
}
filesystem.SetFS(ifs)
}

if stage.Final {
sourceImage, err = mutateCanonicalWithoutLayerEdit(sourceImage)
if err != nil {
Expand Down Expand Up @@ -1143,7 +1164,7 @@ func deduplicatePaths(paths []string) []string {
return deduped
}

func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) error {
func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions, cacheProbe bool, imageDependencies map[string][]string) error {
t := timing.Start("Fetching Extra Stages")
defer timing.DefaultRun.Stop(t)

Expand Down Expand Up @@ -1177,8 +1198,21 @@ func fetchExtraStages(stages []config.KanikoStage, opts *config.KanikoOptions) e
if err := saveStageAsTarball(c.From, sourceImage); err != nil {
return err
}
if err := extractImageToDependencyDir(c.From, sourceImage); err != nil {
return err
if !cacheProbe {
if err := extractImageToDependencyDir(c.From, sourceImage); err != nil {
return err
}
} else {
ifs, err := imagefs.New(
filesystem.FS,
filepath.Join(config.KanikoDir, c.From),
sourceImage,
imageDependencies[c.From],
)
if err != nil {
return errors.Wrap(err, "could not create image filesystem")
}
filesystem.SetFS(ifs)
}
}
// Store the name of the current stage in the list with names, if applicable.
Expand Down
33 changes: 28 additions & 5 deletions pkg/executor/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,10 @@ func TestCalculateDependencies(t *testing.T) {
mockInitConfig func(partial.WithConfigFile, *config.KanikoOptions) (*v1.ConfigFile, error)
}
tests := []struct {
name string
args args
want map[int][]string
name string
args args
want map[int][]string
wantImage map[string][]string
}{
{
name: "no deps",
Expand Down Expand Up @@ -359,9 +360,27 @@ COPY --from=second /bar /bat
1: {"/bar"},
},
},
{
name: "dependency from image",
args: args{
dockerfile: `
FROM scratch as target
COPY --from=alpine /etc/alpine-release /etc/alpine-release
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: have you tried implementing more tests?

  1. FROM three different image.
  2. Copy symlink.
  3. Overwriting a file FROM first image with the file FROM the second image.
  4. Image with cycle symlink inside.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I enabled the disabled TestDoCacheProbe/MultiStage test and added a symlink copy. This tests the basics of cache probe for multi-stage. We'll do more integration testing in envbuilder.

`,
},
wantImage: map[string][]string{
"alpine": {"/etc/alpine-release"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.want == nil {
tt.want = map[int][]string{}
}
if tt.wantImage == nil {
tt.wantImage = map[string][]string{}
}
if tt.args.mockInitConfig != nil {
original := initializeConfig
defer func() { initializeConfig = original }()
Expand All @@ -385,14 +404,18 @@ COPY --from=second /bar /bat
}
stageNameToIdx := ResolveCrossStageInstructions(kanikoStages)

got, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
got, gotImage, err := CalculateDependencies(kanikoStages, opts, stageNameToIdx)
if err != nil {
t.Errorf("got error: %s,", err)
}

if !reflect.DeepEqual(got, tt.want) {
diff := cmp.Diff(got, tt.want)
t.Errorf("CalculateDependencies() = %v, want %v, diff %v", got, tt.want, diff)
t.Errorf("CalculateDependencies() crossStageDependencies = %v, want %v, diff %v", got, tt.want, diff)
}
if !reflect.DeepEqual(gotImage, tt.wantImage) {
diff := cmp.Diff(gotImage, tt.wantImage)
t.Errorf("CalculateDependencies() imageDependencies = %v, wantImage %v, diff %v", gotImage, tt.wantImage, diff)
mafredri marked this conversation as resolved.
Show resolved Hide resolved
}
})
}
Expand Down
Loading
Loading