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