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  }