sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/machinedeployment/machinedeployment_controller.go (about)

     1  /*
     2  Copyright 2019 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 machinedeployment
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/labels"
    29  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/client-go/tools/record"
    31  	"k8s.io/klog/v2"
    32  	ctrl "sigs.k8s.io/controller-runtime"
    33  	"sigs.k8s.io/controller-runtime/pkg/builder"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/controller"
    36  	"sigs.k8s.io/controller-runtime/pkg/handler"
    37  
    38  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    39  	"sigs.k8s.io/cluster-api/controllers/external"
    40  	"sigs.k8s.io/cluster-api/internal/util/ssa"
    41  	"sigs.k8s.io/cluster-api/util"
    42  	"sigs.k8s.io/cluster-api/util/annotations"
    43  	"sigs.k8s.io/cluster-api/util/conditions"
    44  	utilconversion "sigs.k8s.io/cluster-api/util/conversion"
    45  	"sigs.k8s.io/cluster-api/util/patch"
    46  	"sigs.k8s.io/cluster-api/util/predicates"
    47  )
    48  
    49  var (
    50  	// machineDeploymentKind contains the schema.GroupVersionKind for the MachineDeployment type.
    51  	machineDeploymentKind = clusterv1.GroupVersion.WithKind("MachineDeployment")
    52  )
    53  
    54  // machineDeploymentManagerName is the manager name used for Server-Side-Apply (SSA) operations
    55  // in the MachineDeployment controller.
    56  const machineDeploymentManagerName = "capi-machinedeployment"
    57  
    58  // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;patch
    59  // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch
    60  // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch;create;update;patch;delete
    61  // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io;bootstrap.cluster.x-k8s.io,resources=*,verbs=get;list;watch;create;update;patch;delete
    62  // +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments;machinedeployments/status;machinedeployments/finalizers,verbs=get;list;watch;create;update;patch;delete
    63  
    64  // Reconciler reconciles a MachineDeployment object.
    65  type Reconciler struct {
    66  	Client                    client.Client
    67  	UnstructuredCachingClient client.Client
    68  	APIReader                 client.Reader
    69  
    70  	// WatchFilterValue is the label value used to filter events prior to reconciliation.
    71  	WatchFilterValue string
    72  
    73  	recorder record.EventRecorder
    74  	ssaCache ssa.Cache
    75  }
    76  
    77  func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
    78  	clusterToMachineDeployments, err := util.ClusterToTypedObjectsMapper(mgr.GetClient(), &clusterv1.MachineDeploymentList{}, mgr.GetScheme())
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	err = ctrl.NewControllerManagedBy(mgr).
    84  		For(&clusterv1.MachineDeployment{}).
    85  		Owns(&clusterv1.MachineSet{}).
    86  		Watches(
    87  			&clusterv1.MachineSet{},
    88  			handler.EnqueueRequestsFromMapFunc(r.MachineSetToDeployments),
    89  		).
    90  		WithOptions(options).
    91  		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
    92  		Watches(
    93  			&clusterv1.Cluster{},
    94  			handler.EnqueueRequestsFromMapFunc(clusterToMachineDeployments),
    95  			builder.WithPredicates(
    96  				// TODO: should this wait for Cluster.Status.InfrastructureReady similar to Infra Machine resources?
    97  				predicates.All(ctrl.LoggerFrom(ctx),
    98  					predicates.ClusterUnpaused(ctrl.LoggerFrom(ctx)),
    99  				),
   100  			),
   101  		).Complete(r)
   102  	if err != nil {
   103  		return errors.Wrap(err, "failed setting up with a controller manager")
   104  	}
   105  
   106  	r.recorder = mgr.GetEventRecorderFor("machinedeployment-controller")
   107  	r.ssaCache = ssa.NewCache()
   108  	return nil
   109  }
   110  
   111  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
   112  	log := ctrl.LoggerFrom(ctx)
   113  
   114  	// Fetch the MachineDeployment instance.
   115  	deployment := &clusterv1.MachineDeployment{}
   116  	if err := r.Client.Get(ctx, req.NamespacedName, deployment); err != nil {
   117  		if apierrors.IsNotFound(err) {
   118  			// Object not found, return.  Created objects are automatically garbage collected.
   119  			// For additional cleanup logic use finalizers.
   120  			return ctrl.Result{}, nil
   121  		}
   122  		// Error reading the object - requeue the request.
   123  		return ctrl.Result{}, err
   124  	}
   125  
   126  	log = log.WithValues("Cluster", klog.KRef(deployment.Namespace, deployment.Spec.ClusterName))
   127  	ctx = ctrl.LoggerInto(ctx, log)
   128  
   129  	cluster, err := util.GetClusterByName(ctx, r.Client, deployment.Namespace, deployment.Spec.ClusterName)
   130  	if err != nil {
   131  		return ctrl.Result{}, err
   132  	}
   133  
   134  	// Return early if the object or Cluster is paused.
   135  	if annotations.IsPaused(cluster, deployment) {
   136  		log.Info("Reconciliation is paused for this object")
   137  		return ctrl.Result{}, nil
   138  	}
   139  
   140  	// Initialize the patch helper
   141  	patchHelper, err := patch.NewHelper(deployment, r.Client)
   142  	if err != nil {
   143  		return ctrl.Result{}, err
   144  	}
   145  
   146  	defer func() {
   147  		// Always attempt to patch the object and status after each reconciliation.
   148  		// Patch ObservedGeneration only if the reconciliation completed successfully
   149  		patchOpts := []patch.Option{}
   150  		if reterr == nil {
   151  			patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
   152  		}
   153  		if err := patchMachineDeployment(ctx, patchHelper, deployment, patchOpts...); err != nil {
   154  			reterr = kerrors.NewAggregate([]error{reterr, err})
   155  		}
   156  	}()
   157  
   158  	// Ignore deleted MachineDeployments, this can happen when foregroundDeletion
   159  	// is enabled
   160  	if !deployment.DeletionTimestamp.IsZero() {
   161  		return ctrl.Result{}, nil
   162  	}
   163  
   164  	err = r.reconcile(ctx, cluster, deployment)
   165  	if err != nil {
   166  		r.recorder.Eventf(deployment, corev1.EventTypeWarning, "ReconcileError", "%v", err)
   167  	}
   168  	return ctrl.Result{}, err
   169  }
   170  
   171  func patchMachineDeployment(ctx context.Context, patchHelper *patch.Helper, md *clusterv1.MachineDeployment, options ...patch.Option) error {
   172  	// Always update the readyCondition by summarizing the state of other conditions.
   173  	conditions.SetSummary(md,
   174  		conditions.WithConditions(
   175  			clusterv1.MachineDeploymentAvailableCondition,
   176  		),
   177  	)
   178  
   179  	// Patch the object, ignoring conflicts on the conditions owned by this controller.
   180  	options = append(options,
   181  		patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{
   182  			clusterv1.ReadyCondition,
   183  			clusterv1.MachineDeploymentAvailableCondition,
   184  		}},
   185  	)
   186  	return patchHelper.Patch(ctx, md, options...)
   187  }
   188  
   189  func (r *Reconciler) reconcile(ctx context.Context, cluster *clusterv1.Cluster, md *clusterv1.MachineDeployment) error {
   190  	log := ctrl.LoggerFrom(ctx)
   191  	log.V(4).Info("Reconcile MachineDeployment")
   192  
   193  	// Reconcile and retrieve the Cluster object.
   194  	if md.Labels == nil {
   195  		md.Labels = make(map[string]string)
   196  	}
   197  	if md.Spec.Selector.MatchLabels == nil {
   198  		md.Spec.Selector.MatchLabels = make(map[string]string)
   199  	}
   200  	if md.Spec.Template.Labels == nil {
   201  		md.Spec.Template.Labels = make(map[string]string)
   202  	}
   203  
   204  	md.Labels[clusterv1.ClusterNameLabel] = md.Spec.ClusterName
   205  
   206  	// Ensure the MachineDeployment is owned by the Cluster.
   207  	md.SetOwnerReferences(util.EnsureOwnerRef(md.GetOwnerReferences(), metav1.OwnerReference{
   208  		APIVersion: clusterv1.GroupVersion.String(),
   209  		Kind:       "Cluster",
   210  		Name:       cluster.Name,
   211  		UID:        cluster.UID,
   212  	}))
   213  
   214  	// Make sure to reconcile the external infrastructure reference.
   215  	if err := reconcileExternalTemplateReference(ctx, r.UnstructuredCachingClient, cluster, &md.Spec.Template.Spec.InfrastructureRef); err != nil {
   216  		return err
   217  	}
   218  	// Make sure to reconcile the external bootstrap reference, if any.
   219  	if md.Spec.Template.Spec.Bootstrap.ConfigRef != nil {
   220  		if err := reconcileExternalTemplateReference(ctx, r.UnstructuredCachingClient, cluster, md.Spec.Template.Spec.Bootstrap.ConfigRef); err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	msList, err := r.getMachineSetsForDeployment(ctx, md)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	// If not already present, add a label specifying the MachineDeployment name to MachineSets.
   231  	// Ensure all required labels exist on the controlled MachineSets.
   232  	// This logic is needed to add the `cluster.x-k8s.io/deployment-name` label to MachineSets
   233  	// which were created before the `cluster.x-k8s.io/deployment-name` label was added
   234  	// to all MachineSets created by a MachineDeployment or if a user manually removed the label.
   235  	for idx := range msList {
   236  		machineSet := msList[idx]
   237  		if name, ok := machineSet.Labels[clusterv1.MachineDeploymentNameLabel]; ok && name == md.Name {
   238  			continue
   239  		}
   240  
   241  		helper, err := patch.NewHelper(machineSet, r.Client)
   242  		if err != nil {
   243  			return errors.Wrapf(err, "failed to apply %s label to MachineSet %q", clusterv1.MachineDeploymentNameLabel, machineSet.Name)
   244  		}
   245  		machineSet.Labels[clusterv1.MachineDeploymentNameLabel] = md.Name
   246  		if err := helper.Patch(ctx, machineSet); err != nil {
   247  			return errors.Wrapf(err, "failed to apply %s label to MachineSet %q", clusterv1.MachineDeploymentNameLabel, machineSet.Name)
   248  		}
   249  	}
   250  
   251  	// Loop over all MachineSets and cleanup managed fields.
   252  	// We do this so that MachineSets that were created/patched before (< v1.4.0) the controller adopted
   253  	// Server-Side-Apply (SSA) can also work with SSA. Otherwise, fields would be co-owned by our "old" "manager" and
   254  	// "capi-machinedeployment" and then we would not be able to e.g. drop labels and annotations.
   255  	// Note: We are cleaning up managed fields for all MachineSets, so we're able to remove this code in a few
   256  	// Cluster API releases. If we do this only for selected MachineSets, we would have to keep this code forever.
   257  	for idx := range msList {
   258  		machineSet := msList[idx]
   259  		if err := ssa.CleanUpManagedFieldsForSSAAdoption(ctx, r.Client, machineSet, machineDeploymentManagerName); err != nil {
   260  			return errors.Wrapf(err, "failed to clean up managedFields of MachineSet %s", klog.KObj(machineSet))
   261  		}
   262  	}
   263  
   264  	if md.Spec.Paused {
   265  		return r.sync(ctx, md, msList)
   266  	}
   267  
   268  	if md.Spec.Strategy == nil {
   269  		return errors.Errorf("missing MachineDeployment strategy")
   270  	}
   271  
   272  	if md.Spec.Strategy.Type == clusterv1.RollingUpdateMachineDeploymentStrategyType {
   273  		if md.Spec.Strategy.RollingUpdate == nil {
   274  			return errors.Errorf("missing MachineDeployment settings for strategy type: %s", md.Spec.Strategy.Type)
   275  		}
   276  		return r.rolloutRolling(ctx, md, msList)
   277  	}
   278  
   279  	if md.Spec.Strategy.Type == clusterv1.OnDeleteMachineDeploymentStrategyType {
   280  		return r.rolloutOnDelete(ctx, md, msList)
   281  	}
   282  
   283  	return errors.Errorf("unexpected deployment strategy type: %s", md.Spec.Strategy.Type)
   284  }
   285  
   286  // getMachineSetsForDeployment returns a list of MachineSets associated with a MachineDeployment.
   287  func (r *Reconciler) getMachineSetsForDeployment(ctx context.Context, md *clusterv1.MachineDeployment) ([]*clusterv1.MachineSet, error) {
   288  	log := ctrl.LoggerFrom(ctx)
   289  
   290  	// List all MachineSets to find those we own but that no longer match our selector.
   291  	machineSets := &clusterv1.MachineSetList{}
   292  	if err := r.Client.List(ctx, machineSets, client.InNamespace(md.Namespace)); err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	filtered := make([]*clusterv1.MachineSet, 0, len(machineSets.Items))
   297  	for idx := range machineSets.Items {
   298  		ms := &machineSets.Items[idx]
   299  		log.WithValues("MachineSet", klog.KObj(ms))
   300  		selector, err := metav1.LabelSelectorAsSelector(&md.Spec.Selector)
   301  		if err != nil {
   302  			log.Error(err, "Skipping MachineSet, failed to get label selector from spec selector")
   303  			continue
   304  		}
   305  
   306  		// If a MachineDeployment with a nil or empty selector creeps in, it should match nothing, not everything.
   307  		if selector.Empty() {
   308  			log.Info("Skipping MachineSet as the selector is empty")
   309  			continue
   310  		}
   311  
   312  		// Skip this MachineSet unless either selector matches or it has a controller ref pointing to this MachineDeployment
   313  		if !selector.Matches(labels.Set(ms.Labels)) && !metav1.IsControlledBy(ms, md) {
   314  			log.V(4).Info("Skipping MachineSet, label mismatch")
   315  			continue
   316  		}
   317  
   318  		// Attempt to adopt MachineSet if it meets previous conditions and it has no controller references.
   319  		if metav1.GetControllerOf(ms) == nil {
   320  			if err := r.adoptOrphan(ctx, md, ms); err != nil {
   321  				log.Error(err, "Failed to adopt MachineSet into MachineDeployment")
   322  				r.recorder.Eventf(md, corev1.EventTypeWarning, "FailedAdopt", "Failed to adopt MachineSet %q: %v", ms.Name, err)
   323  				continue
   324  			}
   325  			log.Info("Adopted MachineSet into MachineDeployment")
   326  			r.recorder.Eventf(md, corev1.EventTypeNormal, "SuccessfulAdopt", "Adopted MachineSet %q", ms.Name)
   327  		}
   328  
   329  		if !metav1.IsControlledBy(ms, md) {
   330  			continue
   331  		}
   332  
   333  		filtered = append(filtered, ms)
   334  	}
   335  
   336  	return filtered, nil
   337  }
   338  
   339  // adoptOrphan sets the MachineDeployment as a controller OwnerReference to the MachineSet.
   340  func (r *Reconciler) adoptOrphan(ctx context.Context, deployment *clusterv1.MachineDeployment, machineSet *clusterv1.MachineSet) error {
   341  	patch := client.MergeFrom(machineSet.DeepCopy())
   342  	newRef := *metav1.NewControllerRef(deployment, machineDeploymentKind)
   343  	machineSet.SetOwnerReferences(util.EnsureOwnerRef(machineSet.GetOwnerReferences(), newRef))
   344  	return r.Client.Patch(ctx, machineSet, patch)
   345  }
   346  
   347  // getMachineDeploymentsForMachineSet returns a list of MachineDeployments that could potentially match a MachineSet.
   348  func (r *Reconciler) getMachineDeploymentsForMachineSet(ctx context.Context, ms *clusterv1.MachineSet) []*clusterv1.MachineDeployment {
   349  	log := ctrl.LoggerFrom(ctx)
   350  
   351  	if len(ms.Labels) == 0 {
   352  		log.V(2).Info("No MachineDeployments found for MachineSet because it has no labels", "MachineSet", klog.KObj(ms))
   353  		return nil
   354  	}
   355  
   356  	dList := &clusterv1.MachineDeploymentList{}
   357  	if err := r.Client.List(ctx, dList, client.InNamespace(ms.Namespace)); err != nil {
   358  		log.Error(err, "Failed to list MachineDeployments")
   359  		return nil
   360  	}
   361  
   362  	deployments := make([]*clusterv1.MachineDeployment, 0, len(dList.Items))
   363  	for idx := range dList.Items {
   364  		selector, err := metav1.LabelSelectorAsSelector(&dList.Items[idx].Spec.Selector)
   365  		if err != nil {
   366  			continue
   367  		}
   368  
   369  		// If a deployment with a nil or empty selector creeps in, it should match nothing, not everything.
   370  		if selector.Empty() || !selector.Matches(labels.Set(ms.Labels)) {
   371  			continue
   372  		}
   373  
   374  		deployments = append(deployments, &dList.Items[idx])
   375  	}
   376  
   377  	return deployments
   378  }
   379  
   380  // MachineSetToDeployments is a handler.ToRequestsFunc to be used to enqueue requests for reconciliation
   381  // for MachineDeployments that might adopt an orphaned MachineSet.
   382  func (r *Reconciler) MachineSetToDeployments(ctx context.Context, o client.Object) []ctrl.Request {
   383  	result := []ctrl.Request{}
   384  
   385  	ms, ok := o.(*clusterv1.MachineSet)
   386  	if !ok {
   387  		panic(fmt.Sprintf("Expected a MachineSet but got a %T", o))
   388  	}
   389  
   390  	// Check if the controller reference is already set and
   391  	// return an empty result when one is found.
   392  	for _, ref := range ms.ObjectMeta.GetOwnerReferences() {
   393  		if ref.Controller != nil && *ref.Controller {
   394  			return result
   395  		}
   396  	}
   397  
   398  	mds := r.getMachineDeploymentsForMachineSet(ctx, ms)
   399  	if len(mds) == 0 {
   400  		return nil
   401  	}
   402  
   403  	for _, md := range mds {
   404  		name := client.ObjectKey{Namespace: md.Namespace, Name: md.Name}
   405  		result = append(result, ctrl.Request{NamespacedName: name})
   406  	}
   407  
   408  	return result
   409  }
   410  
   411  func reconcileExternalTemplateReference(ctx context.Context, c client.Client, cluster *clusterv1.Cluster, ref *corev1.ObjectReference) error {
   412  	if !strings.HasSuffix(ref.Kind, clusterv1.TemplateSuffix) {
   413  		return nil
   414  	}
   415  
   416  	if err := utilconversion.UpdateReferenceAPIContract(ctx, c, ref); err != nil {
   417  		return err
   418  	}
   419  
   420  	obj, err := external.Get(ctx, c, ref, cluster.Namespace)
   421  	if err != nil {
   422  		return err
   423  	}
   424  
   425  	patchHelper, err := patch.NewHelper(obj, c)
   426  	if err != nil {
   427  		return err
   428  	}
   429  
   430  	obj.SetOwnerReferences(util.EnsureOwnerRef(obj.GetOwnerReferences(), metav1.OwnerReference{
   431  		APIVersion: clusterv1.GroupVersion.String(),
   432  		Kind:       "Cluster",
   433  		Name:       cluster.Name,
   434  		UID:        cluster.UID,
   435  	}))
   436  
   437  	return patchHelper.Patch(ctx, obj)
   438  }