diff --git a/acceptance/analyzer_test.go b/acceptance/analyzer_test.go index aaa5bee3c..08035a260 100644 --- a/acceptance/analyzer_test.go +++ b/acceptance/analyzer_test.go @@ -271,7 +271,7 @@ func testAnalyzerFunc(platformAPI string) func(t *testing.T, when spec.G, it spe output, err := cmd.CombinedOutput() h.AssertNotNil(t, err) - expected := "ensure registry read access to some-run-image-from-run-toml" + expected := "failed to find accessible run image" h.AssertStringContains(t, string(output), expected) }) }) diff --git a/acceptance/creator_test.go b/acceptance/creator_test.go index 3b4955c6a..42676e4db 100644 --- a/acceptance/creator_test.go +++ b/acceptance/creator_test.go @@ -74,7 +74,7 @@ func testCreatorFunc(platformAPI string) func(t *testing.T, when spec.G, it spec output, err := cmd.CombinedOutput() h.AssertNotNil(t, err) - expected := "ensure registry read access to some-run-image-from-run-toml" + expected := "failed to resolve inputs: failed to find accessible run image" h.AssertStringContains(t, string(output), expected) }) }) diff --git a/cmd/lifecycle/rebaser.go b/cmd/lifecycle/rebaser.go index 861da216f..5f68b9f55 100644 --- a/cmd/lifecycle/rebaser.go +++ b/cmd/lifecycle/rebaser.go @@ -176,7 +176,7 @@ func (r *rebaseCmd) setAppImage() error { } // for older platforms, we find the best mirror for the run image as this point - r.RunImageRef, err = md.Stack.BestRunImageMirror(registry) + r.RunImageRef, err = md.Stack.BestRunImageMirror(registry, r.LifecycleInputs.AccessChecker) if err != nil { return err } diff --git a/platform/files.go b/platform/files.go index 144be0a71..cf4010dea 100644 --- a/platform/files.go +++ b/platform/files.go @@ -10,10 +10,14 @@ import ( "github.com/buildpacks/lifecycle/internal/fsutil" "github.com/BurntSushi/toml" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" + "github.com/buildpacks/imgutil/remote" + "github.com/buildpacks/lifecycle/api" + "github.com/buildpacks/lifecycle/auth" "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/internal/encoding" "github.com/buildpacks/lifecycle/launch" @@ -412,38 +416,86 @@ type RunImageForExport struct { Mirrors []string `toml:"mirrors" json:"mirrors,omitempty"` } -func (rm *RunImageForExport) BestRunImageMirror(registry string) (string, error) { +type ImageStrategy interface { + CheckReadAccess(repo string, keychain authn.Keychain) (bool, error) +} + +type RemoteImageStrategy struct{} + +var _ ImageStrategy = &RemoteImageStrategy{} + +func (a *RemoteImageStrategy) 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 +} + +type LocalImageStrategy struct{} + +var _ ImageStrategy = &LocalImageStrategy{} + +func (a *LocalImageStrategy) CheckReadAccess(_ string, _ authn.Keychain) (bool, error) { + return true, nil +} + +func (rm *RunImageForExport) BestRunImageMirror(registry string, accessChecker ImageStrategy) (string, error) { if rm.Image == "" { return "", errors.New("missing run-image metadata") } + runImageMirrors := []string{rm.Image} runImageMirrors = append(runImageMirrors, rm.Mirrors...) - runImageRef, err := byRegistry(registry, runImageMirrors) + + keychain, err := auth.DefaultKeychain(runImageMirrors...) if err != nil { - return "", errors.Wrap(err, "failed to find run image") + return "", errors.Wrap(err, "unable to create keychain") } - return runImageRef, nil -} -func (sm *StackMetadata) BestRunImageMirror(registry string) (string, error) { - return sm.RunImage.BestRunImageMirror(registry) -} + runImageRef := byRegistry(registry, runImageMirrors, accessChecker, keychain) + if runImageRef != "" { + return runImageRef, nil + } + + for _, image := range runImageMirrors { + ok, err := accessChecker.CheckReadAccess(image, keychain) + if err != nil { + return "", err + } -func byRegistry(reg string, imgs []string) (string, error) { - if len(imgs) < 1 { - return "", errors.New("no images provided to search") + if ok { + return image, nil + } } - for _, img := range imgs { - ref, err := name.ParseReference(img, name.WeakValidation) + return "", errors.New("failed to find accessible run image") +} + +func (sm *StackMetadata) BestRunImageMirror(registry string, accessChecker ImageStrategy) (string, error) { + return sm.RunImage.BestRunImageMirror(registry, accessChecker) +} + +func byRegistry(reg string, repos []string, accessChecker ImageStrategy, 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, nil + ok, err := accessChecker.CheckReadAccess(repo, keychain) + if err != nil { + return "" + } + + if ok { + return repo + } } } - return imgs[0], nil + + return "" } func ReadStack(stackPath string, logger log.Logger) (StackMetadata, error) { diff --git a/platform/files_test.go b/platform/files_test.go index 4051680d1..da65ae806 100644 --- a/platform/files_test.go +++ b/platform/files_test.go @@ -16,6 +16,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/platform" + "github.com/buildpacks/lifecycle/platform/testhelpers" h "github.com/buildpacks/lifecycle/testhelpers" ) @@ -119,7 +120,7 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { when("repoName is dockerhub", func() { it("returns the dockerhub image", func() { - name, err := stackMD.BestRunImageMirror("index.docker.io") + name, err := stackMD.BestRunImageMirror("index.docker.io", &testhelpers.SimpleImageStrategy{}) h.AssertNil(t, err) h.AssertEq(t, name, "myorg/myrepo") }) @@ -127,14 +128,14 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { when("registry is gcr.io", func() { it("returns the gcr.io image", func() { - name, err := stackMD.BestRunImageMirror("gcr.io") + name, err := stackMD.BestRunImageMirror("gcr.io", &testhelpers.SimpleImageStrategy{}) 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") + name, err := stackMD.BestRunImageMirror("zonal.gcr.io", &testhelpers.SimpleImageStrategy{}) h.AssertNil(t, err) h.AssertEq(t, name, "zonal.gcr.io/org/repo") }) @@ -142,10 +143,16 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { when("registry is missingzone.gcr.io", func() { it("returns the run image", func() { - name, err := stackMD.BestRunImageMirror("missingzone.gcr.io") + name, err := stackMD.BestRunImageMirror("missingzone.gcr.io", &testhelpers.SimpleImageStrategy{}) h.AssertNil(t, err) h.AssertEq(t, name, "first.com/org/repo") }) + + it("returns the first readable mirror", func() { + name, err := stackMD.BestRunImageMirror("missingzone.gcr.io", &testhelpers.StubImageStrategy{AllowedRepo: "zonal.gcr.io"}) + h.AssertNil(t, err) + h.AssertEq(t, name, "zonal.gcr.io/org/repo") + }) }) }) @@ -155,7 +162,7 @@ func testFiles(t *testing.T, when spec.G, it spec.S) { }) it("skips over it", func() { - name, err := stackMD.BestRunImageMirror("gcr.io") + name, err := stackMD.BestRunImageMirror("gcr.io", &testhelpers.SimpleImageStrategy{}) h.AssertNil(t, err) h.AssertEq(t, name, "gcr.io/myorg/myrepo") }) diff --git a/platform/lifecycle_inputs.go b/platform/lifecycle_inputs.go index 76d198145..148eec27c 100644 --- a/platform/lifecycle_inputs.go +++ b/platform/lifecycle_inputs.go @@ -16,6 +16,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 ImageStrategy AnalyzedPath string AppDir string BuildConfigDir string diff --git a/platform/resolve_analyze_inputs_test.go b/platform/resolve_analyze_inputs_test.go index 68487bb3a..862faabf2 100644 --- a/platform/resolve_analyze_inputs_test.go +++ b/platform/resolve_analyze_inputs_test.go @@ -36,6 +36,7 @@ func testResolveAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G inputs.OutputImageRef = "some-output-image" // satisfy validation logHandler = memory.New() logger = &log.Logger{Handler: logHandler} + inputs.UseDaemon = true // to prevent access checking of run images }) when("latest Platform API(s)", func() { @@ -124,6 +125,7 @@ func testResolveAnalyzeInputs(platformAPI string) func(t *testing.T, when spec.G it.Before(func() { h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") inputs.RunImageRef = "some-run-image" // satisfy validation + inputs.UseDaemon = false }) when("provided destination tags are on different registries", func() { diff --git a/platform/resolve_create_inputs_test.go b/platform/resolve_create_inputs_test.go index 8d53d7ea3..bd2e093cc 100644 --- a/platform/resolve_create_inputs_test.go +++ b/platform/resolve_create_inputs_test.go @@ -37,6 +37,7 @@ func testResolveCreateInputs(platformAPI string) func(t *testing.T, when spec.G, inputs.RunImageRef = "some-run-image" // satisfy validation logHandler = memory.New() logger = &log.Logger{Handler: logHandler} + inputs.UseDaemon = true // to prevent read access check for run image }) when("latest Platform API(s)", func() { @@ -122,6 +123,7 @@ func testResolveCreateInputs(platformAPI string) func(t *testing.T, when spec.G, when("Platform API >= 0.7", func() { it.Before(func() { h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.7"), "") + inputs.UseDaemon = false }) when("provided destination tags are on different registries", func() { diff --git a/platform/resolve_inputs.go b/platform/resolve_inputs.go index dcf6a8691..9e63bf079 100644 --- a/platform/resolve_inputs.go +++ b/platform/resolve_inputs.go @@ -20,6 +20,12 @@ var ( ) func ResolveInputs(phase LifecyclePhase, i *LifecycleInputs, logger log.Logger) error { + if i.UseDaemon || i.UseLayout { + i.AccessChecker = &LocalImageStrategy{} + } else { + i.AccessChecker = &RemoteImageStrategy{} + } + // order of operations is important ops := []LifecycleInputsOperation{UpdatePlaceholderPaths, ResolveAbsoluteDirPaths} switch phase { @@ -178,11 +184,8 @@ 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) - if err != nil { - return errors.New(ErrRunImageRequiredWhenNoRunMD) - } - return nil + i.RunImageRef, err = runMD.Images[0].BestRunImageMirror(targetRegistry, i.AccessChecker) + return err } // fillRunImageFromStackTOMLIfNeeded updates the provided lifecycle inputs to include the run image from stack.toml if the run image input it is missing. @@ -199,7 +202,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) } diff --git a/platform/testhelpers/image_strategy.go b/platform/testhelpers/image_strategy.go new file mode 100644 index 000000000..1c98692e0 --- /dev/null +++ b/platform/testhelpers/image_strategy.go @@ -0,0 +1,44 @@ +package testhelpers + +import ( + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + + "github.com/buildpacks/lifecycle/platform" +) + +type TestResource struct { + Repo string +} + +var _ authn.Resource = &TestResource{} + +func (t *TestResource) String() string { + return t.Repo +} + +func (t *TestResource) RegistryStr() string { + return t.Repo +} + +type SimpleImageStrategy struct{} + +var _ platform.ImageStrategy = &SimpleImageStrategy{} + +func (t *SimpleImageStrategy) CheckReadAccess(repo string, keychain authn.Keychain) (bool, error) { + resource := &TestResource{Repo: repo} + _, err := keychain.Resolve(resource) + + return (err == nil), err +} + +type StubImageStrategy struct { + AllowedRepo string +} + +var _ platform.ImageStrategy = &StubImageStrategy{} + +func (s *StubImageStrategy) CheckReadAccess(repo string, _ authn.Keychain) (bool, error) { + return strings.Contains(repo, s.AllowedRepo), nil +}