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 }