Skip to content

Commit

Permalink
Add unit tests for run image resolution
Browse files Browse the repository at this point in the history
Co-authored-by: Johannes Dillmann <j.dillmann@sap.com>
Co-authored-by: Philipp Stehle <philipp.stehle@sap.com>
  • Loading branch information
3 people committed Mar 1, 2023
1 parent 3c002fb commit 3dc6f0a
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 34 deletions.
3 changes: 1 addition & 2 deletions buildpack/testmock/env.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmd/lifecycle/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (a *analyzeCmd) Args(nargs int, args []string) error {
return cmd.FailErrCode(err, cmd.CodeForInvalidArgs, "parse arguments")
}
a.LifecycleInputs.OutputImageRef = args[0]
a.LifecycleInputs.AccessChecker = &platform.AccessCheckerImpl{}
if err := platform.ResolveInputs(platform.Analyze, &a.LifecycleInputs, cmd.DefaultLogger); err != nil {
return cmd.FailErrCode(err, cmd.CodeForInvalidArgs, "resolve inputs")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/lifecycle/rebaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func (r *rebaseCmd) setAppImage() error {
if md.Stack.RunImage.Image == "" {
return cmd.FailErrCode(errors.New("-run-image is required when there is no stack metadata available"), cmd.CodeForInvalidArgs, "parse arguments")
}
r.RunImageRef, err = md.Stack.BestRunImageMirror(registry)
r.RunImageRef, err = md.Stack.BestRunImageMirror(registry, &platform.AccessCheckerImpl{})
if err != nil {
return err
}
Expand Down
3 changes: 1 addition & 2 deletions launch/testmock/launch_env.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions launch/testmock/launch_execd.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions platform/analyze_inputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/apex/log"
"github.com/apex/log/handlers/memory"
"github.com/golang/mock/gomock"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"

Expand All @@ -15,6 +16,7 @@ import (
llog "github.com/buildpacks/lifecycle/log"
"github.com/buildpacks/lifecycle/platform"
h "github.com/buildpacks/lifecycle/testhelpers"
"github.com/buildpacks/lifecycle/testmock"
)

func TestAnalyzeInputs(t *testing.T) {
Expand All @@ -26,16 +28,28 @@ func TestAnalyzeInputs(t *testing.T) {
func testAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G, it spec.S) {
return func(t *testing.T, when spec.G, it spec.S) {
var (
inputs platform.LifecycleInputs
logHandler *memory.Handler
logger llog.Logger
accessCheckerMock *testmock.MockAccessChecker
ctrl *gomock.Controller
inputs platform.LifecycleInputs
logHandler *memory.Handler
logger llog.Logger
)

it.Before(func() {
inputs = platform.DefaultAnalyzeInputs(api.MustParse(platformAPI))
inputs.OutputImageRef = "some-output-image" // satisfy validation
logHandler = memory.New()
logger = &log.Logger{Handler: logHandler}
ctrl = gomock.NewController(t)
accessCheckerMock = testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).AnyTimes().Return(true, nil)
inputs.AccessChecker = accessCheckerMock
})

it.After(func() {
if ctrl != nil {
ctrl.Finish()
}
})

when("latest Platform API(s)", func() {
Expand Down
1 change: 1 addition & 0 deletions platform/create_inputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func testCreateInputs(platformAPI string) func(t *testing.T, when spec.G, it spe
inputs = platform.DefaultAnalyzeInputs(api.MustParse(platformAPI))
inputs.OutputImageRef = "some-output-image" // satisfy validation
inputs.RunImageRef = "some-run-image" // satisfy validation
inputs.AccessChecker = &platform.AccessCheckerImpl{}
logHandler = memory.New()
logger = &log.Logger{Handler: logHandler}
})
Expand Down
60 changes: 43 additions & 17 deletions platform/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"

"github.com/BurntSushi/toml"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/pkg/errors"

Expand Down Expand Up @@ -282,50 +283,75 @@ type RunImageMetadata struct {
Mirrors []string `toml:"mirrors" json:"mirrors,omitempty"`
}

func (rm *RunImageMetadata) BestRunImageMirror(registry string) (string, error) {
//go:generate mockgen -package testmock -destination ../testmock/access_checker.go github.com/buildpacks/lifecycle/platform AccessChecker
type AccessChecker interface {
CheckReadAccess(repo string, keychain authn.Keychain) (bool, error)
}

type AccessCheckerImpl struct{}

var _ AccessChecker = &AccessCheckerImpl{}

func (a *AccessCheckerImpl) CheckReadAccess(repo string, keychain authn.Keychain) (bool, error) {
img, err := remote.NewImage(repo, keychain)
if err != nil {
return false, errors.Wrap(err, "failed to parse image reference")
}

return img.CheckReadAccess(), nil
}

func (rm *RunImageMetadata) BestRunImageMirror(registry string, accessChecker AccessChecker) (string, error) {
if rm.Image == "" {
return "", errors.New("missing run-image metadata")
}

runImageMirrors := []string{rm.Image}
runImageMirrors = append(runImageMirrors, rm.Mirrors...)
runImageRef := byRegistry(registry, runImageMirrors)
if runImageRef != "" {
// Found run image in the registry of the target image
return runImageRef, nil
}

keychain, err := auth.DefaultKeychain(runImageMirrors...)
if err != nil {
return "", errors.Wrap(err, "failed to get registry credentials")
return "", errors.Wrap(err, "unable to create keychain")
}

runImageRef := byRegistry(registry, runImageMirrors, accessChecker, keychain)
if runImageRef != "" {
return runImageRef, nil
}

for _, image := range runImageMirrors {
img, err := remote.NewImage(image, keychain)
ok, err := accessChecker.CheckReadAccess(image, keychain)
if err != nil {
return "", errors.Wrap(err, "failed to parse image reference")
return "", err
}

if img.CheckReadAccess() {
if ok {
return image, nil
}
}

return "", errors.Wrap(err, "failed to find accessible run image")
return "", errors.New("failed to find accessible run image")
}

func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) {
return sm.RunImage.BestRunImageMirror(registry)
func (sm *StackMetadata) BestRunImageMirror(registry string, accessChecker AccessChecker) (string, error) {
return sm.RunImage.BestRunImageMirror(registry, accessChecker)
}

func byRegistry(reg string, imgs []string) string {
for _, img := range imgs {
ref, err := name.ParseReference(img, name.WeakValidation)
func byRegistry(reg string, repos []string, accessChecker AccessChecker, keychain authn.Keychain) string {
for _, repo := range repos {
ref, err := name.ParseReference(repo, name.WeakValidation)
if err != nil {
continue
}
if reg == ref.Context().RegistryStr() {
return img
ok, err := accessChecker.CheckReadAccess(repo, keychain)
if err != nil {
return ""
}

if ok {
return repo
}
}
}

Expand Down
64 changes: 59 additions & 5 deletions platform/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package platform_test
import (
"testing"

"github.com/golang/mock/gomock"
"github.com/google/go-cmp/cmp"
"github.com/sclevine/spec"

"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/buildpack"
"github.com/buildpacks/lifecycle/platform"
h "github.com/buildpacks/lifecycle/testhelpers"
"github.com/buildpacks/lifecycle/testmock"
)

func TestFiles(t *testing.T) {
Expand Down Expand Up @@ -97,6 +99,8 @@ func testFiles(t *testing.T, when spec.G, it spec.S) {
when("stack.toml", func() {
when("BestRunImageMirror", func() {
var stackMD *platform.StackMetadata
var accessCheckerDefaultMock *testmock.MockAccessChecker
var ctrl *gomock.Controller

it.Before(func() {
stackMD = &platform.StackMetadata{RunImage: platform.RunImageMetadata{
Expand All @@ -107,37 +111,83 @@ func testFiles(t *testing.T, when spec.G, it spec.S) {
"gcr.io/org/repo",
},
}}
ctrl = gomock.NewController(t)
accessCheckerDefaultMock = testmock.NewMockAccessChecker(ctrl)
accessCheckerDefaultMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()
})

it.After(func() {
if ctrl != nil {
ctrl.Finish()
}
})

when("repoName is dockerhub", func() {
it("returns the dockerhub image", func() {
name, err := stackMD.BestRunImageMirror("index.docker.io")
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("myorg/myrepo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("index.docker.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "myorg/myrepo")
})
})

when("registry is gcr.io", func() {
it("returns the gcr.io image", func() {
name, err := stackMD.BestRunImageMirror("gcr.io")
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("gcr.io/org/repo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "gcr.io/org/repo")
})

when("registry is zonal.gcr.io", func() {
it("returns the gcr image", func() {
name, err := stackMD.BestRunImageMirror("zonal.gcr.io")
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("zonal.gcr.io/org/repo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("zonal.gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "zonal.gcr.io/org/repo")
})
})

when("registry is missingzone.gcr.io", func() {
it("returns the run image", func() {
name, err := stackMD.BestRunImageMirror("missingzone.gcr.io")
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("first.com/org/repo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("missingzone.gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "first.com/org/repo")
})

it("returns first mirror if run image is not readable", func() {
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("myorg/myrepo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("missingzone.gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "myorg/myrepo")
})

it("returns the first readable mirror", func() {
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("zonal.gcr.io/org/repo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess("gcr.io/org/repo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("missingzone.gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "zonal.gcr.io/org/repo")
})
})
})

Expand All @@ -147,7 +197,11 @@ func testFiles(t *testing.T, when spec.G, it spec.S) {
})

it("skips over it", func() {
name, err := stackMD.BestRunImageMirror("gcr.io")
accessCheckerMock := testmock.NewMockAccessChecker(ctrl)
accessCheckerMock.EXPECT().CheckReadAccess("gcr.io/myorg/myrepo", gomock.Any()).Return(true, nil).AnyTimes()
accessCheckerMock.EXPECT().CheckReadAccess(gomock.Any(), gomock.Any()).Return(false, nil).AnyTimes()

name, err := stackMD.BestRunImageMirror("gcr.io", accessCheckerMock)
h.AssertNil(t, err)
h.AssertEq(t, name, "gcr.io/myorg/myrepo")
})
Expand Down
5 changes: 3 additions & 2 deletions platform/lifecycle_inputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
// Fields are the cumulative total of inputs across all lifecycle phases and all supported Platform APIs.
type LifecycleInputs struct {
PlatformAPI *api.Version
AccessChecker AccessChecker
AnalyzedPath string
AppDir string
BuildConfigDir string
Expand Down Expand Up @@ -215,7 +216,7 @@ func fillRunImageFromRunTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) erro
if len(runMD.Images) == 0 {
return errors.New(ErrRunImageRequiredWhenNoRunMD)
}
i.RunImageRef, err = runMD.Images[0].BestRunImageMirror(targetRegistry)
i.RunImageRef, err = runMD.Images[0].BestRunImageMirror(targetRegistry, i.AccessChecker)
if err != nil {
return errors.New(ErrRunImageRequiredWhenNoRunMD)
}
Expand All @@ -236,7 +237,7 @@ func fillRunImageFromStackTOMLIfNeeded(i *LifecycleInputs, logger log.Logger) er
if err != nil {
return err
}
i.RunImageRef, err = stackMD.BestRunImageMirror(targetRegistry)
i.RunImageRef, err = stackMD.BestRunImageMirror(targetRegistry, i.AccessChecker)
if err != nil {
return errors.New(ErrRunImageRequiredWhenNoStackMD)
}
Expand Down
Loading

0 comments on commit 3dc6f0a

Please sign in to comment.