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