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

Refactor registry test code and fix fuzz integration #233

Merged
merged 1 commit into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions controllers/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
aclapi "github.com/fluxcd/pkg/apis/acl"

imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1"
"github.com/fluxcd/image-reflector-controller/internal/test"
// +kubebuilder:scaffold:imports
)

Expand All @@ -42,7 +43,7 @@ var _ = Describe("ImagePolicy controller", func() {
var registryServer *httptest.Server

BeforeEach(func() {
registryServer = newRegistryServer()
registryServer = test.NewRegistryServer()
})

AfterEach(func() {
Expand All @@ -61,7 +62,7 @@ var _ = Describe("ImagePolicy controller", func() {
It("fails to reconcile an ImagePolicy with a cross-ns ref", func() {
// a bona fide image repo is needed so that it _would_ succeed if not for the disallowed cross-ns ref.
versions := []string{"1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -119,7 +120,7 @@ var _ = Describe("ImagePolicy controller", func() {
When("Using SemVerPolicy", func() {
It("calculates an image from a repository's tags", func() {
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -184,7 +185,7 @@ var _ = Describe("ImagePolicy controller", func() {
When("Using SemVerPolicy with invalid range", func() {
It("fails with invalid policy error", func() {
versions := []string{"0.1.0", "0.1.1", "0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -250,7 +251,7 @@ var _ = Describe("ImagePolicy controller", func() {
When("Usign AlphabeticalPolicy", func() {
It("calculates an image from a repository's tags", func() {
versions := []string{"xenial", "yakkety", "zesty", "artful", "bionic"}
imgRepo := loadImages(registryServer, "test-alphabetical-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-alphabetical-policy-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -312,7 +313,7 @@ var _ = Describe("ImagePolicy controller", func() {
When("valid regex supplied", func() {
It("correctly filters the repo tags", func() {
versions := []string{"test-0.1.0", "test-0.1.1", "dev-0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Expand Down Expand Up @@ -380,7 +381,7 @@ var _ = Describe("ImagePolicy controller", func() {
When("invalid regex supplied", func() {
It("fails to reconcile returning error", func() {
versions := []string{"test-0.1.0", "test-0.1.1", "dev-0.2.0", "1.0.0", "1.0.1", "1.0.2", "1.1.0-alpha"}
imgRepo := loadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "test-semver-policy-"+randStringRunes(5), versions)
repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Interval: metav1.Duration{Duration: reconciliationInterval},
Expand Down Expand Up @@ -455,7 +456,7 @@ var _ = Describe("ImagePolicy controller", func() {
It("grants access", func() {
versions := []string{"1.0.0", "1.0.1"}
imageName := "test-acl-" + randStringRunes(5)
imgRepo := loadImages(registryServer, imageName, versions)
imgRepo := test.LoadImages(registryServer, imageName, versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -514,7 +515,7 @@ var _ = Describe("ImagePolicy controller", func() {
Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":1.0.1"))

// Updating the image should reconcile the cross-namespace policy
imgRepo = loadImages(registryServer, imageName, []string{"1.0.2"})
imgRepo = test.LoadImages(registryServer, imageName, []string{"1.0.2"})
Eventually(func() bool {
err := r.Get(ctx, imageObjectName, &repo)
return err == nil && repo.Status.LastScanResult.TagCount == len(versions)+1
Expand Down Expand Up @@ -542,7 +543,7 @@ var _ = Describe("ImagePolicy controller", func() {
defer k8sClient.Delete(context.Background(), policyNamespace)

versions := []string{"1.0.0", "1.0.1"}
imgRepo := loadImages(registryServer, "acl-image-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "acl-image-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -615,7 +616,7 @@ var _ = Describe("ImagePolicy controller", func() {
defer k8sClient.Delete(context.Background(), policyNamespace)

versions := []string{"1.0.0", "1.0.1"}
imgRepo := loadImages(registryServer, "acl-image-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "acl-image-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -698,7 +699,7 @@ var _ = Describe("ImagePolicy controller", func() {

versions := []string{"1.0.0", "1.0.1"}
imageName := "acl-image-" + randStringRunes(5)
imgRepo := loadImages(registryServer, imageName, versions)
imgRepo := test.LoadImages(registryServer, imageName, versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down Expand Up @@ -771,7 +772,7 @@ var _ = Describe("ImagePolicy controller", func() {
Expect(pol.Status.LatestImage).To(Equal(imgRepo + ":1.0.1"))

// Updating the image should reconcile the cross-namespace policy
imgRepo = loadImages(registryServer, imageName, []string{"1.0.2"})
imgRepo = test.LoadImages(registryServer, imageName, []string{"1.0.2"})
Eventually(func() bool {
err := r.Get(ctx, repoObjectName, &repo)
return err == nil && repo.Status.LastScanResult.TagCount == len(versions)+1
Expand Down Expand Up @@ -799,7 +800,7 @@ var _ = Describe("ImagePolicy controller", func() {
defer k8sClient.Delete(context.Background(), policyNamespace)

versions := []string{"1.0.0", "1.0.1"}
imgRepo := loadImages(registryServer, "acl-image-"+randStringRunes(5), versions)
imgRepo := test.LoadImages(registryServer, "acl-image-"+randStringRunes(5), versions)

repo := imagev1.ImageRepository{
Spec: imagev1.ImageRepositorySpec{
Expand Down
170 changes: 7 additions & 163 deletions controllers/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,25 @@ limitations under the License.
package controllers

import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"

"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/fluxcd/image-reflector-controller/internal/test"
)

var _ = Context("Registry handler", func() {

It("serves a tag list", func() {
srv := newRegistryServer()
srv := test.NewRegistryServer()
defer srv.Close()

uploadedTags := []string{"tag1", "tag2"}
repoString := loadImages(srv, "testimage", uploadedTags)
repoString := test.LoadImages(srv, "testimage", uploadedTags)
repo, _ := name.NewRepository(repoString)

tags, err := remote.List(repo)
Expand All @@ -48,158 +44,6 @@ var _ = Context("Registry handler", func() {
})
})

// ---

// pre-populated db of tags, so it's not necessary to upload images to
// get results from remote.List.
var convenientTags = map[string][]string{
"convenient": []string{
"tag1", "tag2",
},
}

// set up a local registry for testing scanning
func newRegistryServer() *httptest.Server {
regHandler := registry.New()
srv := httptest.NewServer(&tagListHandler{
registryHandler: regHandler,
imagetags: convenientTags,
})
return srv
}

func newAuthenticatedRegistryServer(username, pass string) *httptest.Server {
regHandler := registry.New()
regHandler = &tagListHandler{
registryHandler: regHandler,
imagetags: convenientTags,
}
regHandler = &authHandler{
registryHandler: regHandler,
allowedUser: username,
allowedPass: pass,
}
srv := httptest.NewServer(regHandler)
return srv
}

// Get the registry part of an image from the registry server
func registryName(srv *httptest.Server) string {
if strings.HasPrefix(srv.URL, "https://") {
return strings.TrimPrefix(srv.URL, "https://")
} // else assume HTTP
return strings.TrimPrefix(srv.URL, "http://")
}

// loadImages uploads images to the local registry, and returns the
// image repo
// name. https://github.com/google/go-containerregistry/blob/v0.1.1/pkg/registry/compatibility_test.go
// has an example of loading a test registry with a random image.
func loadImages(srv *httptest.Server, imageName string, versions []string, options ...remote.Option) string {
imgRepo := registryName(srv) + "/" + imageName
for _, tag := range versions {
imgRef, err := name.NewTag(imgRepo + ":" + tag)
Expect(err).ToNot(HaveOccurred())
img, err := random.Image(512, 1)
Expect(err).ToNot(HaveOccurred())
Expect(remote.Write(imgRef, img, options...)).To(Succeed())
}
return imgRepo
}

// the go-containerregistry test registry implementation does not
// serve /myimage/tags/list. Until it does, I'm adding this handler.
// NB:
// - assumes repo name is a single element
// - assumes no overwriting tags

type tagListHandler struct {
registryHandler http.Handler
imagetags map[string][]string
}

type tagListResult struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}

func (h *tagListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// a tag list request has a path like: /v2/<repo>/tags/list
if withoutTagsList := strings.TrimSuffix(r.URL.Path, "/tags/list"); r.Method == "GET" && withoutTagsList != r.URL.Path {
repo := strings.TrimPrefix(withoutTagsList, "/v2/")
if tags, ok := h.imagetags[repo]; ok {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
result := tagListResult{
Name: repo,
Tags: tags,
}
Expect(json.NewEncoder(w).Encode(result)).To(Succeed())
println("Requested tags", repo, strings.Join(tags, ", "))
return
}
w.WriteHeader(http.StatusNotFound)
return
}

// record the fact of a PUT to a tag; the path looks like: /v2/<repo>/manifests/<tag>
h.registryHandler.ServeHTTP(w, r)
if r.Method == "PUT" {
pathElements := strings.Split(r.URL.Path, "/")
if len(pathElements) == 5 && pathElements[1] == "v2" && pathElements[3] == "manifests" {
repo, tag := pathElements[2], pathElements[4]
println("Recording tag", repo, tag)
h.imagetags[repo] = append(h.imagetags[repo], tag)
}
}
}

// there's no authentication in go-containerregistry/pkg/registry;
// this wrapper adds basic auth to a registry handler. NB: the
// important thing is to be able to test that the credentials get from
// the secret to the registry API library; it's assumed that the
// registry API library does e.g., OAuth2 correctly. See
// https://tools.ietf.org/html/rfc7617 regarding basic authentication.

type authHandler struct {
allowedUser, allowedPass string
registryHandler http.Handler
}

// ServeHTTP serves a request which needs authentication.
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Add("WWW-Authenticate", `Basic realm="Registry"`)
w.WriteHeader(401)
return
}
if !strings.HasPrefix(authHeader, "Basic ") {
w.WriteHeader(403)
w.Write([]byte(`Authorization header does not being with "Basic "`))
return
}
namePass, err := base64.StdEncoding.DecodeString(authHeader[6:])
if err != nil {
w.WriteHeader(403)
w.Write([]byte(`Authorization header doesn't appear to be base64-encoded`))
return
}
namePassSlice := strings.SplitN(string(namePass), ":", 2)
if len(namePassSlice) != 2 {
w.WriteHeader(403)
w.Write([]byte(`Authorization header doesn't appear to be colon-separated value `))
w.Write(namePass)
return
}
if namePassSlice[0] != h.allowedUser || namePassSlice[1] != h.allowedPass {
w.WriteHeader(403)
w.Write([]byte(`Authorization failed: wrong username or password`))
return
}
h.registryHandler.ServeHTTP(w, r)
}

var _ = Context("Authentication handler", func() {

var registryServer *httptest.Server
Expand All @@ -208,22 +52,22 @@ var _ = Context("Authentication handler", func() {
BeforeEach(func() {
username = "user"
password = "password1"
registryServer = newAuthenticatedRegistryServer(username, password)
registryServer = test.NewAuthenticatedRegistryServer(username, password)
})

AfterEach(func() {
registryServer.Close()
})

It("rejects requests without authentication", func() {
repo, err := name.NewRepository(registryName(registryServer) + "/convenient")
repo, err := name.NewRepository(test.RegistryName(registryServer) + "/convenient")
Expect(err).ToNot(HaveOccurred())
_, err = remote.List(repo)
Expect(err).To(HaveOccurred())
})

It("accepts requests with correct authentication", func() {
repo, err := name.NewRepository(registryName(registryServer) + "/convenient")
repo, err := name.NewRepository(test.RegistryName(registryServer) + "/convenient")
Expect(err).ToNot(HaveOccurred())
auth := &authn.Basic{
Username: username,
Expand Down
Loading