sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/clusterclass/clusterclass_controller.go (about) 1 /* 2 Copyright 2021 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 clusterclass 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "sort" 24 "strings" 25 26 "github.com/pkg/errors" 27 corev1 "k8s.io/api/core/v1" 28 apierrors "k8s.io/apimachinery/pkg/api/errors" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/labels" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 kerrors "k8s.io/apimachinery/pkg/util/errors" 33 "k8s.io/apimachinery/pkg/util/sets" 34 ctrl "sigs.k8s.io/controller-runtime" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 "sigs.k8s.io/controller-runtime/pkg/controller" 37 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 38 "sigs.k8s.io/controller-runtime/pkg/handler" 39 "sigs.k8s.io/controller-runtime/pkg/reconcile" 40 41 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 42 "sigs.k8s.io/cluster-api/controllers/external" 43 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 44 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 45 "sigs.k8s.io/cluster-api/feature" 46 tlog "sigs.k8s.io/cluster-api/internal/log" 47 runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client" 48 "sigs.k8s.io/cluster-api/util/annotations" 49 "sigs.k8s.io/cluster-api/util/conditions" 50 "sigs.k8s.io/cluster-api/util/conversion" 51 "sigs.k8s.io/cluster-api/util/patch" 52 "sigs.k8s.io/cluster-api/util/predicates" 53 ) 54 55 // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io;bootstrap.cluster.x-k8s.io;controlplane.cluster.x-k8s.io,resources=*,verbs=get;list;watch;update;patch 56 // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusterclasses;clusterclasses/status,verbs=get;list;watch;update;patch 57 // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch 58 59 // Reconciler reconciles the ClusterClass object. 60 type Reconciler struct { 61 Client client.Client 62 APIReader client.Reader 63 64 // WatchFilterValue is the label value used to filter events prior to reconciliation. 65 WatchFilterValue string 66 67 // RuntimeClient is a client for calling runtime extensions. 68 RuntimeClient runtimeclient.Client 69 70 // UnstructuredCachingClient provides a client that forces caching of unstructured objects, 71 // thus allowing to optimize reads for templates or provider specific objects. 72 UnstructuredCachingClient client.Client 73 } 74 75 func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { 76 err := ctrl.NewControllerManagedBy(mgr). 77 For(&clusterv1.ClusterClass{}). 78 Named("clusterclass"). 79 WithOptions(options). 80 Watches( 81 &runtimev1.ExtensionConfig{}, 82 handler.EnqueueRequestsFromMapFunc(r.extensionConfigToClusterClass), 83 ). 84 WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)). 85 Complete(r) 86 87 if err != nil { 88 return errors.Wrap(err, "failed setting up with a controller manager") 89 } 90 return nil 91 } 92 93 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { 94 log := ctrl.LoggerFrom(ctx) 95 96 clusterClass := &clusterv1.ClusterClass{} 97 if err := r.Client.Get(ctx, req.NamespacedName, clusterClass); err != nil { 98 if apierrors.IsNotFound(err) { 99 return ctrl.Result{}, nil 100 } 101 // Error reading the object - requeue the request. 102 return ctrl.Result{}, err 103 } 104 105 // Return early if the ClusterClass is paused. 106 if annotations.HasPaused(clusterClass) { 107 log.Info("Reconciliation is paused for this object") 108 return ctrl.Result{}, nil 109 } 110 111 if !clusterClass.ObjectMeta.DeletionTimestamp.IsZero() { 112 return ctrl.Result{}, nil 113 } 114 115 patchHelper, err := patch.NewHelper(clusterClass, r.Client) 116 if err != nil { 117 return ctrl.Result{}, errors.Wrapf(err, "failed to create patch helper for %s", tlog.KObj{Obj: clusterClass}) 118 } 119 120 defer func() { 121 // Patch ObservedGeneration only if the reconciliation completed successfully 122 patchOpts := []patch.Option{} 123 if reterr == nil { 124 patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{}) 125 } 126 if err := patchHelper.Patch(ctx, clusterClass, patchOpts...); err != nil { 127 reterr = kerrors.NewAggregate([]error{reterr, errors.Wrapf(err, "failed to patch %s", tlog.KObj{Obj: clusterClass})}) 128 return 129 } 130 }() 131 return ctrl.Result{}, r.reconcile(ctx, clusterClass) 132 } 133 134 func (r *Reconciler) reconcile(ctx context.Context, clusterClass *clusterv1.ClusterClass) error { 135 if err := r.reconcileVariables(ctx, clusterClass); err != nil { 136 return err 137 } 138 outdatedRefs, err := r.reconcileExternalReferences(ctx, clusterClass) 139 if err != nil { 140 return err 141 } 142 143 reconcileConditions(clusterClass, outdatedRefs) 144 145 return nil 146 } 147 148 func (r *Reconciler) reconcileExternalReferences(ctx context.Context, clusterClass *clusterv1.ClusterClass) (map[*corev1.ObjectReference]*corev1.ObjectReference, error) { 149 // Collect all the reference from the ClusterClass to templates. 150 refs := []*corev1.ObjectReference{} 151 152 if clusterClass.Spec.Infrastructure.Ref != nil { 153 refs = append(refs, clusterClass.Spec.Infrastructure.Ref) 154 } 155 156 if clusterClass.Spec.ControlPlane.Ref != nil { 157 refs = append(refs, clusterClass.Spec.ControlPlane.Ref) 158 } 159 if clusterClass.Spec.ControlPlane.MachineInfrastructure != nil && clusterClass.Spec.ControlPlane.MachineInfrastructure.Ref != nil { 160 refs = append(refs, clusterClass.Spec.ControlPlane.MachineInfrastructure.Ref) 161 } 162 163 for _, mdClass := range clusterClass.Spec.Workers.MachineDeployments { 164 if mdClass.Template.Bootstrap.Ref != nil { 165 refs = append(refs, mdClass.Template.Bootstrap.Ref) 166 } 167 if mdClass.Template.Infrastructure.Ref != nil { 168 refs = append(refs, mdClass.Template.Infrastructure.Ref) 169 } 170 } 171 172 for _, mpClass := range clusterClass.Spec.Workers.MachinePools { 173 if mpClass.Template.Bootstrap.Ref != nil { 174 refs = append(refs, mpClass.Template.Bootstrap.Ref) 175 } 176 if mpClass.Template.Infrastructure.Ref != nil { 177 refs = append(refs, mpClass.Template.Infrastructure.Ref) 178 } 179 } 180 181 // Ensure all referenced objects are owned by the ClusterClass. 182 // Nb. Some external objects can be referenced multiple times in the ClusterClass, 183 // but we only want to set the owner reference once per unique external object. 184 // For example the same KubeadmConfigTemplate could be referenced in multiple MachineDeployment 185 // or MachinePool classes. 186 errs := []error{} 187 reconciledRefs := sets.Set[string]{} 188 outdatedRefs := map[*corev1.ObjectReference]*corev1.ObjectReference{} 189 for i := range refs { 190 ref := refs[i] 191 uniqueKey := uniqueObjectRefKey(ref) 192 193 // Continue as we only have to reconcile every referenced object once. 194 if reconciledRefs.Has(uniqueKey) { 195 continue 196 } 197 198 reconciledRefs.Insert(uniqueKey) 199 200 // Add the ClusterClass as owner reference to the templates so clusterctl move 201 // can identify all related objects and Kubernetes garbage collector deletes 202 // all referenced templates on ClusterClass deletion. 203 if err := r.reconcileExternal(ctx, clusterClass, ref); err != nil { 204 errs = append(errs, err) 205 continue 206 } 207 208 // Check if the template reference is outdated, i.e. it is not using the latest apiVersion 209 // for the current CAPI contract. 210 updatedRef := ref.DeepCopy() 211 if err := conversion.UpdateReferenceAPIContract(ctx, r.Client, updatedRef); err != nil { 212 errs = append(errs, err) 213 } 214 if ref.GroupVersionKind().Version != updatedRef.GroupVersionKind().Version { 215 outdatedRefs[ref] = updatedRef 216 } 217 } 218 if len(errs) > 0 { 219 return nil, kerrors.NewAggregate(errs) 220 } 221 return outdatedRefs, nil 222 } 223 224 func (r *Reconciler) reconcileVariables(ctx context.Context, clusterClass *clusterv1.ClusterClass) error { 225 errs := []error{} 226 allVariableDefinitions := map[string]*clusterv1.ClusterClassStatusVariable{} 227 // Add inline variable definitions to the ClusterClass status. 228 for _, variable := range clusterClass.Spec.Variables { 229 allVariableDefinitions[variable.Name] = addNewStatusVariable(variable, clusterv1.VariableDefinitionFromInline) 230 } 231 232 // If RuntimeSDK is enabled call the DiscoverVariables hook for all associated Runtime Extensions and add the variables 233 // to the ClusterClass status. 234 if feature.Gates.Enabled(feature.RuntimeSDK) { 235 for _, patch := range clusterClass.Spec.Patches { 236 if patch.External == nil || patch.External.DiscoverVariablesExtension == nil { 237 continue 238 } 239 req := &runtimehooksv1.DiscoverVariablesRequest{} 240 req.Settings = patch.External.Settings 241 242 resp := &runtimehooksv1.DiscoverVariablesResponse{} 243 err := r.RuntimeClient.CallExtension(ctx, runtimehooksv1.DiscoverVariables, clusterClass, *patch.External.DiscoverVariablesExtension, req, resp) 244 if err != nil { 245 errs = append(errs, errors.Wrapf(err, "failed to call DiscoverVariables for patch %s", patch.Name)) 246 continue 247 } 248 if resp.Status != runtimehooksv1.ResponseStatusSuccess { 249 errs = append(errs, errors.Errorf("patch %s returned status %q with message %q", patch.Name, resp.Status, resp.Message)) 250 continue 251 } 252 if resp.Variables != nil { 253 uniqueNamesForPatch := sets.Set[string]{} 254 for _, variable := range resp.Variables { 255 // Ensure a patch doesn't define multiple variables with the same name. 256 if uniqueNamesForPatch.Has(variable.Name) { 257 errs = append(errs, errors.Errorf("variable %q is defined multiple times in variable discovery response from patch %q", variable.Name, patch.Name)) 258 continue 259 } 260 uniqueNamesForPatch.Insert(variable.Name) 261 262 // If a variable of the same name already exists in allVariableDefinitions add the new definition to the existing list. 263 if _, ok := allVariableDefinitions[variable.Name]; ok { 264 allVariableDefinitions[variable.Name] = addDefinitionToExistingStatusVariable(variable, patch.Name, allVariableDefinitions[variable.Name]) 265 continue 266 } 267 268 // Add the new variable to the list. 269 allVariableDefinitions[variable.Name] = addNewStatusVariable(variable, patch.Name) 270 } 271 } 272 } 273 } 274 if len(errs) > 0 { 275 // TODO: Decide whether to remove old variables if discovery fails. 276 conditions.MarkFalse(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition, clusterv1.VariableDiscoveryFailedReason, clusterv1.ConditionSeverityError, 277 "VariableDiscovery failed: %s", kerrors.NewAggregate(errs)) 278 return errors.Wrapf(kerrors.NewAggregate(errs), "failed to discover variables for ClusterClass %s", clusterClass.Name) 279 } 280 281 statusVarList := []clusterv1.ClusterClassStatusVariable{} 282 for _, variable := range allVariableDefinitions { 283 statusVarList = append(statusVarList, *variable) 284 } 285 // Alphabetically sort the variables by name. This ensures no unnecessary updates to the ClusterClass status. 286 // Note: Definitions in `statusVarList[i].Definitions` have a stable order as they are added in a deterministic order 287 // and are always held in an array. 288 sort.SliceStable(statusVarList, func(i, j int) bool { 289 return statusVarList[i].Name < statusVarList[j].Name 290 }) 291 clusterClass.Status.Variables = statusVarList 292 conditions.MarkTrue(clusterClass, clusterv1.ClusterClassVariablesReconciledCondition) 293 return nil 294 } 295 296 func reconcileConditions(clusterClass *clusterv1.ClusterClass, outdatedRefs map[*corev1.ObjectReference]*corev1.ObjectReference) { 297 if len(outdatedRefs) > 0 { 298 var msg []string 299 for currentRef, updatedRef := range outdatedRefs { 300 msg = append(msg, fmt.Sprintf("Ref %q should be %q", refString(currentRef), refString(updatedRef))) 301 } 302 conditions.Set( 303 clusterClass, 304 conditions.FalseCondition( 305 clusterv1.ClusterClassRefVersionsUpToDateCondition, 306 clusterv1.ClusterClassOutdatedRefVersionsReason, 307 clusterv1.ConditionSeverityWarning, 308 strings.Join(msg, ", "), 309 ), 310 ) 311 return 312 } 313 314 conditions.Set( 315 clusterClass, 316 conditions.TrueCondition(clusterv1.ClusterClassRefVersionsUpToDateCondition), 317 ) 318 } 319 320 func addNewStatusVariable(variable clusterv1.ClusterClassVariable, from string) *clusterv1.ClusterClassStatusVariable { 321 return &clusterv1.ClusterClassStatusVariable{ 322 Name: variable.Name, 323 DefinitionsConflict: false, 324 Definitions: []clusterv1.ClusterClassStatusVariableDefinition{ 325 { 326 From: from, 327 Required: variable.Required, 328 Schema: variable.Schema, 329 }, 330 }} 331 } 332 333 func addDefinitionToExistingStatusVariable(variable clusterv1.ClusterClassVariable, from string, existingVariable *clusterv1.ClusterClassStatusVariable) *clusterv1.ClusterClassStatusVariable { 334 combinedVariable := existingVariable.DeepCopy() 335 newVariableDefinition := clusterv1.ClusterClassStatusVariableDefinition{ 336 From: from, 337 Required: variable.Required, 338 Schema: variable.Schema, 339 } 340 combinedVariable.Definitions = append(existingVariable.Definitions, newVariableDefinition) 341 342 // If the new definition is different from any existing definition, set DefinitionsConflict to true. 343 // If definitions already conflict, no need to check. 344 if !combinedVariable.DefinitionsConflict { 345 currentDefinition := combinedVariable.Definitions[0] 346 if !(currentDefinition.Required == newVariableDefinition.Required && reflect.DeepEqual(currentDefinition.Schema, newVariableDefinition.Schema)) { 347 combinedVariable.DefinitionsConflict = true 348 } 349 } 350 return combinedVariable 351 } 352 353 func refString(ref *corev1.ObjectReference) string { 354 return fmt.Sprintf("%s %s/%s", ref.GroupVersionKind().String(), ref.Namespace, ref.Name) 355 } 356 357 func (r *Reconciler) reconcileExternal(ctx context.Context, clusterClass *clusterv1.ClusterClass, ref *corev1.ObjectReference) error { 358 log := ctrl.LoggerFrom(ctx) 359 360 obj, err := external.Get(ctx, r.UnstructuredCachingClient, ref, clusterClass.Namespace) 361 if err != nil { 362 if apierrors.IsNotFound(errors.Cause(err)) { 363 return errors.Wrapf(err, "Could not find external object for the ClusterClass. refGroupVersionKind: %s, refName: %s", ref.GroupVersionKind(), ref.Name) 364 } 365 return errors.Wrapf(err, "failed to get the external object for the ClusterClass. refGroupVersionKind: %s, refName: %s", ref.GroupVersionKind(), ref.Name) 366 } 367 368 // If referenced object is paused, return early. 369 if annotations.HasPaused(obj) { 370 log.V(3).Info("External object referenced is paused", "refGroupVersionKind", ref.GroupVersionKind(), "refName", ref.Name) 371 return nil 372 } 373 374 // Initialize the patch helper. 375 patchHelper, err := patch.NewHelper(obj, r.Client) 376 if err != nil { 377 return errors.Wrapf(err, "failed to create patch helper for %s", tlog.KObj{Obj: obj}) 378 } 379 380 // Set external object ControllerReference to the ClusterClass. 381 if err := controllerutil.SetOwnerReference(clusterClass, obj, r.Client.Scheme()); err != nil { 382 return errors.Wrapf(err, "failed to set ClusterClass owner reference for %s", tlog.KObj{Obj: obj}) 383 } 384 385 // Patch the external object. 386 if err := patchHelper.Patch(ctx, obj); err != nil { 387 return errors.Wrapf(err, "failed to patch object %s", tlog.KObj{Obj: obj}) 388 } 389 390 return nil 391 } 392 393 func uniqueObjectRefKey(ref *corev1.ObjectReference) string { 394 return fmt.Sprintf("Name:%s, Namespace:%s, Kind:%s, APIVersion:%s", ref.Name, ref.Namespace, ref.Kind, ref.APIVersion) 395 } 396 397 // extensionConfigToClusterClass maps an ExtensionConfigs to the corresponding ClusterClass to reconcile them on updates 398 // of the ExtensionConfig. 399 func (r *Reconciler) extensionConfigToClusterClass(ctx context.Context, o client.Object) []reconcile.Request { 400 res := []ctrl.Request{} 401 log := ctrl.LoggerFrom(ctx) 402 ext, ok := o.(*runtimev1.ExtensionConfig) 403 if !ok { 404 panic(fmt.Sprintf("Expected an ExtensionConfig but got a %T", o)) 405 } 406 407 clusterClasses := clusterv1.ClusterClassList{} 408 selector, err := metav1.LabelSelectorAsSelector(ext.Spec.NamespaceSelector) 409 if err != nil { 410 return nil 411 } 412 if err := r.Client.List(ctx, &clusterClasses); err != nil { 413 return nil 414 } 415 for _, clusterClass := range clusterClasses.Items { 416 if !matchNamespace(ctx, r.Client, selector, clusterClass.Namespace) { 417 continue 418 } 419 for _, patch := range clusterClass.Spec.Patches { 420 if patch.External != nil && patch.External.DiscoverVariablesExtension != nil { 421 extName, err := runtimeclient.ExtensionNameFromHandlerName(*patch.External.DiscoverVariablesExtension) 422 if err != nil { 423 log.Error(err, "failed to reconcile ClusterClass for ExtensionConfig") 424 continue 425 } 426 if extName == ext.Name { 427 res = append(res, ctrl.Request{NamespacedName: client.ObjectKey{Namespace: clusterClass.Namespace, Name: clusterClass.Name}}) 428 // Once we've added the ClusterClass once we can break here. 429 break 430 } 431 } 432 } 433 } 434 return res 435 } 436 437 // matchNamespace returns true if the passed namespace matches the selector. 438 func matchNamespace(ctx context.Context, c client.Client, selector labels.Selector, namespace string) bool { 439 // Return early if the selector is empty. 440 if selector.Empty() { 441 return true 442 } 443 444 ns := &metav1.PartialObjectMetadata{} 445 ns.SetGroupVersionKind(schema.GroupVersionKind{ 446 Group: "", 447 Version: "v1", 448 Kind: "Namespace", 449 }) 450 if err := c.Get(ctx, client.ObjectKey{Name: namespace}, ns); err != nil { 451 return false 452 } 453 return selector.Matches(labels.Set(ns.GetLabels())) 454 }