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