sigs.k8s.io/cluster-api@v1.7.1/exp/addons/internal/controllers/clusterresourceset_controller.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package controllers 18 19 import ( 20 "context" 21 "fmt" 22 "time" 23 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 apierrors "k8s.io/apimachinery/pkg/api/errors" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/labels" 30 "k8s.io/apimachinery/pkg/types" 31 kerrors "k8s.io/apimachinery/pkg/util/errors" 32 "k8s.io/klog/v2" 33 ctrl "sigs.k8s.io/controller-runtime" 34 "sigs.k8s.io/controller-runtime/pkg/builder" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 37 "sigs.k8s.io/controller-runtime/pkg/controller" 38 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 39 "sigs.k8s.io/controller-runtime/pkg/handler" 40 41 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 42 "sigs.k8s.io/cluster-api/controllers/remote" 43 addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1" 44 resourcepredicates "sigs.k8s.io/cluster-api/exp/addons/internal/controllers/predicates" 45 "sigs.k8s.io/cluster-api/util" 46 "sigs.k8s.io/cluster-api/util/conditions" 47 "sigs.k8s.io/cluster-api/util/patch" 48 "sigs.k8s.io/cluster-api/util/predicates" 49 ) 50 51 // ErrSecretTypeNotSupported signals that a Secret is not supported. 52 var ErrSecretTypeNotSupported = errors.New("unsupported secret type") 53 54 // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;patch 55 // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;patch 56 // +kubebuilder:rbac:groups=addons.cluster.x-k8s.io,resources=*,verbs=get;list;watch;create;update;patch;delete 57 // +kubebuilder:rbac:groups=addons.cluster.x-k8s.io,resources=clusterresourcesets/status;clusterresourcesets/finalizers,verbs=get;update;patch 58 59 // ClusterResourceSetReconciler reconciles a ClusterResourceSet object. 60 type ClusterResourceSetReconciler struct { 61 Client client.Client 62 Tracker *remote.ClusterCacheTracker 63 64 // WatchFilterValue is the label value used to filter events prior to reconciliation. 65 WatchFilterValue string 66 } 67 68 func (r *ClusterResourceSetReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { 69 err := ctrl.NewControllerManagedBy(mgr). 70 For(&addonsv1.ClusterResourceSet{}). 71 Watches( 72 &clusterv1.Cluster{}, 73 handler.EnqueueRequestsFromMapFunc(r.clusterToClusterResourceSet), 74 ). 75 WatchesMetadata( 76 &corev1.ConfigMap{}, 77 handler.EnqueueRequestsFromMapFunc(r.resourceToClusterResourceSet), 78 builder.WithPredicates( 79 resourcepredicates.ResourceCreateOrUpdate(ctrl.LoggerFrom(ctx)), 80 ), 81 ). 82 WatchesMetadata( 83 &corev1.Secret{}, 84 handler.EnqueueRequestsFromMapFunc(r.resourceToClusterResourceSet), 85 builder.WithPredicates( 86 resourcepredicates.ResourceCreateOrUpdate(ctrl.LoggerFrom(ctx)), 87 ), 88 ). 89 WithOptions(options). 90 WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). 91 Complete(r) 92 if err != nil { 93 return errors.Wrap(err, "failed setting up with a controller manager") 94 } 95 96 return nil 97 } 98 99 func (r *ClusterResourceSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { 100 log := ctrl.LoggerFrom(ctx) 101 102 // Fetch the ClusterResourceSet instance. 103 clusterResourceSet := &addonsv1.ClusterResourceSet{} 104 if err := r.Client.Get(ctx, req.NamespacedName, clusterResourceSet); err != nil { 105 if apierrors.IsNotFound(err) { 106 // Object not found, return. Created objects are automatically garbage collected. 107 // For additional cleanup logic use finalizers. 108 return ctrl.Result{}, nil 109 } 110 // Error reading the object - requeue the request. 111 return ctrl.Result{}, err 112 } 113 114 // Initialize the patch helper. 115 patchHelper, err := patch.NewHelper(clusterResourceSet, r.Client) 116 if err != nil { 117 return ctrl.Result{}, err 118 } 119 120 defer func() { 121 // Always attempt to Patch the ClusterResourceSet object and status after each reconciliation. 122 if err := patchHelper.Patch(ctx, clusterResourceSet, patch.WithStatusObservedGeneration{}); err != nil { 123 reterr = kerrors.NewAggregate([]error{reterr, err}) 124 } 125 }() 126 127 clusters, err := r.getClustersByClusterResourceSetSelector(ctx, clusterResourceSet) 128 if err != nil { 129 log.Error(err, "Failed fetching clusters that matches ClusterResourceSet labels", "ClusterResourceSet", klog.KObj(clusterResourceSet)) 130 conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.ClusterMatchFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) 131 return ctrl.Result{}, err 132 } 133 134 // Handle deletion reconciliation loop. 135 if !clusterResourceSet.ObjectMeta.DeletionTimestamp.IsZero() { 136 return ctrl.Result{}, r.reconcileDelete(ctx, clusters, clusterResourceSet) 137 } 138 139 // Add finalizer first if not set to avoid the race condition between init and delete. 140 // Note: Finalizers in general can only be added when the deletionTimestamp is not set. 141 if !controllerutil.ContainsFinalizer(clusterResourceSet, addonsv1.ClusterResourceSetFinalizer) { 142 controllerutil.AddFinalizer(clusterResourceSet, addonsv1.ClusterResourceSetFinalizer) 143 return ctrl.Result{}, nil 144 } 145 146 errs := []error{} 147 errClusterLockedOccurred := false 148 for _, cluster := range clusters { 149 if err := r.ApplyClusterResourceSet(ctx, cluster, clusterResourceSet); err != nil { 150 // Requeue if the reconcile failed because the ClusterCacheTracker was locked for 151 // the current cluster because of concurrent access. 152 if errors.Is(err, remote.ErrClusterLocked) { 153 log.V(5).Info("Requeuing because another worker has the lock on the ClusterCacheTracker") 154 errClusterLockedOccurred = true 155 } else { 156 // Append the error if the error is not ErrClusterLocked. 157 errs = append(errs, err) 158 } 159 } 160 } 161 162 // Return an aggregated error if errors occurred. 163 if len(errs) > 0 { 164 return ctrl.Result{}, kerrors.NewAggregate(errs) 165 } 166 167 // Requeue if ErrClusterLocked was returned for one of the clusters. 168 if errClusterLockedOccurred { 169 // Requeue after a minute to not end up in exponential delayed requeue which 170 // could take up to 16m40s. 171 return ctrl.Result{RequeueAfter: time.Minute}, nil 172 } 173 174 return ctrl.Result{}, nil 175 } 176 177 // reconcileDelete removes the deleted ClusterResourceSet from all the ClusterResourceSetBindings it is added to. 178 func (r *ClusterResourceSetReconciler) reconcileDelete(ctx context.Context, clusters []*clusterv1.Cluster, crs *addonsv1.ClusterResourceSet) error { 179 for _, cluster := range clusters { 180 log := ctrl.LoggerFrom(ctx, "Cluster", klog.KObj(cluster)) 181 182 clusterResourceSetBinding := &addonsv1.ClusterResourceSetBinding{} 183 clusterResourceSetBindingKey := client.ObjectKey{ 184 Namespace: cluster.Namespace, 185 Name: cluster.Name, 186 } 187 if err := r.Client.Get(ctx, clusterResourceSetBindingKey, clusterResourceSetBinding); err != nil { 188 if !apierrors.IsNotFound(err) { 189 return errors.Wrapf(err, "failed to get ClusterResourceSetBinding during ClusterResourceSet deletion") 190 } 191 controllerutil.RemoveFinalizer(crs, addonsv1.ClusterResourceSetFinalizer) 192 return nil 193 } 194 195 // Initialize the patch helper. 196 patchHelper, err := patch.NewHelper(clusterResourceSetBinding, r.Client) 197 if err != nil { 198 return err 199 } 200 201 clusterResourceSetBinding.RemoveBinding(crs) 202 clusterResourceSetBinding.OwnerReferences = util.RemoveOwnerRef(clusterResourceSetBinding.GetOwnerReferences(), metav1.OwnerReference{ 203 APIVersion: addonsv1.GroupVersion.String(), 204 Kind: "ClusterResourceSet", 205 Name: crs.Name, 206 }) 207 208 // If CRS list is empty in the binding, delete the binding else 209 // attempt to Patch the ClusterResourceSetBinding object after delete reconciliation if there is at least 1 binding left. 210 if len(clusterResourceSetBinding.Spec.Bindings) == 0 { 211 if r.Client.Delete(ctx, clusterResourceSetBinding) != nil { 212 log.Error(err, "failed to delete empty ClusterResourceSetBinding") 213 } 214 } else if err := patchHelper.Patch(ctx, clusterResourceSetBinding); err != nil { 215 return err 216 } 217 } 218 219 controllerutil.RemoveFinalizer(crs, addonsv1.ClusterResourceSetFinalizer) 220 return nil 221 } 222 223 // getClustersByClusterResourceSetSelector fetches Clusters matched by the ClusterResourceSet's label selector that are in the same namespace as the ClusterResourceSet object. 224 func (r *ClusterResourceSetReconciler) getClustersByClusterResourceSetSelector(ctx context.Context, clusterResourceSet *addonsv1.ClusterResourceSet) ([]*clusterv1.Cluster, error) { 225 log := ctrl.LoggerFrom(ctx) 226 227 clusterList := &clusterv1.ClusterList{} 228 selector, err := metav1.LabelSelectorAsSelector(&clusterResourceSet.Spec.ClusterSelector) 229 if err != nil { 230 return nil, errors.Wrap(err, "unable to convert selector") 231 } 232 233 // If a ClusterResourceSet has a nil or empty selector, it should match nothing, not everything. 234 if selector.Empty() { 235 log.Info("Empty ClusterResourceSet selector: No clusters are selected.") 236 return nil, nil 237 } 238 239 if err := r.Client.List(ctx, clusterList, client.InNamespace(clusterResourceSet.Namespace), client.MatchingLabelsSelector{Selector: selector}); err != nil { 240 return nil, errors.Wrap(err, "failed to list clusters") 241 } 242 243 clusters := []*clusterv1.Cluster{} 244 for i := range clusterList.Items { 245 c := &clusterList.Items[i] 246 if c.DeletionTimestamp.IsZero() { 247 clusters = append(clusters, c) 248 } 249 } 250 return clusters, nil 251 } 252 253 // ApplyClusterResourceSet applies resources in a ClusterResourceSet to a Cluster. Once applied, a record will be added to the 254 // cluster's ClusterResourceSetBinding. 255 // In ApplyOnce strategy, resources are applied only once to a particular cluster. ClusterResourceSetBinding is used to check if a resource is applied before. 256 // It applies resources best effort and continue on scenarios like: unsupported resource types, failure during creation, missing resources. 257 // In Reconcile strategy, resources are re-applied to a particular cluster when their definition changes. The hash in ClusterResourceSetBinding is used to check 258 // if a resource has changed or not. 259 // TODO: If a resource already exists in the cluster but not applied by ClusterResourceSet, the resource will be updated ? 260 func (r *ClusterResourceSetReconciler) ApplyClusterResourceSet(ctx context.Context, cluster *clusterv1.Cluster, clusterResourceSet *addonsv1.ClusterResourceSet) error { 261 log := ctrl.LoggerFrom(ctx, "Cluster", klog.KObj(cluster)) 262 ctx = ctrl.LoggerInto(ctx, log) 263 264 remoteClient, err := r.Tracker.GetClient(ctx, util.ObjectKey(cluster)) 265 if err != nil { 266 conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.RemoteClusterClientFailedReason, clusterv1.ConditionSeverityError, err.Error()) 267 return err 268 } 269 270 // Ensure that the Kubernetes API Server service has been created in the remote cluster before applying the ClusterResourceSet to avoid service IP conflict. 271 // This action is required when the remote cluster Kubernetes version is lower than v1.25. 272 // TODO: Remove this action once CAPI no longer supports Kubernetes versions below v1.25. See: https://github.com/kubernetes-sigs/cluster-api/issues/7804 273 if err = ensureKubernetesServiceCreated(ctx, remoteClient); err != nil { 274 return errors.Wrapf(err, "failed to retrieve the Service for Kubernetes API Server of the cluster %s/%s", cluster.Namespace, cluster.Name) 275 } 276 277 // Get ClusterResourceSetBinding object for the cluster. 278 clusterResourceSetBinding, err := r.getOrCreateClusterResourceSetBinding(ctx, cluster, clusterResourceSet) 279 if err != nil { 280 return err 281 } 282 283 // Initialize the patch helper. 284 patchHelper, err := patch.NewHelper(clusterResourceSetBinding, r.Client) 285 if err != nil { 286 return err 287 } 288 289 defer func() { 290 // Always attempt to Patch the ClusterResourceSetBinding object after each reconciliation. 291 if err := patchHelper.Patch(ctx, clusterResourceSetBinding); err != nil { 292 log.Error(err, "failed to patch config") 293 } 294 }() 295 296 // Ensure that the owner references are set on the ClusterResourceSetBinding. 297 clusterResourceSetBinding.SetOwnerReferences(util.EnsureOwnerRef(clusterResourceSetBinding.GetOwnerReferences(), metav1.OwnerReference{ 298 APIVersion: addonsv1.GroupVersion.String(), 299 Kind: "ClusterResourceSet", 300 Name: clusterResourceSet.Name, 301 UID: clusterResourceSet.UID, 302 })) 303 errList := []error{} 304 resourceSetBinding := clusterResourceSetBinding.GetOrCreateBinding(clusterResourceSet) 305 306 // Iterate all resources and apply them to the cluster and update the resource status in the ClusterResourceSetBinding object. 307 for _, resource := range clusterResourceSet.Spec.Resources { 308 unstructuredObj, err := r.getResource(ctx, resource, cluster.GetNamespace()) 309 if err != nil { 310 if err == ErrSecretTypeNotSupported { 311 conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.WrongSecretTypeReason, clusterv1.ConditionSeverityWarning, err.Error()) 312 } else { 313 conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.RetrievingResourceFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) 314 315 // Continue without adding the error to the aggregate if we can't find the resource. 316 if apierrors.IsNotFound(err) { 317 continue 318 } 319 } 320 errList = append(errList, err) 321 continue 322 } 323 324 // Ensure an ownerReference to the clusterResourceSet is on the resource. 325 if err := r.ensureResourceOwnerRef(ctx, clusterResourceSet, unstructuredObj); err != nil { 326 log.Error(err, "Failed to add ClusterResourceSet as resource owner reference", 327 "Resource type", unstructuredObj.GetKind(), "Resource name", unstructuredObj.GetName()) 328 errList = append(errList, err) 329 } 330 331 resourceScope, err := reconcileScopeForResource(clusterResourceSet, resource, resourceSetBinding, unstructuredObj) 332 if err != nil { 333 resourceSetBinding.SetBinding(addonsv1.ResourceBinding{ 334 ResourceRef: resource, 335 Hash: "", 336 Applied: false, 337 LastAppliedTime: &metav1.Time{Time: time.Now().UTC()}, 338 }) 339 340 errList = append(errList, err) 341 continue 342 } 343 344 if !resourceScope.needsApply() { 345 continue 346 } 347 348 // Set status in ClusterResourceSetBinding in case of early continue due to a failure. 349 // Set only when resource is retrieved successfully. 350 resourceSetBinding.SetBinding(addonsv1.ResourceBinding{ 351 ResourceRef: resource, 352 Hash: "", 353 Applied: false, 354 LastAppliedTime: &metav1.Time{Time: time.Now().UTC()}, 355 }) 356 357 // Apply all values in the key-value pair of the resource to the cluster. 358 // As there can be multiple key-value pairs in a resource, each value may have multiple objects in it. 359 isSuccessful := true 360 if err := resourceScope.apply(ctx, remoteClient); err != nil { 361 isSuccessful = false 362 log.Error(err, "failed to apply ClusterResourceSet resource", "Resource kind", resource.Kind, "Resource name", resource.Name) 363 conditions.MarkFalse(clusterResourceSet, addonsv1.ResourcesAppliedCondition, addonsv1.ApplyFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) 364 errList = append(errList, err) 365 } 366 367 resourceSetBinding.SetBinding(addonsv1.ResourceBinding{ 368 ResourceRef: resource, 369 Hash: resourceScope.hash(), 370 Applied: isSuccessful, 371 LastAppliedTime: &metav1.Time{Time: time.Now().UTC()}, 372 }) 373 } 374 if len(errList) > 0 { 375 return kerrors.NewAggregate(errList) 376 } 377 378 conditions.MarkTrue(clusterResourceSet, addonsv1.ResourcesAppliedCondition) 379 380 return nil 381 } 382 383 // getResource retrieves the requested resource and convert it to unstructured type. 384 // Unsupported resource kinds are not denied by validation webhook, hence no need to check here. 385 // Only supports Secrets/Configmaps as resource types and allow using resources in the same namespace with the cluster. 386 func (r *ClusterResourceSetReconciler) getResource(ctx context.Context, resourceRef addonsv1.ResourceRef, namespace string) (*unstructured.Unstructured, error) { 387 resourceName := types.NamespacedName{Name: resourceRef.Name, Namespace: namespace} 388 389 var resourceInterface interface{} 390 switch resourceRef.Kind { 391 case string(addonsv1.ConfigMapClusterResourceSetResourceKind): 392 resourceConfigMap, err := getConfigMap(ctx, r.Client, resourceName) 393 if err != nil { 394 return nil, err 395 } 396 397 resourceInterface = resourceConfigMap.DeepCopyObject() 398 case string(addonsv1.SecretClusterResourceSetResourceKind): 399 resourceSecret, err := getSecret(ctx, r.Client, resourceName) 400 if err != nil { 401 return nil, err 402 } 403 404 if resourceSecret.Type != addonsv1.ClusterResourceSetSecretType { 405 return nil, ErrSecretTypeNotSupported 406 } 407 resourceInterface = resourceSecret.DeepCopyObject() 408 } 409 410 raw := &unstructured.Unstructured{} 411 err := r.Client.Scheme().Convert(resourceInterface, raw, nil) 412 if err != nil { 413 return nil, err 414 } 415 416 return raw, nil 417 } 418 419 // ensureResourceOwnerRef adds the ClusterResourceSet as a OwnerReference to the resource. 420 func (r *ClusterResourceSetReconciler) ensureResourceOwnerRef(ctx context.Context, clusterResourceSet *addonsv1.ClusterResourceSet, resource *unstructured.Unstructured) error { 421 obj := resource.DeepCopy() 422 patchHelper, err := patch.NewHelper(obj, r.Client) 423 if err != nil { 424 return err 425 } 426 newRef := metav1.OwnerReference{ 427 APIVersion: addonsv1.GroupVersion.String(), 428 Kind: clusterResourceSet.GroupVersionKind().Kind, 429 Name: clusterResourceSet.GetName(), 430 UID: clusterResourceSet.GetUID(), 431 } 432 obj.SetOwnerReferences(util.EnsureOwnerRef(obj.GetOwnerReferences(), newRef)) 433 return patchHelper.Patch(ctx, obj) 434 } 435 436 // clusterToClusterResourceSet is mapper function that maps clusters to ClusterResourceSet. 437 func (r *ClusterResourceSetReconciler) clusterToClusterResourceSet(ctx context.Context, o client.Object) []ctrl.Request { 438 result := []ctrl.Request{} 439 440 cluster, ok := o.(*clusterv1.Cluster) 441 if !ok { 442 panic(fmt.Sprintf("Expected a Cluster but got a %T", o)) 443 } 444 445 resourceList := &addonsv1.ClusterResourceSetList{} 446 if err := r.Client.List(ctx, resourceList, client.InNamespace(cluster.Namespace)); err != nil { 447 return nil 448 } 449 450 labels := labels.Set(cluster.GetLabels()) 451 for i := range resourceList.Items { 452 rs := &resourceList.Items[i] 453 454 selector, err := metav1.LabelSelectorAsSelector(&rs.Spec.ClusterSelector) 455 if err != nil { 456 return nil 457 } 458 459 // If a ClusterResourceSet has a nil or empty selector, it should match nothing, not everything. 460 if selector.Empty() { 461 return nil 462 } 463 464 if !selector.Matches(labels) { 465 continue 466 } 467 468 name := client.ObjectKey{Namespace: rs.Namespace, Name: rs.Name} 469 result = append(result, ctrl.Request{NamespacedName: name}) 470 } 471 return result 472 } 473 474 // resourceToClusterResourceSet is mapper function that maps resources to ClusterResourceSet. 475 func (r *ClusterResourceSetReconciler) resourceToClusterResourceSet(ctx context.Context, o client.Object) []ctrl.Request { 476 result := []ctrl.Request{} 477 478 // Add all ClusterResourceSet owners. 479 for _, owner := range o.GetOwnerReferences() { 480 if owner.Kind == "ClusterResourceSet" { 481 name := client.ObjectKey{Namespace: o.GetNamespace(), Name: owner.Name} 482 result = append(result, ctrl.Request{NamespacedName: name}) 483 } 484 } 485 486 // If there is any ClusterResourceSet owner, that means the resource is reconciled before, 487 // and existing owners are the only matching ClusterResourceSets to this resource, so no need to return all ClusterResourceSets. 488 if len(result) > 0 { 489 return result 490 } 491 492 // Only core group is accepted as resources group 493 if o.GetObjectKind().GroupVersionKind().Group != "" { 494 return result 495 } 496 497 crsList := &addonsv1.ClusterResourceSetList{} 498 if err := r.Client.List(ctx, crsList, client.InNamespace(o.GetNamespace())); err != nil { 499 return nil 500 } 501 objKind, err := apiutil.GVKForObject(o, r.Client.Scheme()) 502 if err != nil { 503 return nil 504 } 505 for _, crs := range crsList.Items { 506 for _, resource := range crs.Spec.Resources { 507 if resource.Kind == objKind.Kind && resource.Name == o.GetName() { 508 name := client.ObjectKey{Namespace: o.GetNamespace(), Name: crs.Name} 509 result = append(result, ctrl.Request{NamespacedName: name}) 510 break 511 } 512 } 513 } 514 515 return result 516 }