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  }