From 789f5d2814994a598cd6a1a79aea857a8d678df8 Mon Sep 17 00:00:00 2001 From: Aurel Canciu Date: Sun, 15 Nov 2020 21:59:45 +0200 Subject: [PATCH] Add bucket controller tests Add basic integration tests for the bucket controller Signed-off-by: Aurel Canciu --- controllers/bucket_controller_test.go | 308 ++++++++++++++++++++++++++ controllers/suite_test.go | 11 + 2 files changed, 319 insertions(+) create mode 100644 controllers/bucket_controller_test.go diff --git a/controllers/bucket_controller_test.go b/controllers/bucket_controller_test.go new file mode 100644 index 000000000..cce269107 --- /dev/null +++ b/controllers/bucket_controller_test.go @@ -0,0 +1,308 @@ +/* +Copyright 2020 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "crypto/md5" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "runtime" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" +) + +var _ = Describe("BucketReconciler", func() { + + const ( + timeout = time.Second * 30 + interval = time.Second * 1 + indexInterval = time.Second * 1 + bucketTimeout = time.Second * 5 + ) + + var ( + namespace *corev1.Namespace + s3Server *s3MockServer + err error + ) + + BeforeEach(func() { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "bucket-test-" + randStringRunes(5)}, + } + err = k8sClient.Create(ctx, namespace) + Expect(err).NotTo(HaveOccurred(), "failed to create test namespace") + + s3Server = newS3Server("test-bucket") + s3Server.Objects = []*s3MockObject{ + { + Key: "test.txt", + Content: []byte("test"), + ContentType: "text/plain", + LastModified: time.Now(), + }, + } + s3Server.Start() + }) + + AfterEach(func() { + s3Server.Stop() + + err = k8sClient.Delete(ctx, namespace) + Expect(err).NotTo(HaveOccurred(), "failed to delete test namespace") + }) + + It("Creates artifacts for", func() { + u, err := url.Parse(s3Server.GetURL()) + Expect(err).NotTo(HaveOccurred()) + + key := types.NamespacedName{ + Name: "bucket-test-" + randStringRunes(5), + Namespace: namespace.Name, + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Data: map[string][]byte{ + "accesskey": []byte("key"), + "secretkey": []byte("secret"), + }, + } + + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + defer k8sClient.Delete(ctx, secret) + + bucket := &sourcev1.Bucket{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: sourcev1.BucketSpec{ + Provider: "aws", + BucketName: s3Server.BucketName, + Endpoint: u.Host, + Insecure: true, + Interval: metav1.Duration{Duration: indexInterval}, + Timeout: &metav1.Duration{Duration: bucketTimeout}, + SecretRef: &corev1.LocalObjectReference{ + Name: secret.Name, + }, + }, + } + + Expect(k8sClient.Create(ctx, bucket)).Should(Succeed()) + defer k8sClient.Delete(ctx, bucket) + + By("Expecting artifact") + Eventually(func() bool { + if err := k8sClient.Get(ctx, key, bucket); err != nil { + return false + } + return bucket.Status.Artifact != nil && + storage.ArtifactExist(*bucket.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + + By("Expecting finalizers to be registered") + Expect(len(bucket.Finalizers) > 0).To(BeTrue()) + + By("Updating the bucket files") + s3Server.Objects = append(s3Server.Objects, &s3MockObject{ + Key: "new.txt", + Content: []byte("new"), + ContentType: "text/plain", + LastModified: time.Now(), + }) + + By("Expecting checksum change and GC") + Eventually(func() bool { + now := &sourcev1.Bucket{} + if err := k8sClient.Get(ctx, key, now); err != nil { + return false + } + // Test revision change and garbage collection + return now.Status.Artifact.Checksum != bucket.Status.Artifact.Checksum && + !storage.ArtifactExist(*bucket.Status.Artifact) + }, timeout, interval).Should(BeTrue()) + + updated := &sourcev1.Bucket{} + Expect(k8sClient.Get(ctx, key, updated)).Should(Succeed()) + updated.Spec.BucketName = "invalid#bucket?" + Expect(k8sClient.Update(ctx, updated)).Should(Succeed()) + Eventually(func() bool { + if err := k8sClient.Get(ctx, key, updated); err != nil { + return false + } + for _, c := range updated.Status.Conditions { + if c.Reason == sourcev1.BucketOperationFailedReason { + return true + } + } + return false + }, timeout, interval).Should(BeTrue()) + Expect(updated.Status.Artifact).ToNot(BeNil()) + + By("Expecting to delete successfully") + Eventually(func() error { + _ = k8sClient.Get(ctx, key, bucket) + return k8sClient.Delete(ctx, bucket) + }, timeout, interval).Should(Succeed()) + + By("Expecting delete to finish") + Eventually(func() error { + return k8sClient.Get(ctx, key, bucket) + }, timeout, interval).ShouldNot(Succeed()) + + exists := func(path string) bool { + // wait for tmp sync on macOS + if runtime.GOOS == "darwin" { + time.Sleep(time.Second) + } + + _, err := os.Stat(path) + return err == nil + } + + By("Expecting GC after delete") + Eventually(exists(bucket.Status.Artifact.Path), timeout, interval).ShouldNot(BeTrue()) + }) +}) + +type s3MockObject struct { + Key string + LastModified time.Time + ContentType string + Content []byte +} + +type s3MockServer struct { + srv *httptest.Server + mux *http.ServeMux + + BucketName string + Objects []*s3MockObject +} + +func newS3Server(bucketName string) *s3MockServer { + s := &s3MockServer{BucketName: bucketName} + s.mux = http.NewServeMux() + s.mux.Handle(fmt.Sprintf("/%s/", s.BucketName), http.HandlerFunc(s.handler)) + + s.srv = httptest.NewUnstartedServer(s.mux) + + return s +} + +func (s *s3MockServer) Start() { + s.srv.Start() +} + +func (s *s3MockServer) Stop() { + s.srv.Close() +} + +func (s *s3MockServer) GetURL() string { + return s.srv.URL +} + +func (s *s3MockServer) handler(w http.ResponseWriter, r *http.Request) { + key := path.Base(r.URL.Path) + + switch key { + case s.BucketName: + w.Header().Add("Content-Type", "application/xml") + + if r.Method == http.MethodHead { + return + } + + q := r.URL.Query() + + if q["location"] != nil { + fmt.Fprint(w, ` + +Europe + `) + return + } + + contents := "" + for _, o := range s.Objects { + etag := md5.Sum([]byte(o.Content)) + contents += fmt.Sprintf(` + + %s + %s + %d + "%b" + STANDARD + `, o.Key, o.LastModified.UTC().Format(time.RFC3339), len(o.Content), etag) + } + + fmt.Fprintf(w, ` + + + %s + + + %d + 1000 + false + %s + + `, s.BucketName, len(s.Objects), contents) + default: + var found *s3MockObject + for _, o := range s.Objects { + if key == o.Key { + found = o + } + } + if found == nil { + w.WriteHeader(404) + return + } + + etag := md5.Sum([]byte(found.Content)) + lastModified := strings.Replace(found.LastModified.UTC().Format(time.RFC1123), "UTC", "GMT", 1) + + w.Header().Add("Content-Type", found.ContentType) + w.Header().Add("Last-Modified", lastModified) + w.Header().Add("ETag", fmt.Sprintf("\"%b\"", etag)) + w.Header().Add("Content-Length", fmt.Sprintf("%d", len(found.Content))) + + if r.Method == http.MethodHead { + return + } + + w.Write(found.Content) + } +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 964c15c4f..eb18dbb00 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,6 +17,7 @@ limitations under the License. package controllers import ( + "context" "io/ioutil" "math/rand" "os" @@ -53,6 +54,8 @@ var examplePublicKey []byte var examplePrivateKey []byte var exampleCA []byte +var ctx context.Context = context.Background() + func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -105,6 +108,14 @@ var _ = BeforeSuite(func(done Done) { }) Expect(err).ToNot(HaveOccurred()) + err = (&BucketReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName(sourcev1.BucketKind), + Scheme: scheme.Scheme, + Storage: storage, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred(), "failed to setup BucketReconciler") + err = (&GitRepositoryReconciler{ Client: k8sManager.GetClient(), Log: ctrl.Log.WithName("controllers").WithName(sourcev1.GitRepositoryKind),