sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/machinedeployment/mdutil/util.go (about) 1 /* 2 Copyright 2018 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 mdutil implements MachineDeployment utilities. 18 package mdutil 19 20 import ( 21 "fmt" 22 "sort" 23 "strconv" 24 "strings" 25 26 "github.com/go-logr/logr" 27 "github.com/pkg/errors" 28 corev1 "k8s.io/api/core/v1" 29 apiequality "k8s.io/apimachinery/pkg/api/equality" 30 "k8s.io/apimachinery/pkg/api/meta" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 intstrutil "k8s.io/apimachinery/pkg/util/intstr" 34 "k8s.io/klog/v2" 35 "k8s.io/utils/integer" 36 37 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 38 "sigs.k8s.io/cluster-api/util/conversion" 39 ) 40 41 // MachineSetsByDecreasingReplicas sorts the list of MachineSets in decreasing order of replicas, 42 // using creation time (ascending order) and name (alphabetical) as tie breakers. 43 type MachineSetsByDecreasingReplicas []*clusterv1.MachineSet 44 45 func (o MachineSetsByDecreasingReplicas) Len() int { return len(o) } 46 func (o MachineSetsByDecreasingReplicas) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 47 func (o MachineSetsByDecreasingReplicas) Less(i, j int) bool { 48 if o[i].Spec.Replicas == nil { 49 return false 50 } 51 if o[j].Spec.Replicas == nil { 52 return true 53 } 54 if *o[i].Spec.Replicas == *o[j].Spec.Replicas { 55 if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) { 56 return o[i].Name < o[j].Name 57 } 58 return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) 59 } 60 return *o[i].Spec.Replicas > *o[j].Spec.Replicas 61 } 62 63 // MachineSetsByCreationTimestamp sorts a list of MachineSet by creation timestamp, using their names as a tie breaker. 64 type MachineSetsByCreationTimestamp []*clusterv1.MachineSet 65 66 func (o MachineSetsByCreationTimestamp) Len() int { return len(o) } 67 func (o MachineSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 68 func (o MachineSetsByCreationTimestamp) Less(i, j int) bool { 69 if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) { 70 return o[i].Name < o[j].Name 71 } 72 return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) 73 } 74 75 // MachineSetsBySizeOlder sorts a list of MachineSet by size in descending order, using their creation timestamp or name as a tie breaker. 76 // By using the creation timestamp, this sorts from old to new machine sets. 77 type MachineSetsBySizeOlder []*clusterv1.MachineSet 78 79 func (o MachineSetsBySizeOlder) Len() int { return len(o) } 80 func (o MachineSetsBySizeOlder) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 81 func (o MachineSetsBySizeOlder) Less(i, j int) bool { 82 if *(o[i].Spec.Replicas) == *(o[j].Spec.Replicas) { 83 return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) 84 } 85 return *(o[i].Spec.Replicas) > *(o[j].Spec.Replicas) 86 } 87 88 // MachineSetsBySizeNewer sorts a list of MachineSet by size in descending order, using their creation timestamp or name as a tie breaker. 89 // By using the creation timestamp, this sorts from new to old machine sets. 90 type MachineSetsBySizeNewer []*clusterv1.MachineSet 91 92 func (o MachineSetsBySizeNewer) Len() int { return len(o) } 93 func (o MachineSetsBySizeNewer) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 94 func (o MachineSetsBySizeNewer) Less(i, j int) bool { 95 if *(o[i].Spec.Replicas) == *(o[j].Spec.Replicas) { 96 return o[j].CreationTimestamp.Before(&o[i].CreationTimestamp) 97 } 98 return *(o[i].Spec.Replicas) > *(o[j].Spec.Replicas) 99 } 100 101 // SetDeploymentRevision updates the revision for a deployment. 102 func SetDeploymentRevision(deployment *clusterv1.MachineDeployment, revision string) bool { 103 updated := false 104 105 if deployment.Annotations == nil { 106 deployment.Annotations = make(map[string]string) 107 } 108 if deployment.Annotations[clusterv1.RevisionAnnotation] != revision { 109 deployment.Annotations[clusterv1.RevisionAnnotation] = revision 110 updated = true 111 } 112 113 return updated 114 } 115 116 // MaxRevision finds the highest revision in the machine sets. 117 func MaxRevision(allMSs []*clusterv1.MachineSet, logger logr.Logger) int64 { 118 max := int64(0) 119 for _, ms := range allMSs { 120 if v, err := Revision(ms); err != nil { 121 // Skip the machine sets when it failed to parse their revision information 122 logger.Error(err, "Couldn't parse revision for machine set, deployment controller will skip it when reconciling revisions", 123 "machineset", ms.Name) 124 } else if v > max { 125 max = v 126 } 127 } 128 return max 129 } 130 131 // Revision returns the revision number of the input object. 132 func Revision(obj runtime.Object) (int64, error) { 133 acc, err := meta.Accessor(obj) 134 if err != nil { 135 return 0, err 136 } 137 v, ok := acc.GetAnnotations()[clusterv1.RevisionAnnotation] 138 if !ok { 139 return 0, nil 140 } 141 return strconv.ParseInt(v, 10, 64) 142 } 143 144 var annotationsToSkip = map[string]bool{ 145 corev1.LastAppliedConfigAnnotation: true, 146 clusterv1.RevisionAnnotation: true, 147 clusterv1.RevisionHistoryAnnotation: true, 148 clusterv1.DesiredReplicasAnnotation: true, 149 clusterv1.MaxReplicasAnnotation: true, 150 151 // Exclude the conversion annotation, to avoid infinite loops between the conversion webhook 152 // and the MachineDeployment controller syncing the annotations between a MachineDeployment 153 // and its linked MachineSets. 154 // 155 // See https://github.com/kubernetes-sigs/cluster-api/pull/3010#issue-413767831 for more details. 156 conversion.DataAnnotation: true, 157 } 158 159 // skipCopyAnnotation returns true if we should skip copying the annotation with the given annotation key 160 // TODO(tbd): How to decide which annotations should / should not be copied? 161 // 162 // See https://github.com/kubernetes/kubernetes/pull/20035#issuecomment-179558615 163 func skipCopyAnnotation(key string) bool { 164 return annotationsToSkip[key] 165 } 166 167 func getMaxReplicasAnnotation(ms *clusterv1.MachineSet, logger logr.Logger) (int32, bool) { 168 return getIntFromAnnotation(ms, clusterv1.MaxReplicasAnnotation, logger) 169 } 170 171 func getIntFromAnnotation(ms *clusterv1.MachineSet, annotationKey string, logger logr.Logger) (int32, bool) { 172 logger = logger.WithValues("MachineSet", klog.KObj(ms)) 173 174 annotationValue, ok := ms.Annotations[annotationKey] 175 if !ok { 176 return int32(0), false 177 } 178 intValue, err := strconv.ParseInt(annotationValue, 10, 32) 179 if err != nil { 180 logger.V(2).Info(fmt.Sprintf("Cannot convert annotation %q with value %q to integer", annotationKey, annotationValue)) 181 return int32(0), false 182 } 183 return int32(intValue), true 184 } 185 186 // ComputeMachineSetAnnotations computes the annotations that should be set on the MachineSet. 187 // Note: The passed in newMS is nil if the new MachineSet doesn't exist in the apiserver yet. 188 func ComputeMachineSetAnnotations(log logr.Logger, deployment *clusterv1.MachineDeployment, oldMSs []*clusterv1.MachineSet, newMS *clusterv1.MachineSet) (map[string]string, error) { 189 // Copy annotations from Deployment annotations while filtering out some annotations 190 // that we don't want to propagate. 191 annotations := map[string]string{} 192 for k, v := range deployment.Annotations { 193 if skipCopyAnnotation(k) { 194 continue 195 } 196 annotations[k] = v 197 } 198 199 // The newMS's revision should be the greatest among all MSes. Usually, its revision number is newRevision (the max revision number 200 // of all old MSes + 1). However, it's possible that some old MSes are deleted after the newMS revision being updated, and 201 // newRevision becomes smaller than newMS's revision. We will never decrease a revision of a MachineSet. 202 maxOldRevision := MaxRevision(oldMSs, log) 203 newRevisionInt := maxOldRevision + 1 204 newRevision := strconv.FormatInt(newRevisionInt, 10) 205 if newMS != nil { 206 currentRevision, currentRevisionExists := newMS.Annotations[clusterv1.RevisionAnnotation] 207 if currentRevisionExists { 208 currentRevisionInt, err := strconv.ParseInt(currentRevision, 10, 64) 209 if err != nil { 210 return nil, errors.Wrapf(err, "failed to parse current revision on MachineSet %s", klog.KObj(newMS)) 211 } 212 if newRevisionInt < currentRevisionInt { 213 newRevision = currentRevision 214 } 215 } 216 217 // Ensure we preserve the revision history annotation in any case if it already exists. 218 // Note: With Server-Side-Apply not setting the annotation would drop it. 219 revisionHistory, revisionHistoryExists := newMS.Annotations[clusterv1.RevisionHistoryAnnotation] 220 if revisionHistoryExists { 221 annotations[clusterv1.RevisionHistoryAnnotation] = revisionHistory 222 } 223 224 // If the revision changes then add the old revision to the revision history annotation 225 if currentRevisionExists && currentRevision != newRevision { 226 oldRevisions := strings.Split(revisionHistory, ",") 227 if oldRevisions[0] == "" { 228 annotations[clusterv1.RevisionHistoryAnnotation] = currentRevision 229 } else { 230 annotations[clusterv1.RevisionHistoryAnnotation] = strings.Join(append(oldRevisions, currentRevision), ",") 231 } 232 } 233 } 234 235 annotations[clusterv1.RevisionAnnotation] = newRevision 236 annotations[clusterv1.DesiredReplicasAnnotation] = fmt.Sprintf("%d", *deployment.Spec.Replicas) 237 annotations[clusterv1.MaxReplicasAnnotation] = fmt.Sprintf("%d", *(deployment.Spec.Replicas)+MaxSurge(*deployment)) 238 return annotations, nil 239 } 240 241 // FindOneActiveOrLatest returns the only active or the latest machine set in case there is at most one active 242 // machine set. If there are more than one active machine sets, return nil so machine sets can be scaled down 243 // to the point where there is only one active machine set. 244 func FindOneActiveOrLatest(newMS *clusterv1.MachineSet, oldMSs []*clusterv1.MachineSet) *clusterv1.MachineSet { 245 if newMS == nil && len(oldMSs) == 0 { 246 return nil 247 } 248 249 sort.Sort(sort.Reverse(MachineSetsByCreationTimestamp(oldMSs))) 250 allMSs := FilterActiveMachineSets(append(oldMSs, newMS)) 251 252 switch len(allMSs) { 253 case 0: 254 // If there is no active machine set then we should return the newest. 255 if newMS != nil { 256 return newMS 257 } 258 return oldMSs[0] 259 case 1: 260 return allMSs[0] 261 default: 262 return nil 263 } 264 } 265 266 // SetReplicasAnnotations sets the desiredReplicas and maxReplicas into the annotations. 267 func SetReplicasAnnotations(ms *clusterv1.MachineSet, desiredReplicas, maxReplicas int32) bool { 268 updated := false 269 if ms.Annotations == nil { 270 ms.Annotations = make(map[string]string) 271 } 272 desiredString := fmt.Sprintf("%d", desiredReplicas) 273 if hasString := ms.Annotations[clusterv1.DesiredReplicasAnnotation]; hasString != desiredString { 274 ms.Annotations[clusterv1.DesiredReplicasAnnotation] = desiredString 275 updated = true 276 } 277 if hasString := ms.Annotations[clusterv1.MaxReplicasAnnotation]; hasString != fmt.Sprintf("%d", maxReplicas) { 278 ms.Annotations[clusterv1.MaxReplicasAnnotation] = fmt.Sprintf("%d", maxReplicas) 279 updated = true 280 } 281 return updated 282 } 283 284 // ReplicasAnnotationsNeedUpdate return true if the replicas annotation needs to be updated. 285 func ReplicasAnnotationsNeedUpdate(ms *clusterv1.MachineSet, desiredReplicas, maxReplicas int32) bool { 286 if ms.Annotations == nil { 287 return true 288 } 289 desiredString := fmt.Sprintf("%d", desiredReplicas) 290 if hasString := ms.Annotations[clusterv1.DesiredReplicasAnnotation]; hasString != desiredString { 291 return true 292 } 293 if hasString := ms.Annotations[clusterv1.MaxReplicasAnnotation]; hasString != fmt.Sprintf("%d", maxReplicas) { 294 return true 295 } 296 return false 297 } 298 299 // MaxUnavailable returns the maximum unavailable machines a rolling deployment can take. 300 func MaxUnavailable(deployment clusterv1.MachineDeployment) int32 { 301 if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 { 302 return int32(0) 303 } 304 // Error caught by validation 305 _, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas)) 306 if maxUnavailable > *deployment.Spec.Replicas { 307 return *deployment.Spec.Replicas 308 } 309 return maxUnavailable 310 } 311 312 // MaxSurge returns the maximum surge machines a rolling deployment can take. 313 func MaxSurge(deployment clusterv1.MachineDeployment) int32 { 314 if !IsRollingUpdate(&deployment) { 315 return int32(0) 316 } 317 // Error caught by validation 318 maxSurge, _, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas)) 319 return maxSurge 320 } 321 322 // GetProportion will estimate the proportion for the provided machine set using 1. the current size 323 // of the parent deployment, 2. the replica count that needs be added on the machine sets of the 324 // deployment, and 3. the total replicas added in the machine sets of the deployment so far. 325 func GetProportion(ms *clusterv1.MachineSet, md clusterv1.MachineDeployment, deploymentReplicasToAdd, deploymentReplicasAdded int32, logger logr.Logger) int32 { 326 if ms == nil || *(ms.Spec.Replicas) == 0 || deploymentReplicasToAdd == 0 || deploymentReplicasToAdd == deploymentReplicasAdded { 327 return int32(0) 328 } 329 330 msFraction := getMachineSetFraction(*ms, md, logger) 331 allowed := deploymentReplicasToAdd - deploymentReplicasAdded 332 333 if deploymentReplicasToAdd > 0 { 334 // Use the minimum between the machine set fraction and the maximum allowed replicas 335 // when scaling up. This way we ensure we will not scale up more than the allowed 336 // replicas we can add. 337 return integer.Int32Min(msFraction, allowed) 338 } 339 // Use the maximum between the machine set fraction and the maximum allowed replicas 340 // when scaling down. This way we ensure we will not scale down more than the allowed 341 // replicas we can remove. 342 return integer.Int32Max(msFraction, allowed) 343 } 344 345 // getMachineSetFraction estimates the fraction of replicas a machine set can have in 346 // 1. a scaling event during a rollout or 2. when scaling a paused deployment. 347 func getMachineSetFraction(ms clusterv1.MachineSet, md clusterv1.MachineDeployment, logger logr.Logger) int32 { 348 // If we are scaling down to zero then the fraction of this machine set is its whole size (negative) 349 if *(md.Spec.Replicas) == int32(0) { 350 return -*(ms.Spec.Replicas) 351 } 352 353 deploymentReplicas := *(md.Spec.Replicas) + MaxSurge(md) 354 annotatedReplicas, ok := getMaxReplicasAnnotation(&ms, logger) 355 if !ok { 356 // If we cannot find the annotation then fallback to the current deployment size. Note that this 357 // will not be an accurate proportion estimation in case other machine sets have different values 358 // which means that the deployment was scaled at some point but we at least will stay in limits 359 // due to the min-max comparisons in getProportion. 360 annotatedReplicas = md.Status.Replicas 361 } 362 363 // We should never proportionally scale up from zero which means ms.spec.replicas and annotatedReplicas 364 // will never be zero here. 365 newMSsize := (float64(*(ms.Spec.Replicas) * deploymentReplicas)) / float64(annotatedReplicas) 366 return integer.RoundToInt32(newMSsize) - *(ms.Spec.Replicas) 367 } 368 369 // EqualMachineTemplate returns true if two given machineTemplateSpec are equal, 370 // ignoring all the in-place propagated fields, and the version from external references. 371 func EqualMachineTemplate(template1, template2 *clusterv1.MachineTemplateSpec) bool { 372 t1Copy := MachineTemplateDeepCopyRolloutFields(template1) 373 t2Copy := MachineTemplateDeepCopyRolloutFields(template2) 374 375 return apiequality.Semantic.DeepEqual(t1Copy, t2Copy) 376 } 377 378 // MachineTemplateDeepCopyRolloutFields copies a MachineTemplateSpec 379 // and sets all fields that should be propagated in-place to nil and drops version from 380 // external references. 381 func MachineTemplateDeepCopyRolloutFields(template *clusterv1.MachineTemplateSpec) *clusterv1.MachineTemplateSpec { 382 templateCopy := template.DeepCopy() 383 384 // Drop labels and annotations 385 templateCopy.Labels = nil 386 templateCopy.Annotations = nil 387 388 // Drop node timeout values 389 templateCopy.Spec.NodeDrainTimeout = nil 390 templateCopy.Spec.NodeDeletionTimeout = nil 391 templateCopy.Spec.NodeVolumeDetachTimeout = nil 392 393 // Remove the version part from the references APIVersion field, 394 // for more details see issue #2183 and #2140. 395 templateCopy.Spec.InfrastructureRef.APIVersion = templateCopy.Spec.InfrastructureRef.GroupVersionKind().Group 396 if templateCopy.Spec.Bootstrap.ConfigRef != nil { 397 templateCopy.Spec.Bootstrap.ConfigRef.APIVersion = templateCopy.Spec.Bootstrap.ConfigRef.GroupVersionKind().Group 398 } 399 400 return templateCopy 401 } 402 403 // FindNewMachineSet returns the new MS this given deployment targets (the one with the same machine template, ignoring 404 // in-place mutable fields). 405 // Note: If the reconciliation time is after the deployment's `rolloutAfter` time, a MS has to be newer than 406 // `rolloutAfter` to be considered as matching the deployment's intent. 407 // NOTE: If we find a matching MachineSet which only differs in in-place mutable fields we can use it to 408 // fulfill the intent of the MachineDeployment by just updating the MachineSet to propagate in-place mutable fields. 409 // Thus we don't have to create a new MachineSet and we can avoid an unnecessary rollout. 410 // NOTE: Even after we changed EqualMachineTemplate to ignore fields that are propagated in-place we can guarantee that if there exists a "new machineset" 411 // using the old logic then a new machineset will definitely exist using the new logic. The new logic is looser. Therefore, we will 412 // not face a case where there exists a machine set matching the old logic but there does not exist a machineset matching the new logic. 413 // In fact previously not matching MS can now start matching the target. Since there could be multiple matches, lets choose the 414 // MS with the most replicas so that there is minimum machine churn. 415 func FindNewMachineSet(deployment *clusterv1.MachineDeployment, msList []*clusterv1.MachineSet, reconciliationTime *metav1.Time) *clusterv1.MachineSet { 416 sort.Sort(MachineSetsByDecreasingReplicas(msList)) 417 for i := range msList { 418 if EqualMachineTemplate(&msList[i].Spec.Template, &deployment.Spec.Template) && 419 !shouldRolloutAfter(msList[i], reconciliationTime, deployment.Spec.RolloutAfter) { 420 // In rare cases, such as after cluster upgrades, Deployment may end up with 421 // having more than one new MachineSets that have the same template, 422 // see https://github.com/kubernetes/kubernetes/issues/40415 423 // We deterministically choose the oldest new MachineSet with matching template hash. 424 return msList[i] 425 } 426 } 427 // new MachineSet does not exist. 428 return nil 429 } 430 431 func shouldRolloutAfter(ms *clusterv1.MachineSet, reconciliationTime *metav1.Time, rolloutAfter *metav1.Time) bool { 432 if ms == nil { 433 return false 434 } 435 if reconciliationTime == nil || rolloutAfter == nil { 436 return false 437 } 438 return ms.CreationTimestamp.Before(rolloutAfter) && rolloutAfter.Before(reconciliationTime) 439 } 440 441 // FindOldMachineSets returns the old machine sets targeted by the given Deployment, within the given slice of MSes. 442 // Returns a list of machine sets which contains all old machine sets. 443 func FindOldMachineSets(deployment *clusterv1.MachineDeployment, msList []*clusterv1.MachineSet, reconciliationTime *metav1.Time) []*clusterv1.MachineSet { 444 allMSs := make([]*clusterv1.MachineSet, 0, len(msList)) 445 newMS := FindNewMachineSet(deployment, msList, reconciliationTime) 446 for _, ms := range msList { 447 // Filter out new machine set 448 if newMS != nil && ms.UID == newMS.UID { 449 continue 450 } 451 allMSs = append(allMSs, ms) 452 } 453 return allMSs 454 } 455 456 // GetReplicaCountForMachineSets returns the sum of Replicas of the given machine sets. 457 func GetReplicaCountForMachineSets(machineSets []*clusterv1.MachineSet) int32 { 458 totalReplicas := int32(0) 459 for _, ms := range machineSets { 460 if ms != nil { 461 totalReplicas += *(ms.Spec.Replicas) 462 } 463 } 464 return totalReplicas 465 } 466 467 // GetActualReplicaCountForMachineSets returns the sum of actual replicas of the given machine sets. 468 func GetActualReplicaCountForMachineSets(machineSets []*clusterv1.MachineSet) int32 { 469 totalActualReplicas := int32(0) 470 for _, ms := range machineSets { 471 if ms != nil { 472 totalActualReplicas += ms.Status.Replicas 473 } 474 } 475 return totalActualReplicas 476 } 477 478 // TotalMachineSetsReplicaSum returns sum of max(ms.Spec.Replicas, ms.Status.Replicas) across all the machine sets. 479 // 480 // This is used to guarantee that the total number of machines will not exceed md.Spec.Replicas + maxSurge. 481 // Use max(spec.Replicas,status.Replicas) to cover the cases that: 482 // 1. Scale up, where spec.Replicas increased but no machine created yet, so spec.Replicas > status.Replicas 483 // 2. Scale down, where spec.Replicas decreased but machine not deleted yet, so spec.Replicas < status.Replicas. 484 func TotalMachineSetsReplicaSum(machineSets []*clusterv1.MachineSet) int32 { 485 totalReplicas := int32(0) 486 for _, ms := range machineSets { 487 if ms != nil { 488 totalReplicas += integer.Int32Max(*(ms.Spec.Replicas), ms.Status.Replicas) 489 } 490 } 491 return totalReplicas 492 } 493 494 // GetReadyReplicaCountForMachineSets returns the number of ready machines corresponding to the given machine sets. 495 func GetReadyReplicaCountForMachineSets(machineSets []*clusterv1.MachineSet) int32 { 496 totalReadyReplicas := int32(0) 497 for _, ms := range machineSets { 498 if ms != nil { 499 totalReadyReplicas += ms.Status.ReadyReplicas 500 } 501 } 502 return totalReadyReplicas 503 } 504 505 // GetAvailableReplicaCountForMachineSets returns the number of available machines corresponding to the given machine sets. 506 func GetAvailableReplicaCountForMachineSets(machineSets []*clusterv1.MachineSet) int32 { 507 totalAvailableReplicas := int32(0) 508 for _, ms := range machineSets { 509 if ms != nil { 510 totalAvailableReplicas += ms.Status.AvailableReplicas 511 } 512 } 513 return totalAvailableReplicas 514 } 515 516 // IsRollingUpdate returns true if the strategy type is a rolling update. 517 func IsRollingUpdate(deployment *clusterv1.MachineDeployment) bool { 518 return deployment.Spec.Strategy.Type == clusterv1.RollingUpdateMachineDeploymentStrategyType 519 } 520 521 // DeploymentComplete considers a deployment to be complete once all of its desired replicas 522 // are updated and available, and no old machines are running. 523 func DeploymentComplete(deployment *clusterv1.MachineDeployment, newStatus *clusterv1.MachineDeploymentStatus) bool { 524 return newStatus.UpdatedReplicas == *(deployment.Spec.Replicas) && 525 newStatus.Replicas == *(deployment.Spec.Replicas) && 526 newStatus.AvailableReplicas == *(deployment.Spec.Replicas) && 527 newStatus.ObservedGeneration >= deployment.Generation 528 } 529 530 // NewMSNewReplicas calculates the number of replicas a deployment's new MS should have. 531 // When one of the following is true, we're rolling out the deployment; otherwise, we're scaling it. 532 // 1) The new MS is saturated: newMS's replicas == deployment's replicas 533 // 2) For RollingUpdateStrategy: Max number of machines allowed is reached: deployment's replicas + maxSurge == all MSs' replicas. 534 // 3) For OnDeleteStrategy: Max number of machines allowed is reached: deployment's replicas == all MSs' replicas. 535 func NewMSNewReplicas(deployment *clusterv1.MachineDeployment, allMSs []*clusterv1.MachineSet, newMSReplicas int32) (int32, error) { 536 switch deployment.Spec.Strategy.Type { 537 case clusterv1.RollingUpdateMachineDeploymentStrategyType: 538 // Check if we can scale up. 539 maxSurge, err := intstrutil.GetScaledValueFromIntOrPercent(deployment.Spec.Strategy.RollingUpdate.MaxSurge, int(*(deployment.Spec.Replicas)), true) 540 if err != nil { 541 return 0, err 542 } 543 // Find the total number of machines 544 currentMachineCount := TotalMachineSetsReplicaSum(allMSs) 545 maxTotalMachines := *(deployment.Spec.Replicas) + int32(maxSurge) 546 if currentMachineCount >= maxTotalMachines { 547 // Cannot scale up. 548 return newMSReplicas, nil 549 } 550 // Scale up. 551 scaleUpCount := maxTotalMachines - currentMachineCount 552 // Do not exceed the number of desired replicas. 553 scaleUpCount = integer.Int32Min(scaleUpCount, *(deployment.Spec.Replicas)-newMSReplicas) 554 return newMSReplicas + scaleUpCount, nil 555 case clusterv1.OnDeleteMachineDeploymentStrategyType: 556 // Find the total number of machines 557 currentMachineCount := TotalMachineSetsReplicaSum(allMSs) 558 if currentMachineCount >= *(deployment.Spec.Replicas) { 559 // Cannot scale up as more replicas exist than desired number of replicas in the MachineDeployment. 560 return newMSReplicas, nil 561 } 562 // Scale up the latest MachineSet so the total amount of replicas across all MachineSets match 563 // the desired number of replicas in the MachineDeployment 564 scaleUpCount := *(deployment.Spec.Replicas) - currentMachineCount 565 return newMSReplicas + scaleUpCount, nil 566 default: 567 return 0, fmt.Errorf("failed to compute replicas: deployment strategy %v isn't supported", deployment.Spec.Strategy.Type) 568 } 569 } 570 571 // IsSaturated checks if the new machine set is saturated by comparing its size with its deployment size. 572 // Both the deployment and the machine set have to believe this machine set can own all of the desired 573 // replicas in the deployment and the annotation helps in achieving that. All machines of the MachineSet 574 // need to be available. 575 func IsSaturated(deployment *clusterv1.MachineDeployment, ms *clusterv1.MachineSet) bool { 576 if ms == nil { 577 return false 578 } 579 desiredString := ms.Annotations[clusterv1.DesiredReplicasAnnotation] 580 desired, err := strconv.ParseInt(desiredString, 10, 32) 581 if err != nil { 582 return false 583 } 584 return *(ms.Spec.Replicas) == *(deployment.Spec.Replicas) && 585 int32(desired) == *(deployment.Spec.Replicas) && 586 ms.Status.AvailableReplicas == *(deployment.Spec.Replicas) 587 } 588 589 // ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one 590 // step. For example: 591 // 592 // 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1) 593 // 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1) 594 // 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) 595 // 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1) 596 // 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) 597 // 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1). 598 func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) { 599 surge, err := intstrutil.GetScaledValueFromIntOrPercent(maxSurge, int(desired), true) 600 if err != nil { 601 return 0, 0, err 602 } 603 unavailable, err := intstrutil.GetScaledValueFromIntOrPercent(maxUnavailable, int(desired), false) 604 if err != nil { 605 return 0, 0, err 606 } 607 608 if surge == 0 && unavailable == 0 { 609 // Validation should never allow the user to explicitly use zero values for both maxSurge 610 // maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero. 611 // If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the 612 // theory that surge might not work due to quota. 613 unavailable = 1 614 } 615 616 return int32(surge), int32(unavailable), nil 617 } 618 619 // FilterActiveMachineSets returns machine sets that have (or at least ought to have) machines. 620 func FilterActiveMachineSets(machineSets []*clusterv1.MachineSet) []*clusterv1.MachineSet { 621 activeFilter := func(ms *clusterv1.MachineSet) bool { 622 return ms != nil && ms.Spec.Replicas != nil && *(ms.Spec.Replicas) > 0 623 } 624 return FilterMachineSets(machineSets, activeFilter) 625 } 626 627 type filterMS func(ms *clusterv1.MachineSet) bool 628 629 // FilterMachineSets returns machine sets that are filtered by filterFn (all returned ones should match filterFn). 630 func FilterMachineSets(mSes []*clusterv1.MachineSet, filterFn filterMS) []*clusterv1.MachineSet { 631 var filtered []*clusterv1.MachineSet 632 for i := range mSes { 633 if filterFn(mSes[i]) { 634 filtered = append(filtered, mSes[i]) 635 } 636 } 637 return filtered 638 } 639 640 // CloneAndAddLabel clones the given map and returns a new map with the given key and value added. 641 // Returns the given map, if labelKey is empty. 642 func CloneAndAddLabel(labels map[string]string, labelKey, labelValue string) map[string]string { 643 if labelKey == "" { 644 // Don't need to add a label. 645 return labels 646 } 647 // Clone. 648 newLabels := map[string]string{} 649 for key, value := range labels { 650 newLabels[key] = value 651 } 652 newLabels[labelKey] = labelValue 653 return newLabels 654 } 655 656 // CloneSelectorAndAddLabel clones the given selector and returns a new selector with the given key and value added. 657 // Returns the given selector, if labelKey is empty. 658 func CloneSelectorAndAddLabel(selector *metav1.LabelSelector, labelKey, labelValue string) *metav1.LabelSelector { 659 if labelKey == "" { 660 // Don't need to add a label. 661 return selector 662 } 663 664 // Clone. 665 newSelector := new(metav1.LabelSelector) 666 667 // TODO(madhusudancs): Check if you can use deepCopy_extensions_LabelSelector here. 668 newSelector.MatchLabels = make(map[string]string) 669 if selector.MatchLabels != nil { 670 for key, val := range selector.MatchLabels { 671 newSelector.MatchLabels[key] = val 672 } 673 } 674 newSelector.MatchLabels[labelKey] = labelValue 675 676 if selector.MatchExpressions != nil { 677 newMExps := make([]metav1.LabelSelectorRequirement, len(selector.MatchExpressions)) 678 for i, me := range selector.MatchExpressions { 679 newMExps[i].Key = me.Key 680 newMExps[i].Operator = me.Operator 681 if me.Values != nil { 682 newMExps[i].Values = make([]string, len(me.Values)) 683 copy(newMExps[i].Values, me.Values) 684 } else { 685 newMExps[i].Values = nil 686 } 687 } 688 newSelector.MatchExpressions = newMExps 689 } else { 690 newSelector.MatchExpressions = nil 691 } 692 693 return newSelector 694 } 695 696 // GetDeletingMachineCount gets the number of machines that are in the process of being deleted 697 // in a machineList. 698 func GetDeletingMachineCount(machineList *clusterv1.MachineList) int32 { 699 var deletingMachineCount int32 700 for _, machine := range machineList.Items { 701 if !machine.GetDeletionTimestamp().IsZero() { 702 deletingMachineCount++ 703 } 704 } 705 return deletingMachineCount 706 }