diff --git a/exp/addons/controllers/clusterresourceset_controller.go b/exp/addons/controllers/clusterresourceset_controller.go index 742acb65f94a..766ef8d1fd6f 100644 --- a/exp/addons/controllers/clusterresourceset_controller.go +++ b/exp/addons/controllers/clusterresourceset_controller.go @@ -25,6 +25,7 @@ import ( "github.com/go-logr/logr" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -35,12 +36,14 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha3" "sigs.k8s.io/cluster-api/controllers/remote" addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1alpha3" + resourcepredicates "sigs.k8s.io/cluster-api/exp/addons/controllers/predicates" "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api/util/conditions" "sigs.k8s.io/cluster-api/util/patch" "sigs.k8s.io/cluster-api/util/predicates" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/source" @@ -65,7 +68,7 @@ type ClusterResourceSetReconciler struct { } func (r *ClusterResourceSetReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error { - _, err := ctrl.NewControllerManagedBy(mgr). + controller, err := ctrl.NewControllerManagedBy(mgr). For(&addonsv1.ClusterResourceSet{}). Watches( &source.Kind{Type: &clusterv1.Cluster{}}, @@ -78,6 +81,27 @@ func (r *ClusterResourceSetReconciler) SetupWithManager(mgr ctrl.Manager, option return errors.Wrap(err, "failed setting up with a controller manager") } + err = controller.Watch( + &source.Kind{Type: &corev1.ConfigMap{}}, + &handler.EnqueueRequestsFromMapFunc{ + ToRequests: handler.ToRequestsFunc(r.resourceToClusterResourceSet), + }, + resourcepredicates.ResourceCreate(r.Log), + ) + if err != nil { + return errors.Wrap(err, "failed adding Watch for ConfigMaps to controller manager") + } + + err = controller.Watch( + &source.Kind{Type: &corev1.Secret{}}, + &handler.EnqueueRequestsFromMapFunc{ + ToRequests: handler.ToRequestsFunc(r.resourceToClusterResourceSet), + }, + resourcepredicates.AddonsSecretCreate(r.Log), + ) + if err != nil { + return errors.Wrap(err, "failed adding Watch for Secret to controller manager") + } r.scheme = mgr.GetScheme() return nil } @@ -390,3 +414,47 @@ func (r *ClusterResourceSetReconciler) clusterToClusterResourceSet(o handler.Map } return result } + +// resourceToClusterResourceSet is mapper function that maps resources to ClusterResourceSet +func (r *ClusterResourceSetReconciler) resourceToClusterResourceSet(o handler.MapObject) []ctrl.Request { + result := []ctrl.Request{} + + // Add all ClusterResourceSet owners. + for _, owner := range o.Meta.GetOwnerReferences() { + if owner.Kind == "ClusterResourceSet" { + name := client.ObjectKey{Namespace: o.Meta.GetNamespace(), Name: owner.Name} + result = append(result, ctrl.Request{NamespacedName: name}) + } + } + + // If there is any ClusterResourceSet owner, that means the resource is reconciled before, + // and existing owners are the only matching ClusterResourceSets to this resource, so no need to return all ClusterResourceSets. + if len(result) > 0 { + return result + } + + // Only core group is accepted as resources group + if o.Object.GetObjectKind().GroupVersionKind().Group != "" { + return result + } + + crsList := &addonsv1.ClusterResourceSetList{} + if err := r.Client.List(context.Background(), crsList, client.InNamespace(o.Meta.GetNamespace())); err != nil { + return nil + } + objKind, err := apiutil.GVKForObject(o.Object, r.scheme) + if err != nil { + return nil + } + for _, crs := range crsList.Items { + for _, resource := range crs.Spec.Resources { + if resource.Kind == objKind.Kind && resource.Name == o.Meta.GetName() { + name := client.ObjectKey{Namespace: o.Meta.GetNamespace(), Name: crs.Name} + result = append(result, ctrl.Request{NamespacedName: name}) + break + } + } + } + + return result +} diff --git a/exp/addons/controllers/clusterresourceset_controller_test.go b/exp/addons/controllers/clusterresourceset_controller_test.go index fb61620ac384..88c832cc5776 100644 --- a/exp/addons/controllers/clusterresourceset_controller_test.go +++ b/exp/addons/controllers/clusterresourceset_controller_test.go @@ -217,4 +217,79 @@ apiVersion: v1`, return apierrors.IsNotFound(err) }, timeout).Should(BeTrue()) }) + It("Should reconcile a ClusterResourceSet when a resource is created that is part of ClusterResourceSet resources", func() { + + labels := map[string]string{"foo2": "bar2"} + newCMName := "test-configmap2" + + clusterResourceSetInstance := &addonsv1.ClusterResourceSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-clusterresourceset", + Namespace: defaultNamespaceName, + }, + Spec: addonsv1.ClusterResourceSetSpec{ + ClusterSelector: metav1.LabelSelector{ + MatchLabels: labels, + }, + Resources: []addonsv1.ResourceRef{{Name: newCMName, Kind: "ConfigMap"}}, + }, + } + // Create the ClusterResourceSet. + Expect(testEnv.Create(ctx, clusterResourceSetInstance)).To(Succeed()) + defer func() { + Expect(testEnv.Delete(ctx, clusterResourceSetInstance)).To(Succeed()) + }() + + testCluster.SetLabels(labels) + Expect(testEnv.Update(ctx, testCluster)).To(Succeed()) + + By("Verifying ClusterResourceSetBinding is created with cluster owner reference") + // Wait until ClusterResourceSetBinding is created for the Cluster + clusterResourceSetBindingKey := client.ObjectKey{ + Namespace: testCluster.Namespace, + Name: testCluster.Name, + } + Eventually(func() bool { + binding := &addonsv1.ClusterResourceSetBinding{} + + err := testEnv.Get(ctx, clusterResourceSetBindingKey, binding) + return err == nil + }, timeout).Should(BeTrue()) + + // Initially ConfiMap is missing, so no resources in the binding. + Eventually(func() bool { + binding := &addonsv1.ClusterResourceSetBinding{} + + err := testEnv.Get(ctx, clusterResourceSetBindingKey, binding) + if err == nil { + if len(binding.Spec.Bindings) > 0 && len(binding.Spec.Bindings[0].Resources) == 0 { + return true + } + } + return false + }, timeout).Should(BeTrue()) + + testConfigmap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: newCMName, + Namespace: defaultNamespaceName, + }, + Data: map[string]string{}, + } + Expect(testEnv.Create(ctx, testConfigmap)).To(Succeed()) + + // When the ConfigMap resource is created, CRS should get reconciled immediately. + Eventually(func() bool { + binding := &addonsv1.ClusterResourceSetBinding{} + + err := testEnv.Get(ctx, clusterResourceSetBindingKey, binding) + if err == nil { + if len(binding.Spec.Bindings[0].Resources) > 0 && binding.Spec.Bindings[0].Resources[0].Name == newCMName { + return true + } + } + return false + }, timeout).Should(BeTrue()) + Expect(testEnv.Delete(ctx, testConfigmap)).To(Succeed()) + }) }) diff --git a/exp/addons/controllers/predicates/resource_predicates.go b/exp/addons/controllers/predicates/resource_predicates.go new file mode 100644 index 000000000000..f872e8728996 --- /dev/null +++ b/exp/addons/controllers/predicates/resource_predicates.go @@ -0,0 +1,60 @@ +/* +Copyright 2020 The Kubernetes 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 predicates + +import ( + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1alpha3" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// ResourceCreate returns a predicate that returns true for a create event +func ResourceCreate(logger logr.Logger) predicate.Funcs { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + UpdateFunc: func(e event.UpdateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + } +} + +// AddonsSecretCreate returns a predicate that returns true for a Secret create event if in addons Secret type +func AddonsSecretCreate(logger logr.Logger) predicate.Funcs { + log := logger.WithValues("predicate", "SecretCreateOrUpdate") + + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + log = log.WithValues("eventType", "create") + s, ok := e.Object.(*corev1.Secret) + if !ok { + log.V(4).Info("Expected Secret", "secret", e.Object.GetObjectKind().GroupVersionKind().String()) + return false + } + if string(s.Type) != string(addonsv1.ClusterResourceSetSecretType) { + log.V(4).Info("Expected Secret Type", "type", addonsv1.SecretClusterResourceSetResourceKind, + "got", string(s.Type)) + return false + } + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { return false }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + } +}