From 4c0f6d69bee8c75c3f82e7bc0e39bee32f85afb3 Mon Sep 17 00:00:00 2001 From: Kevin McDermott Date: Wed, 26 Aug 2020 21:31:38 +0100 Subject: [PATCH] Add support for suspending image scans. Co-authored-by: Michael Bridgen --- api/v1alpha1/imagerepository_types.go | 5 ++ ...e.toolkit.fluxcd.io_imagerepositories.yaml | 5 ++ controllers/imagerepository_controller.go | 16 ++++++ controllers/scan_test.go | 54 +++++++++++++++++-- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/imagerepository_types.go b/api/v1alpha1/imagerepository_types.go index bd12927..c097c1b 100644 --- a/api/v1alpha1/imagerepository_types.go +++ b/api/v1alpha1/imagerepository_types.go @@ -33,6 +33,11 @@ type ImageRepositorySpec struct { // scans of the image repository. // +optional ScanInterval *metav1.Duration `json:"scanInterval,omitempty"` + + // This flag tells the controller to suspend subsequent image scans. + // It does not apply to already started scans. Defaults to false. + // +optional + Suspend bool `json:"suspend,omitempty"` } type ScanResult struct { diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml index 329d6d3..c2bece4 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imagerepositories.yaml @@ -51,6 +51,11 @@ spec: description: ScanInterval is the (minimum) length of time to wait between scans of the image repository. type: string + suspend: + description: This flag tells the controller to suspend + scanning of the image repository. + Defaults to false. + type: boolean type: object status: description: ImageRepositoryStatus defines the observed state of ImageRepository diff --git a/controllers/imagerepository_controller.go b/controllers/imagerepository_controller.go index 05837b1..0bb47b7 100644 --- a/controllers/imagerepository_controller.go +++ b/controllers/imagerepository_controller.go @@ -77,6 +77,22 @@ func (r *ImageRepositoryReconciler) Reconcile(req ctrl.Request) (ctrl.Result, er log := r.Log.WithValues("controller", strings.ToLower(imagev1alpha1.ImageRepositoryKind), "request", req.NamespacedName) + if imageRepo.Spec.Suspend { + msg := "ImageRepository is suspended, skipping reconciliation" + status := imagev1alpha1.SetImageRepositoryReadiness( + imageRepo, + corev1.ConditionFalse, + imagev1alpha1.SuspendedReason, + msg, + ) + if err := r.Status().Update(ctx, &status); err != nil { + log.Error(err, "unable to update status") + return ctrl.Result{Requeue: true}, err + } + log.Info(msg) + return ctrl.Result{}, nil + } + ref, err := name.ParseReference(imageRepo.Spec.Image) if err != nil { status := imagev1alpha1.SetImageRepositoryReadiness( diff --git a/controllers/scan_test.go b/controllers/scan_test.go index 2a7ea99..906c86a 100644 --- a/controllers/scan_test.go +++ b/controllers/scan_test.go @@ -36,17 +36,24 @@ import ( // has an example of loading a test registry with a random image. var _ = Describe("ImageRepository controller", func() { + const imageName = "alpine-image" + var repo imagev1alpha1.ImageRepository + + AfterEach(func() { + Expect(k8sClient.Delete(context.Background(), &repo)).To(Succeed()) + }) + It("expands the canonical image name", func() { // would be good to test this without needing to do the scanning, since // 1. better to not rely on external services being available // 2. probably going to want to have several test cases - repo := imagev1alpha1.ImageRepository{ + repo = imagev1alpha1.ImageRepository{ Spec: imagev1alpha1.ImageRepositorySpec{ Image: "alpine", }, } imageRepoName := types.NamespacedName{ - Name: "alpine-image", + Name: imageName, Namespace: "default", } @@ -65,7 +72,7 @@ var _ = Describe("ImageRepository controller", func() { err := r.Get(context.Background(), imageRepoName, &repoAfter) return err == nil && repoAfter.Status.CanonicalImageName != "" }, timeout, interval).Should(BeTrue()) - Expect(repoAfter.Name).To(Equal("alpine-image")) + Expect(repoAfter.Name).To(Equal(imageName)) Expect(repoAfter.Namespace).To(Equal("default")) Expect(repoAfter.Status.CanonicalImageName).To(Equal("index.docker.io/library/alpine")) }) @@ -74,7 +81,7 @@ var _ = Describe("ImageRepository controller", 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("test-fetch", versions) - repo := imagev1alpha1.ImageRepository{ + repo = imagev1alpha1.ImageRepository{ Spec: imagev1alpha1.ImageRepositorySpec{ Image: imgRepo, }, @@ -101,6 +108,45 @@ var _ = Describe("ImageRepository controller", func() { Expect(repoAfter.Status.CanonicalImageName).To(Equal(imgRepo)) Expect(repoAfter.Status.LastScanResult.TagCount).To(Equal(len(versions))) }) + + Context("when the ImageRepository is suspended", func() { + It("does not process the image", func() { + repo = imagev1alpha1.ImageRepository{ + Spec: imagev1alpha1.ImageRepositorySpec{ + Image: "alpine", + Suspend: true, + }, + } + imageRepoName := types.NamespacedName{ + Name: imageName, + Namespace: "default", + } + + repo.Name = imageRepoName.Name + repo.Namespace = imageRepoName.Namespace + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + r := imageRepoReconciler + + err := r.Create(ctx, &repo) + Expect(err).ToNot(HaveOccurred()) + + var repoAfter imagev1alpha1.ImageRepository + Eventually(func() bool { + err := r.Get(ctx, imageRepoName, &repoAfter) + return err == nil && len(repoAfter.Status.Conditions) > 0 + }, timeout, interval).Should(BeTrue()) + Expect(repoAfter.Status.CanonicalImageName).To(Equal("")) + cond := repoAfter.Status.Conditions[0] + Expect(cond.Message).To( + Equal("ImageRepository is suspended, skipping reconciliation")) + Expect(cond.Reason).To( + Equal(imagev1alpha1.SuspendedReason)) + }) + }) + }) // loadImages uploads images to the local registry, and returns the