sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/topology/machineset/machineset_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 machineset
    18  
    19  import (
    20  	"context"
    21  
    22  	"github.com/pkg/errors"
    23  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    27  	"k8s.io/klog/v2"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  	"sigs.k8s.io/controller-runtime/pkg/builder"
    30  	"sigs.k8s.io/controller-runtime/pkg/client"
    31  	"sigs.k8s.io/controller-runtime/pkg/controller"
    32  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    33  	"sigs.k8s.io/controller-runtime/pkg/handler"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	tlog "sigs.k8s.io/cluster-api/internal/log"
    37  	"sigs.k8s.io/cluster-api/util"
    38  	"sigs.k8s.io/cluster-api/util/annotations"
    39  	clog "sigs.k8s.io/cluster-api/util/log"
    40  	"sigs.k8s.io/cluster-api/util/patch"
    41  	"sigs.k8s.io/cluster-api/util/predicates"
    42  )
    43  
    44  // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io;bootstrap.cluster.x-k8s.io,resources=*,verbs=delete
    45  // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters,verbs=get;list;watch
    46  // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments,verbs=get;list;watch
    47  // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinesets;machinesets/finalizers,verbs=get;list;watch;update;patch
    48  
    49  // Reconciler deletes referenced templates during deletion of topology-owned MachineSets.
    50  // The templates are only deleted, if they are not used in other MachineDeployments or MachineSets which are not in deleting state,
    51  // i.e. the templates would otherwise be orphaned after the MachineSet deletion completes.
    52  // Note: To achieve this the reconciler sets a finalizer to hook into the MachineSet deletions.
    53  type Reconciler struct {
    54  	Client client.Client
    55  	// APIReader is used to list MachineSets directly via the API server to avoid
    56  	// race conditions caused by an outdated cache.
    57  	APIReader        client.Reader
    58  	WatchFilterValue string
    59  }
    60  
    61  func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
    62  	clusterToMachineSets, err := util.ClusterToTypedObjectsMapper(mgr.GetClient(), &clusterv1.MachineSetList{}, mgr.GetScheme())
    63  	if err != nil {
    64  		return err
    65  	}
    66  
    67  	err = ctrl.NewControllerManagedBy(mgr).
    68  		For(&clusterv1.MachineSet{},
    69  			builder.WithPredicates(
    70  				predicates.All(ctrl.LoggerFrom(ctx),
    71  					predicates.ResourceIsTopologyOwned(ctrl.LoggerFrom(ctx)),
    72  					predicates.ResourceNotPaused(ctrl.LoggerFrom(ctx))),
    73  			),
    74  		).
    75  		Named("topology/machineset").
    76  		WithOptions(options).
    77  		WithEventFilter(predicates.ResourceHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
    78  		Watches(
    79  			&clusterv1.Cluster{},
    80  			handler.EnqueueRequestsFromMapFunc(clusterToMachineSets),
    81  			builder.WithPredicates(
    82  				predicates.All(ctrl.LoggerFrom(ctx),
    83  					predicates.ClusterUnpaused(ctrl.LoggerFrom(ctx)),
    84  					predicates.ClusterHasTopology(ctrl.LoggerFrom(ctx)),
    85  				),
    86  			),
    87  		).
    88  		Complete(r)
    89  	if err != nil {
    90  		return errors.Wrap(err, "failed setting up with a controller manager")
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  // Reconcile deletes referenced templates during deletion of topology-owned MachineSets.
    97  // The templates are only deleted, if they are not used in other MachineDeployments or MachineSets which are not in deleting state,
    98  // i.e. the templates would otherwise be orphaned after the MachineSet deletion completes.
    99  // Additional context:
   100  // * MachineSet deletion:
   101  //   - MachineSets are deleted and garbage collected first (without waiting until all Machines are also deleted)
   102  //   - After that, deletion of Machines is automatically triggered by Kubernetes based on owner references.
   103  //
   104  // Note: We assume templates are not reused by different MachineDeployments, which is (only) true for topology-owned
   105  //
   106  //	MachineDeployments.
   107  //
   108  // We don't have to set the finalizer, as it's already set during MachineSet creation
   109  // in the MachineSet controller.
   110  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
   111  	// Fetch the MachineSet instance.
   112  	ms := &clusterv1.MachineSet{}
   113  	if err := r.Client.Get(ctx, req.NamespacedName, ms); err != nil {
   114  		if apierrors.IsNotFound(err) {
   115  			// Object not found, return.
   116  			return ctrl.Result{}, nil
   117  		}
   118  		// Error reading the object - requeue the request.
   119  		return ctrl.Result{}, errors.Wrapf(err, "failed to get MachineSet/%s", req.NamespacedName.Name)
   120  	}
   121  
   122  	// AddOwners adds the owners of MachineSet as k/v pairs to the logger.
   123  	// Specifically, it will add MachineDeployment.
   124  	ctx, log, err := clog.AddOwners(ctx, r.Client, ms)
   125  	if err != nil {
   126  		return ctrl.Result{}, err
   127  	}
   128  
   129  	log = log.WithValues("Cluster", klog.KRef(ms.Namespace, ms.Spec.ClusterName))
   130  	ctx = ctrl.LoggerInto(ctx, log)
   131  
   132  	cluster, err := util.GetClusterByName(ctx, r.Client, ms.Namespace, ms.Spec.ClusterName)
   133  	if err != nil {
   134  		return ctrl.Result{}, err
   135  	}
   136  
   137  	// Return early if the object or Cluster is paused.
   138  	if annotations.IsPaused(cluster, ms) {
   139  		log.Info("Reconciliation is paused for this object")
   140  		return ctrl.Result{}, nil
   141  	}
   142  
   143  	// Create a patch helper to add or remove the finalizer from the MachineSet.
   144  	patchHelper, err := patch.NewHelper(ms, r.Client)
   145  	if err != nil {
   146  		return ctrl.Result{}, err
   147  	}
   148  	defer func() {
   149  		if err := patchHelper.Patch(ctx, ms); err != nil {
   150  			reterr = kerrors.NewAggregate([]error{reterr, err})
   151  		}
   152  	}()
   153  
   154  	// Handle deletion reconciliation loop.
   155  	if !ms.ObjectMeta.DeletionTimestamp.IsZero() {
   156  		return ctrl.Result{}, r.reconcileDelete(ctx, ms)
   157  	}
   158  
   159  	// Add finalizer first if not set to avoid the race condition between init and delete.
   160  	// Note: Finalizers in general can only be added when the deletionTimestamp is not set.
   161  	if !controllerutil.ContainsFinalizer(ms, clusterv1.MachineSetTopologyFinalizer) {
   162  		controllerutil.AddFinalizer(ms, clusterv1.MachineSetTopologyFinalizer)
   163  		return ctrl.Result{}, nil
   164  	}
   165  
   166  	return ctrl.Result{}, nil
   167  }
   168  
   169  // reconcileDelete deletes templates referenced in a MachineSet, if the templates are not used by other
   170  // MachineDeployments or MachineSets.
   171  func (r *Reconciler) reconcileDelete(ctx context.Context, ms *clusterv1.MachineSet) error {
   172  	// Gets the name of the MachineDeployment that controls this MachineSet.
   173  	mdName, err := getMachineDeploymentName(ms)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// Get all the MachineSets for the MachineDeployment.
   179  	msList, err := GetMachineSetsForDeployment(ctx, r.APIReader, *mdName)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	// Fetch the MachineDeployment instance, if it still exists.
   185  	// Note: This can happen because MachineDeployments are deleted before their corresponding MachineSets.
   186  	md := &clusterv1.MachineDeployment{}
   187  	if err := r.Client.Get(ctx, *mdName, md); err != nil {
   188  		if !apierrors.IsNotFound(err) {
   189  			// Error reading the object - requeue the request.
   190  			return errors.Wrapf(err, "failed to get MachineDeployment/%s", mdName.Name)
   191  		}
   192  		// If the MachineDeployment doesn't exist anymore, set md to nil, so we can handle that case correctly below.
   193  		md = nil
   194  	}
   195  
   196  	// Calculate which templates are still in use by MachineDeployments or MachineSets which are not in deleting state.
   197  	templatesInUse, err := CalculateTemplatesInUse(md, msList)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	// Delete unused templates.
   203  	ref := ms.Spec.Template.Spec.Bootstrap.ConfigRef
   204  	if err := DeleteTemplateIfUnused(ctx, r.Client, templatesInUse, ref); err != nil {
   205  		return errors.Wrapf(err, "failed to delete bootstrap template for %s", tlog.KObj{Obj: ms})
   206  	}
   207  	ref = &ms.Spec.Template.Spec.InfrastructureRef
   208  	if err := DeleteTemplateIfUnused(ctx, r.Client, templatesInUse, ref); err != nil {
   209  		return errors.Wrapf(err, "failed to delete infrastructure template for %s", tlog.KObj{Obj: ms})
   210  	}
   211  
   212  	// Remove the finalizer so the MachineSet can be garbage collected by Kubernetes.
   213  	controllerutil.RemoveFinalizer(ms, clusterv1.MachineSetTopologyFinalizer)
   214  
   215  	return nil
   216  }
   217  
   218  // getMachineDeploymentName calculates the MachineDeployment name based on owner references.
   219  func getMachineDeploymentName(ms *clusterv1.MachineSet) (*types.NamespacedName, error) {
   220  	for _, ref := range ms.GetOwnerReferences() {
   221  		if ref.Kind != "MachineDeployment" {
   222  			continue
   223  		}
   224  		gv, err := schema.ParseGroupVersion(ref.APIVersion)
   225  		if err != nil {
   226  			return nil, errors.Errorf("could not calculate MachineDeployment name for %s: invalid apiVersion %q: %v",
   227  				tlog.KObj{Obj: ms}, ref.APIVersion, err)
   228  		}
   229  		if gv.Group == clusterv1.GroupVersion.Group {
   230  			return &client.ObjectKey{Namespace: ms.Namespace, Name: ref.Name}, nil
   231  		}
   232  	}
   233  
   234  	// Note: Once we set an owner reference to a MachineDeployment in a MachineSet it stays there
   235  	// and is not deleted when the MachineDeployment is deleted. So we assume there's something wrong,
   236  	// if we couldn't find a MachineDeployment owner reference.
   237  	return nil, errors.Errorf("could not calculate MachineDeployment name for %s", tlog.KObj{Obj: ms})
   238  }