diff --git a/controllers/infra/controllers.go b/controllers/infra/controllers.go index b2d471e97..44107ac93 100644 --- a/controllers/infra/controllers.go +++ b/controllers/infra/controllers.go @@ -12,7 +12,6 @@ import ( "github.com/vmware-tanzu/vm-operator/controllers/infra/configmap" "github.com/vmware-tanzu/vm-operator/controllers/infra/node" "github.com/vmware-tanzu/vm-operator/controllers/infra/secret" - "github.com/vmware-tanzu/vm-operator/controllers/infra/validatingwebhookconfiguration" "github.com/vmware-tanzu/vm-operator/controllers/infra/zone" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" @@ -32,11 +31,6 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err if err := secret.AddToManager(ctx, mgr); err != nil { return fmt.Errorf("failed to initialize infra secret controller: %w", err) } - if pkgcfg.FromContext(ctx).Features.UnifiedStorageQuota { - if err := validatingwebhookconfiguration.AddToManager(ctx, mgr); err != nil { - return fmt.Errorf("failed to initialize validatingwebhookconfiguration webhook controller: %w", err) - } - } if pkgcfg.FromContext(ctx).Features.WorkloadDomainIsolation { if err := zone.AddToManager(ctx, mgr); err != nil { return fmt.Errorf("failed to initialize infra zone controller: %w", err) diff --git a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller.go b/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller.go deleted file mode 100644 index 7e496d420..000000000 --- a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2024 VMware, Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package validatingwebhookconfiguration - -import ( - "bytes" - "context" - "fmt" - "reflect" - "strings" - - "github.com/go-logr/logr" - admissionv1 "k8s.io/api/admissionregistration/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - - spqv1 "github.com/vmware-tanzu/vm-operator/external/storage-policy-quota/api/v1alpha2" - pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - pkgctx "github.com/vmware-tanzu/vm-operator/pkg/context" - pkgmgr "github.com/vmware-tanzu/vm-operator/pkg/manager" - "github.com/vmware-tanzu/vm-operator/pkg/record" - kubeutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube" - spqutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube/spq" -) - -// AddToManager adds this package's controller to the provided manager. -func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) error { - var ( - controlledType = &admissionv1.ValidatingWebhookConfiguration{} - controlledTypeName = reflect.TypeOf(controlledType).Elem().Name() - - controllerNameShort = fmt.Sprintf("%s-controller", strings.ToLower(controlledTypeName)) - controllerNameLong = fmt.Sprintf("%s/%s/%s", ctx.Namespace, ctx.Name, controllerNameShort) - ) - - r := NewReconciler( - ctx, - mgr.GetClient(), - ctrl.Log.WithName("controllers").WithName(controlledTypeName), - record.New(mgr.GetEventRecorderFor(controllerNameLong)), - ) - - c, err := controller.New(controllerNameShort, mgr, controller.Options{Reconciler: r}) - if err != nil { - return err - } - - cache, err := pkgmgr.NewNamespacedCacheForObject(mgr, &ctx.SyncPeriod, controlledType) - if err != nil { - return err - } - - return c.Watch(source.Kind(cache, controlledType, &handler.TypedEnqueueRequestForObject[*admissionv1.ValidatingWebhookConfiguration]{}, - predicate.TypedFuncs[*admissionv1.ValidatingWebhookConfiguration]{ - CreateFunc: func(e event.TypedCreateEvent[*admissionv1.ValidatingWebhookConfiguration]) bool { - return e.Object.GetName() == spqutil.ValidatingWebhookConfigName - }, - UpdateFunc: func(e event.TypedUpdateEvent[*admissionv1.ValidatingWebhookConfiguration]) bool { - return e.ObjectOld.GetName() == spqutil.ValidatingWebhookConfigName - }, - DeleteFunc: func(e event.TypedDeleteEvent[*admissionv1.ValidatingWebhookConfiguration]) bool { - return false - }, - GenericFunc: func(e event.TypedGenericEvent[*admissionv1.ValidatingWebhookConfiguration]) bool { - return false - }, - }, - kubeutil.TypedResourceVersionChangedPredicate[*admissionv1.ValidatingWebhookConfiguration]{}, - )) -} - -func NewReconciler(ctx context.Context, client client.Client, logger logr.Logger, recorder record.Recorder) *Reconciler { - - return &Reconciler{ - Context: ctx, - Client: client, - Logger: logger, - Recorder: recorder, - } -} - -// Reconciler reconciles a ValidatingWebhookConfiguration object. -type Reconciler struct { - client.Client - Context context.Context - Logger logr.Logger - Recorder record.Recorder -} - -// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;list;watch - -func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { - ctx = pkgcfg.JoinContext(ctx, r.Context) - - if req.Name != spqutil.ValidatingWebhookConfigName { - r.Logger.Error(nil, "Reconcile called for unexpected object", "name", req.Name) - return ctrl.Result{}, nil - } - return ctrl.Result{}, r.ReconcileNormal(ctx, req) -} - -func (r *Reconciler) ReconcileNormal(ctx context.Context, req ctrl.Request) error { - r.Logger.Info("Reconciling validating webhook configuration", "name", req.Name) - - caBundle, err := spqutil.GetWebhookCABundle(ctx, r.Client) - if err != nil { - return err - } - - spuList := &spqv1.StoragePolicyUsageList{} - if err := r.Client.List(ctx, spuList); err != nil { - return fmt.Errorf("unable to list StoragePolicyUsage objects: %w", err) - } - - for _, spu := range spuList.Items { - if spu.Spec.ResourceExtensionName == spqutil.StoragePolicyQuotaExtensionName { - if !bytes.Equal(spu.Spec.CABundle, caBundle) { - spuPatch := client.MergeFrom(spu.DeepCopy()) - spu.Spec.CABundle = caBundle - - if err := r.Client.Patch(ctx, &spu, spuPatch); err != nil { - return fmt.Errorf("unable to patch StoragePolicyUsage object: %w", err) - } - } - } - } - return nil -} diff --git a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_intg_test.go b/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_intg_test.go deleted file mode 100644 index 8988bc4c0..000000000 --- a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_intg_test.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2024 VMware, Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package validatingwebhookconfiguration_test - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - admissionv1 "k8s.io/api/admissionregistration/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - spqv1 "github.com/vmware-tanzu/vm-operator/external/storage-policy-quota/api/v1alpha2" - "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" - spqutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube/spq" - "github.com/vmware-tanzu/vm-operator/pkg/util/ptr" - "github.com/vmware-tanzu/vm-operator/test/builder" -) - -func intgTests() { - Describe( - "Reconcile", - Label( - testlabels.Controller, - testlabels.EnvTest, - ), - intgTestsReconcile, - ) -} - -func intgTestsReconcile() { - - const ( - secretName = "vmware-system-vmop-serving-cert" - vmSPUName = "vm-spu" - pvcSPUName = "pvc-spu" - ) - - var ( - ctx *builder.IntegrationTestContext - caBundle []byte - validatingWebhookConfiguration *admissionv1.ValidatingWebhookConfiguration - ) - - BeforeEach(func() { - ctx = suite.NewIntegrationTestContext() - caBundle = []byte("fake-ca-bundle") - - validatingWebhookConfiguration = &admissionv1.ValidatingWebhookConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: spqutil.ValidatingWebhookConfigName, - Annotations: map[string]string{ - "cert-manager.io/inject-ca-from": fmt.Sprintf("%s/vmware-system-vmop-serving-cert", ctx.Namespace), - }, - }, - Webhooks: []admissionv1.ValidatingWebhook{ - { - AdmissionReviewVersions: []string{"v1beta1", "v1"}, - ClientConfig: admissionv1.WebhookClientConfig{ - Service: &admissionv1.ServiceReference{ - Name: "vmware-system-vmop-webhook-service", - Namespace: "vmware-system-vmop", - Path: ptr.To("/default-validate-vmoperator-vmware-com-v1alpha3-virtualmachine"), - }, - CABundle: caBundle, - }, - FailurePolicy: ptr.To(admissionv1.Fail), - Name: "default.validating.virtualmachine.v1alpha3.vmoperator.vmware.com", - SideEffects: ptr.To(admissionv1.SideEffectClassNone), - }, - }, - } - }) - - AfterEach(func() { - Expect(ctx.Client.Delete(ctx, validatingWebhookConfiguration)).To(Succeed()) - - caBundle = nil - validatingWebhookConfiguration = nil - - ctx.AfterEach() - }) - - JustBeforeEach(func() { - // Different resourceExtensionName - pvcSPU := &spqv1.StoragePolicyUsage{ - ObjectMeta: metav1.ObjectMeta{ - Name: pvcSPUName, - Namespace: ctx.Namespace, - }, - Spec: spqv1.StoragePolicyUsageSpec{ - StorageClassName: builder.DummyStorageClassName, - StoragePolicyId: "dummy-storage-policy-id", - ResourceExtensionName: "fake.cns.vsphere.vmware.com", - CABundle: []byte("invalid-resource-ca-bundle"), - }, - } - Expect(ctx.Client.Create(ctx, pvcSPU)).To(Succeed()) - - // Same resourceExtensionName - vmSPU := &spqv1.StoragePolicyUsage{ - ObjectMeta: metav1.ObjectMeta{ - Name: vmSPUName, - Namespace: ctx.Namespace, - }, - Spec: spqv1.StoragePolicyUsageSpec{ - StorageClassName: builder.DummyStorageClassName, - StoragePolicyId: "dummy-storage-policy-id", - ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, - CABundle: []byte("initial-ca-bundle"), - }, - } - Expect(ctx.Client.Create(ctx, vmSPU)).To(Succeed()) - }) - - Context("CABundle from ValidatingWebhookConfig", func() { - - When("ValidatingWebhookConfiguration is created", func() { - - It("should update the correct resources and leave others untouched", func() { - // Create ValidatingWebhookConfiguration - Expect(ctx.Client.Create(ctx, validatingWebhookConfiguration)).To(Succeed()) - // SPU for vm should be updated with new CABundle, while that for pvc should be left alone - Eventually(func(g Gomega) { - pvcSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: pvcSPUName, Namespace: ctx.Namespace}, pvcSPU)).To(Succeed()) - - g.Expect(pvcSPU.Spec.CABundle).NotTo(Equal(validatingWebhookConfiguration.Webhooks[0].ClientConfig.CABundle)) - g.Expect(pvcSPU.Spec.CABundle).To(Equal([]byte("invalid-resource-ca-bundle"))) - - vmSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: vmSPUName, Namespace: ctx.Namespace}, vmSPU)).To(Succeed()) - - g.Expect(vmSPU.Spec.CABundle).NotTo(Equal([]byte("initial-ca-bundle"))) - g.Expect(vmSPU.Spec.CABundle).To(Equal(caBundle)) - }).Should(Succeed()) - }) - }) - - When("ValidatingWebhookConfiguration is updated", FlakeAttempts(5), func() { - - BeforeEach(func() { - // Create ValidatingWebhookConfiguration - Expect(ctx.Client.Create(ctx, validatingWebhookConfiguration)).To(Succeed()) - }) - - It("should update the correct resources and leave others untouched", func() { - // Update ValidatingWebhookConfiguration with new CABundle - webhookConfiguration := &admissionv1.ValidatingWebhookConfiguration{} - Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: spqutil.ValidatingWebhookConfigName}, webhookConfiguration)).To(Succeed()) - - webhookConfiguration.Webhooks[0].ClientConfig.CABundle = []byte("updated-ca-bundle") - Expect(ctx.Client.Update(ctx, webhookConfiguration)).To(Succeed()) - - // SPU for vm should be updated with new CABundle, while that for pvc should be left alone - Eventually(func(g Gomega) { - pvcSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: pvcSPUName, Namespace: ctx.Namespace}, pvcSPU)).To(Succeed()) - - g.Expect(pvcSPU.Spec.CABundle).NotTo(Equal(webhookConfiguration.Webhooks[0].ClientConfig.CABundle)) - g.Expect(pvcSPU.Spec.CABundle).To(Equal([]byte("invalid-resource-ca-bundle"))) - - vmSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: vmSPUName, Namespace: ctx.Namespace}, vmSPU)).To(Succeed()) - - g.Expect(vmSPU.Spec.CABundle).NotTo(Equal(caBundle)) - g.Expect(vmSPU.Spec.CABundle).To(Equal(webhookConfiguration.Webhooks[0].ClientConfig.CABundle)) - }).Should(Succeed()) - }) - }) - }) - - Context("CABundle from Secret", func() { - var certSecret *corev1.Secret - - BeforeEach(func() { - validatingWebhookConfiguration.Webhooks = []admissionv1.ValidatingWebhook{} - - certSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: ctx.Namespace, - }, - Data: map[string][]byte{ - "ca.crt": caBundle, - }, - } - - Expect(ctx.Client.Create(ctx, certSecret)).To(Succeed()) - }) - - When("ValidatingWebhookConfiguration is created", func() { - - It("should update the correct resources and leave others untouched", func() { - // Create ValidatingWebhookConfiguration - Expect(ctx.Client.Create(ctx, validatingWebhookConfiguration)).To(Succeed()) - // SPU for vm should be updated with new CABundle, while that for pvc should be left alone - Eventually(func(g Gomega) { - pvcSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: pvcSPUName, Namespace: ctx.Namespace}, pvcSPU)).To(Succeed()) - - g.Expect(pvcSPU.Spec.CABundle).NotTo(Equal(certSecret.Data["ca.crt"])) - g.Expect(pvcSPU.Spec.CABundle).To(Equal([]byte("invalid-resource-ca-bundle"))) - - vmSPU := &spqv1.StoragePolicyUsage{} - g.Expect(ctx.Client.Get(ctx, client.ObjectKey{Name: vmSPUName, Namespace: ctx.Namespace}, vmSPU)).To(Succeed()) - - g.Expect(vmSPU.Spec.CABundle).NotTo(Equal([]byte("initial-ca-bundle"))) - g.Expect(vmSPU.Spec.CABundle).To(Equal(caBundle)) - }).Should(Succeed()) - }) - }) - }) -} diff --git a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_suite_test.go b/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_suite_test.go deleted file mode 100644 index c01a75a3a..000000000 --- a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_suite_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2024 VMware, Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package validatingwebhookconfiguration_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - - "github.com/vmware-tanzu/vm-operator/controllers/infra/validatingwebhookconfiguration" - pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" - "github.com/vmware-tanzu/vm-operator/pkg/manager" - "github.com/vmware-tanzu/vm-operator/pkg/util/kube/cource" - "github.com/vmware-tanzu/vm-operator/test/builder" -) - -var suite = builder.NewTestSuiteForControllerWithContext( - cource.WithContext( - pkgcfg.UpdateContext( - pkgcfg.NewContext(), - func(config *pkgcfg.Config) { - config.Features.PodVMOnStretchedSupervisor = true - }, - ), - ), - validatingwebhookconfiguration.AddToManager, - manager.InitializeProvidersNoopFn) - -func TestWebhookConfiguration(t *testing.T) { - suite.Register(t, "ValidatingWebhookConfiguration controller suite", intgTests, unitTests) -} - -var _ = BeforeSuite(suite.BeforeSuite) - -var _ = AfterSuite(suite.AfterSuite) diff --git a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_unit_test.go b/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_unit_test.go deleted file mode 100644 index 6dcc4349d..000000000 --- a/controllers/infra/validatingwebhookconfiguration/validatingwebhookconfiguration_controller_unit_test.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2024 VMware, Inc. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package validatingwebhookconfiguration_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - admissionv1 "k8s.io/api/admissionregistration/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/vmware-tanzu/vm-operator/controllers/infra/validatingwebhookconfiguration" - spqv1 "github.com/vmware-tanzu/vm-operator/external/storage-policy-quota/api/v1alpha2" - "github.com/vmware-tanzu/vm-operator/pkg/constants/testlabels" - spqutil "github.com/vmware-tanzu/vm-operator/pkg/util/kube/spq" - "github.com/vmware-tanzu/vm-operator/test/builder" -) - -func unitTests() { - Describe( - "Reconcile", - Label( - testlabels.Controller, - ), - unitTestsReconcile, - ) -} - -func unitTestsReconcile() { - const ( - secretName = "vmware-system-vmop-serving-cert" - secretNamespace = "vmware-system-vmop" - ) - var ( - ctx *builder.UnitTestContextForController - withObjects []client.Object - - reconciler *validatingwebhookconfiguration.Reconciler - - certSecret *corev1.Secret - validatingWebhookConfiguration *admissionv1.ValidatingWebhookConfiguration - spu *spqv1.StoragePolicyUsage - ) - - BeforeEach(func() { - withObjects = []client.Object{} - - certSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: secretNamespace, - }, - Data: map[string][]byte{ - "ca.crt": []byte("fake-ca-bundle"), - }, - } - - spu = &spqv1.StoragePolicyUsage{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake-spu-name", - Namespace: "fake-spu-namespace", - }, - Spec: spqv1.StoragePolicyUsageSpec{ - ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, - }, - } - - validatingWebhookConfiguration = &admissionv1.ValidatingWebhookConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: spqutil.ValidatingWebhookConfigName, - Annotations: map[string]string{ - "cert-manager.io/inject-ca-from": "vmware-system-vmop/vmware-system-vmop-serving-cert", - }, - }, - Webhooks: []admissionv1.ValidatingWebhook{}, - } - }) - - JustBeforeEach(func() { - withObjects = append(withObjects, certSecret, validatingWebhookConfiguration, spu) - ctx = suite.NewUnitTestContextForController(withObjects...) - reconciler = validatingwebhookconfiguration.NewReconciler(ctx, ctx.Client, ctx.Logger, ctx.Recorder) - }) - - Context("Reconcile", func() { - var ( - err error - name string - ) - - BeforeEach(func() { - err = nil - name = spqutil.ValidatingWebhookConfigName - }) - - JustBeforeEach(func() { - _, err = reconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: name, - }}) - }) - - When("Reconcile is called with invalid name", func() { - BeforeEach(func() { - name = "fakeName" - }) - - It("should return nil error", func() { - Expect(err).NotTo(HaveOccurred()) - }) - }) - - When("ValidatingWebhookConfiguration is not found", func() { - errMsg := `validatingwebhookconfigurations.admissionregistration.k8s.io "vmware-system-vmop-validating-webhook-configuration" not found` - - BeforeEach(func() { - validatingWebhookConfiguration = &admissionv1.ValidatingWebhookConfiguration{} - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errMsg)) - }) - }) - - When("certificate annotation is not present", func() { - BeforeEach(func() { - delete(validatingWebhookConfiguration.Annotations, "cert-manager.io/inject-ca-from") - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to get annotation for key")) - }) - }) - - When("certificate annotation is present with empty value", func() { - BeforeEach(func() { - validatingWebhookConfiguration.Annotations["cert-manager.io/inject-ca-from"] = "" - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to get annotation for key")) - }) - }) - - When("certificate annotation is present and missing namespace", func() { - BeforeEach(func() { - validatingWebhookConfiguration.Annotations["cert-manager.io/inject-ca-from"] = "vmware-system-vmop-serving-cert" - }) - - It("should not return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to get namespace and name for key")) - }) - }) - - When("certificate Secret is not found", func() { - BeforeEach(func() { - certSecret.Name = "fake" - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(`secrets "vmware-system-vmop-serving-cert" not found`)) - }) - }) - - When("secret is missing ca.crt", func() { - BeforeEach(func() { - delete(certSecret.Data, "ca.crt") - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to get CA bundle from secret")) - }) - }) - - When("ca.crt is empty", func() { - BeforeEach(func() { - certSecret.Data["ca.crt"] = []byte("") - }) - - It("should return an error", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to get CA bundle from secret")) - }) - }) - - Context("update SPU docs", func() { - JustBeforeEach(func() { - Expect(err).To(BeNil()) - err = ctx.Client.Get(ctx, client.ObjectKeyFromObject(spu), spu) - - Expect(err).To(BeNil()) - }) - - When("no SPU docs exist for vmservice", func() { - BeforeEach(func() { - spu.Spec.ResourceExtensionName = "fake.cns.vsphere.vmware.com" - spu.Spec.CABundle = []byte("fake.cns.vsphere.vmware.com") - }) - - It("should not update caBundle", func() { - Expect(spu.Spec.CABundle).To(Equal([]byte("fake.cns.vsphere.vmware.com"))) - }) - }) - - When("SPU caBundle matches contents of secret", func() { - BeforeEach(func() { - spu.Spec.CABundle = certSecret.Data["ca.crt"] - }) - - It("should not update caBundle", func() { - Expect(spu.Spec.CABundle).To(Equal(certSecret.Data["ca.crt"])) - }) - }) - - When("SPU caBundle does not match contents of secret", func() { - BeforeEach(func() { - spu.Spec.CABundle = []byte("outdated-fake-ca-bundle") - }) - - It("should update caBundle", func() { - Expect(spu.Spec.CABundle).NotTo(Equal([]byte("outdated-fake-ca-bundle"))) - Expect(spu.Spec.CABundle).To(Equal(certSecret.Data["ca.crt"])) - }) - }) - - When("webhooks slice is non-empty", func() { - BeforeEach(func() { - validatingWebhookConfiguration.Webhooks = []admissionv1.ValidatingWebhook{ - { - ClientConfig: admissionv1.WebhookClientConfig{ - CABundle: []byte("fake-ca-bundle"), - }, - }, - } - spu.Spec.CABundle = []byte("outdated-fake-ca-bundle") - }) - - It("should get caBundle from first non-empty entry in slice and update caBundle", func() { - Expect(spu.Spec.CABundle).NotTo(Equal([]byte("outdated-fake-ca-bundle"))) - Expect(spu.Spec.CABundle).To(Equal(certSecret.Data["ca.crt"])) - }) - }) - }) - }) -} diff --git a/controllers/storagepolicyquota/storagepolicyquota_controller.go b/controllers/storagepolicyquota/storagepolicyquota_controller.go index e528d9757..c3d5f0666 100644 --- a/controllers/storagepolicyquota/storagepolicyquota_controller.go +++ b/controllers/storagepolicyquota/storagepolicyquota_controller.go @@ -10,10 +10,12 @@ import ( "strings" "github.com/go-logr/logr" + admissionv1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha3" @@ -44,9 +46,21 @@ func AddToManager(ctx *pkgctx.ControllerManagerContext, mgr manager.Manager) err ctx.Namespace, ) - return ctrl.NewControllerManagedBy(mgr). - For(controlledType). - Complete(r) + builder := ctrl.NewControllerManagedBy(mgr). + For(controlledType) + + // Add a watch on ValidatingWebhookConfiguration so that any updates + // to webhook CA Bundle will trigger reconciliation and update the + // appropriate StoragePolicyUsage(s) + if pkgcfg.FromContext(ctx).Features.UnifiedStorageQuota { + builder = builder.Watches( + &admissionv1.ValidatingWebhookConfiguration{}, + handler.EnqueueRequestsFromMapFunc( + spqutil.ValidatingWebhookConfigurationToStoragePolicyQuotaMapper(ctx, r.Client), + )) + } + + return builder.Complete(r) } func NewReconciler( @@ -85,6 +99,7 @@ type Reconciler struct { // +kubebuilder:rbac:groups=cns.vmware.com,resources=storagepolicyquotas/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cns.vmware.com,resources=storagepolicyusages,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=cns.vmware.com,resources=storagepolicyusages/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;list;watch func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { ctx = pkgcfg.JoinContext(ctx, r.Context) diff --git a/controllers/storagepolicyquota/storagepolicyquota_controller_intg_test.go b/controllers/storagepolicyquota/storagepolicyquota_controller_intg_test.go index 4ad4a43c8..818d1d488 100644 --- a/controllers/storagepolicyquota/storagepolicyquota_controller_intg_test.go +++ b/controllers/storagepolicyquota/storagepolicyquota_controller_intg_test.go @@ -38,6 +38,9 @@ func intgTestsReconcile() { storageQuotaName = "my-storage-quota" storageClassName = "my-storage-class" storagePolicyID = "my-storage-policy" + + caBundle = "fake-ca-bundle" + caBundleUpdated = "updated-ca-bundle" ) var ( @@ -54,8 +57,13 @@ func intgTestsReconcile() { }) Context("Reconcile", func() { + var ( + validatingWebhookConfiguration *admissionv1.ValidatingWebhookConfiguration + storageClass *storagev1.StorageClass + ) + BeforeEach(func() { - validatingWebhookConfiguration := &admissionv1.ValidatingWebhookConfiguration{ + validatingWebhookConfiguration = &admissionv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: spqutil.ValidatingWebhookConfigName, }, @@ -68,7 +76,7 @@ func intgTestsReconcile() { Namespace: "vmware-system-vmop", Path: ptr.To("/default-validate-vmoperator-vmware-com-v1alpha3-virtualmachine"), }, - CABundle: []byte("fake-ca-bundle"), + CABundle: []byte(caBundle), }, FailurePolicy: ptr.To(admissionv1.Fail), Name: "default.validating.virtualmachine.v1alpha3.vmoperator.vmware.com", @@ -78,6 +86,17 @@ func intgTestsReconcile() { } Expect(ctx.Client.Create(ctx, validatingWebhookConfiguration)).To(Succeed()) + storageClass = &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: storageClassName, + }, + Provisioner: "fake", + Parameters: map[string]string{ + "storagePolicyID": storagePolicyID, + }, + } + Expect(ctx.Client.Create(ctx, storageClass)).To(Succeed()) + obj := spqv1.StoragePolicyQuota{ ObjectMeta: metav1.ObjectMeta{ Name: storageQuotaName, @@ -98,17 +117,11 @@ func intgTestsReconcile() { } storageQuotaUID = obj.UID Expect(ctx.Client.Status().Update(ctx, &obj)).To(Succeed()) - Expect(ctx.Client.Create( - ctx, - &storagev1.StorageClass{ - ObjectMeta: metav1.ObjectMeta{ - Name: storageClassName, - }, - Provisioner: "fake", - Parameters: map[string]string{ - "storagePolicyID": storagePolicyID, - }, - })).To(Succeed()) + }) + + AfterEach(func() { + Expect(ctx.Client.Delete(ctx, validatingWebhookConfiguration)).To(Succeed()) + Expect(ctx.Client.Delete(ctx, storageClass)).To(Succeed()) }) It("should result in the creation of a StoragePolicyUsage resource", func() { @@ -136,7 +149,41 @@ func intgTestsReconcile() { g.Expect(obj.Spec.ResourceKind).To(Equal("VirtualMachine")) g.Expect(obj.Spec.ResourceExtensionName).To(Equal(spqutil.StoragePolicyQuotaExtensionName)) g.Expect(obj.Spec.ResourceExtensionNamespace).To(Equal(ctx.PodNamespace)) - g.Expect(obj.Spec.CABundle).To(Equal([]byte("fake-ca-bundle"))) + g.Expect(obj.Spec.CABundle).To(Equal([]byte(caBundle))) + }).Should(Succeed()) + }) + + It("reconciles a change to ValidatingWebhookConfiguration", func() { + Eventually(func(g Gomega) { + var spu spqv1.StoragePolicyUsage + dstKey := client.ObjectKey{ + Namespace: ctx.Namespace, + Name: spqutil.StoragePolicyUsageName(storageClassName), + } + g.Expect(ctx.Client.Get(ctx, dstKey, &spu)).To(Succeed()) + + g.Expect(spu.Spec.CABundle).To(Equal([]byte(caBundle))) + }).Should(Succeed()) + + var obj admissionv1.ValidatingWebhookConfiguration + objKey := client.ObjectKey{ + Name: spqutil.ValidatingWebhookConfigName, + } + Expect(ctx.Client.Get(ctx, objKey, &obj)).To(Succeed()) + + obj.Webhooks[0].ClientConfig.CABundle = []byte(caBundleUpdated) + + Expect(ctx.Client.Update(ctx, &obj)).To(Succeed()) + + Eventually(func(g Gomega) { + var spu spqv1.StoragePolicyUsage + dstKey := client.ObjectKey{ + Namespace: ctx.Namespace, + Name: spqutil.StoragePolicyUsageName(storageClassName), + } + g.Expect(ctx.Client.Get(ctx, dstKey, &spu)).To(Succeed()) + + g.Expect(spu.Spec.CABundle).To(Equal([]byte(caBundleUpdated))) }).Should(Succeed()) }) }) diff --git a/controllers/storagepolicyquota/storagepolicyquota_controller_suite_test.go b/controllers/storagepolicyquota/storagepolicyquota_controller_suite_test.go index f717dc8fb..191c29997 100644 --- a/controllers/storagepolicyquota/storagepolicyquota_controller_suite_test.go +++ b/controllers/storagepolicyquota/storagepolicyquota_controller_suite_test.go @@ -21,6 +21,7 @@ var suite = builder.NewTestSuiteForControllerWithContext( pkgcfg.NewContext(), func(config *pkgcfg.Config) { config.Features.PodVMOnStretchedSupervisor = true + config.Features.UnifiedStorageQuota = true }, ), ), diff --git a/pkg/util/kube/spq/spq.go b/pkg/util/kube/spq/spq.go index ec69555fa..197563d6b 100644 --- a/pkg/util/kube/spq/spq.go +++ b/pkg/util/kube/spq/spq.go @@ -4,14 +4,20 @@ package spq import ( + "bytes" "context" "fmt" "strings" + "github.com/go-logr/logr" admissionv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" spqv1 "github.com/vmware-tanzu/vm-operator/external/storage-policy-quota/api/v1alpha2" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" @@ -232,3 +238,88 @@ func GetWebhookCABundle(ctx context.Context, k8sClient client.Client) ([]byte, e return caBundle, nil } + +func ValidatingWebhookConfigurationToStoragePolicyQuotaMapper( + ctx context.Context, + k8sClient client.Client) handler.MapFunc { + + if ctx == nil { + panic("context is nil") + } + if k8sClient == nil { + panic("client is nil") + } + + return func(ctx context.Context, o client.Object) []reconcile.Request { + if ctx == nil { + panic("context is nil") + } + if o == nil { + panic("object is nil") + } + + obj, ok := o.(*admissionv1.ValidatingWebhookConfiguration) + if !ok { + panic(fmt.Sprintf("object is %T", o)) + } + + // If this is not the ValidatingWebhookConfiguration that we care about, + // then just return + if obj.Name != ValidatingWebhookConfigName { + return nil + } + + logger := logr.FromContextOrDiscard(ctx).WithValues("name", o.GetName()) + logger.V(4).Info("Reconciling all VirtualMachine StoragePolicyUsages with updated CA Bundle") + + caBundle, err := GetWebhookCABundle(ctx, k8sClient) + if err != nil { + logger.Error(err, "Failed to get CA bundle from ValidatingWebhookConfiguration") + return nil + } + + spuList := &spqv1.StoragePolicyUsageList{} + if err = k8sClient.List(ctx, spuList); err != nil { + if !apierrors.IsNotFound(err) { + logger.Error(err, "Failed to list StoragePolicyUsages for reconciliation due to ValidatingWebhookConfiguration watch") + } + return nil + } + // Populate reconcile requests for StoragePolicyQuotas whose StoragePolicyUsage + // CA Bundle differs from that of the ValidatingWebhookConfiguration + var requests []reconcile.Request + for _, spu := range spuList.Items { + // We only care about a VM's StoragePolicyUsage with an outdated CA Bundle + if spu.Spec.ResourceExtensionName == StoragePolicyQuotaExtensionName && !bytes.Equal(caBundle, spu.Spec.CABundle) { + // Get the owning object for this StoragePolicyUsage as we want to + // reconcile the StoragePolicyQuota in order to update the StoragePolicyUsage + // CA Bundle + var owningObj *metav1.OwnerReference + for i := range spu.OwnerReferences { + owner := &spu.OwnerReferences[i] + if owner.Kind == StoragePolicyQuotaKind { + owningObj = owner + } + } + + if owningObj == nil { + logger.Error(err, "Failed to get the OwnerReference for StoragePolicyUsage %v", client.ObjectKeyFromObject(&spu)) + continue + } + + requests = append(requests, reconcile.Request{ + NamespacedName: client.ObjectKey{ + Name: owningObj.Name, + Namespace: spu.Namespace, + }, + }) + } + } + + if len(requests) > 0 { + logger.V(4).Info("Reconciling VirtualMachine StoragePolicyUsages due to ValidatingWebhookConfiguration watch") + } + + return requests + } +} diff --git a/pkg/util/kube/spq/spq_test.go b/pkg/util/kube/spq/spq_test.go index 51ac288c1..2128204c9 100644 --- a/pkg/util/kube/spq/spq_test.go +++ b/pkg/util/kube/spq/spq_test.go @@ -17,8 +17,11 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" spqv1 "github.com/vmware-tanzu/vm-operator/external/storage-policy-quota/api/v1alpha2" pkgcfg "github.com/vmware-tanzu/vm-operator/pkg/config" @@ -724,3 +727,372 @@ var _ = Describe("GetWebhookCABundle", func() { }) }) }) + +var _ = Describe("ValidatingWebhookConfigurationToStoragePolicyQuotaMapper", func() { + const ( + caBundle = "fake-ca-bundle" + fakeName = "fake-name" + namespace = "default" + ) + var ( + ctx context.Context + k8sClient ctrlclient.Client + withFuncs interceptor.Funcs + withObjs []ctrlclient.Object + obj ctrlclient.Object + mapFn handler.MapFunc + mapFnCtx context.Context + mapFnObj ctrlclient.Object + reqs []reconcile.Request + ) + + BeforeEach(func() { + reqs = nil + withObjs = nil + withFuncs = interceptor.Funcs{} + + ctx = context.Background() + mapFnCtx = ctx + + obj = &admissionv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: spqutil.ValidatingWebhookConfigName, + }, + Webhooks: []admissionv1.ValidatingWebhook{ + { + ClientConfig: admissionv1.WebhookClientConfig{ + CABundle: []byte(caBundle), + }, + }, + }, + } + mapFnObj = obj + }) + + JustBeforeEach(func() { + withObjs = append(withObjs, obj) + k8sClient = builder.NewFakeClientWithInterceptors(withFuncs, withObjs...) + Expect(k8sClient).ToNot(BeNil()) + }) + + Context("expected panic", func() { + When("context is nil", func() { + JustBeforeEach(func() { + ctx = nil + }) + + It("should panic", func() { + Expect(func() { + _ = spqutil.ValidatingWebhookConfigurationToStoragePolicyQuotaMapper(ctx, k8sClient) + }).To(PanicWith("context is nil")) + }) + }) + + When("client is nil", func() { + JustBeforeEach(func() { + k8sClient = nil + }) + It("should panic", func() { + Expect(func() { + _ = spqutil.ValidatingWebhookConfigurationToStoragePolicyQuotaMapper(ctx, k8sClient) + }).To(PanicWith("client is nil")) + }) + }) + + Context("in returned MapFunc", func() { + JustBeforeEach(func() { + mapFn = spqutil.ValidatingWebhookConfigurationToStoragePolicyQuotaMapper(ctx, k8sClient) + Expect(mapFn).NotTo(BeNil()) + }) + + When("context is nil", func() { + BeforeEach(func() { + mapFnCtx = nil + }) + + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith("context is nil")) + }) + }) + + When("object is nil", func() { + JustBeforeEach(func() { + mapFnObj = nil + }) + + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith("object is nil")) + }) + }) + + When("object is not valid type", func() { + JustBeforeEach(func() { + mapFnObj = &admissionv1.MutatingWebhookConfiguration{} + }) + + It("should panic", func() { + Expect(func() { + _ = mapFn(mapFnCtx, mapFnObj) + }).To(PanicWith(fmt.Sprintf("object is %T", mapFnObj))) + }) + }) + }) + }) + + Context("no panic is expected", func() { + JustBeforeEach(func() { + mapFn = spqutil.ValidatingWebhookConfigurationToStoragePolicyQuotaMapper(ctx, k8sClient) + Expect(mapFn).NotTo(BeNil()) + + reqs = mapFn(mapFnCtx, mapFnObj) + }) + + When("ValidatingWebhookConfiguration name does not match", func() { + BeforeEach(func() { + obj.SetName(fakeName) + }) + + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + + When("there is an error getting CA Bundle from ValidatingWebhookConfiguration", func() { + BeforeEach(func() { + obj = &admissionv1.ValidatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: spqutil.ValidatingWebhookConfigName, + }, + Webhooks: []admissionv1.ValidatingWebhook{}, + } + }) + + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + + When("there is an error listing StoragePolicyUsages", func() { + BeforeEach(func() { + withFuncs.List = func( + ctx context.Context, + client ctrlclient.WithWatch, + list ctrlclient.ObjectList, + opts ...ctrlclient.ListOption) error { + + if _, ok := list.(*spqv1.StoragePolicyUsageList); ok { + return errors.New("fake error") + } + + return client.List(ctx, list, opts...) + } + }) + + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + + Context("", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-x"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-x", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-123", + StorageClassName: "storage-class-x", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte(caBundle), + }, + }, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-y"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-y", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-456", + StorageClassName: "storage-class-y", + ResourceExtensionName: "fake.cns.vsphere.vmware.com", + CABundle: []byte("fake"), + }, + }, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-z"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-z", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-789", + StorageClassName: "storage-class-z", + ResourceExtensionName: "fake.cns.vsphere.vmware.com", + CABundle: []byte("fake"), + }, + }, + ) + }) + + When("there are no matching StoragePolicyUsages", func() { + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + + When("OwnerReference is not set", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-a"), + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-1", + StorageClassName: "storage-class-a", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte("outdated"), + }, + }, + ) + }) + + Specify("no reconcile requests should be returned", func() { + Expect(reqs).To(BeEmpty()) + }) + }) + }) + + When("there is a single matching StoragePolicyUsage", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-a"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-a", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-1", + StorageClassName: "storage-class-a", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte("outdated"), + }, + }, + ) + }) + + Specify("one reconcile request should be returned", func() { + Expect(reqs).To(ConsistOf( + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: "storage-class-a", + }, + }, + )) + }) + }) + + When("there are multiple matching StoragePolicyUsages", func() { + BeforeEach(func() { + withObjs = append(withObjs, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-a"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-a", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-1", + StorageClassName: "storage-class-a", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte("outdated"), + }, + }, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-c"), + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-3", + StorageClassName: "storage-class-c", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte("outdated"), + }, + }, + &spqv1.StoragePolicyUsage{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: spqutil.StoragePolicyUsageName("storage-class-b"), + OwnerReferences: []metav1.OwnerReference{ + { + Name: "storage-class-b", + Kind: spqutil.StoragePolicyQuotaKind, + }, + }, + }, + Spec: spqv1.StoragePolicyUsageSpec{ + StoragePolicyId: "fake-2", + StorageClassName: "storage-class-b", + ResourceExtensionName: spqutil.StoragePolicyQuotaExtensionName, + CABundle: []byte("outdated"), + }, + }, + ) + }) + + Specify("an equal number of reconcile requests should be returned", func() { + Expect(reqs).To(ConsistOf( + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: "storage-class-a", + }, + }, + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: namespace, + Name: "storage-class-b", + }, + }, + )) + }) + }) + }) + }) +})