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 }