// isScalingEvent checks whether the provided deployment has been updated with a scaling event // by looking at the desired-replicas annotation in the active replica sets of the deployment. func (dc *DeploymentController) isScalingEvent(d *extensions.Deployment) (bool, error) { newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, false) if err != nil { return false, err } // If there is no new replica set matching this deployment and the deployment isn't paused // then there is a new rollout that waits to happen if newRS == nil && !d.Spec.Paused { // Update all active replicas sets to the new deployment size. SetReplicasAnnotations makes // sure that we will update only replica sets that don't have the current size of the deployment. maxSurge := deploymentutil.MaxSurge(*d) for _, rs := range controller.FilterActiveReplicaSets(oldRSs) { if updated := deploymentutil.SetReplicasAnnotations(rs, d.Spec.Replicas, d.Spec.Replicas+maxSurge); updated { if _, err := dc.client.Extensions().ReplicaSets(rs.Namespace).Update(rs); err != nil { glog.Infof("Cannot update annotations for replica set %q: %v", rs.Name, err) return false, err } } } return false, nil } allRSs := append(oldRSs, newRS) for _, rs := range controller.FilterActiveReplicaSets(allRSs) { desired, ok := deploymentutil.GetDesiredReplicasAnnotation(rs) if !ok { continue } if desired != d.Spec.Replicas { return true, nil } } return false, nil }
func TestScale(t *testing.T) { newTimestamp := metav1.Date(2016, 5, 20, 2, 0, 0, 0, time.UTC) oldTimestamp := metav1.Date(2016, 5, 20, 1, 0, 0, 0, time.UTC) olderTimestamp := metav1.Date(2016, 5, 20, 0, 0, 0, 0, time.UTC) var updatedTemplate = func(replicas int) *extensions.Deployment { d := newDeployment("foo", replicas, nil, nil, nil, map[string]string{"foo": "bar"}) d.Spec.Template.Labels["another"] = "label" return d } tests := []struct { name string deployment *extensions.Deployment oldDeployment *extensions.Deployment newRS *extensions.ReplicaSet oldRSs []*extensions.ReplicaSet expectedNew *extensions.ReplicaSet expectedOld []*extensions.ReplicaSet wasntUpdated map[string]bool desiredReplicasAnnotations map[string]int32 }{ { name: "normal scaling event: 10 -> 12", deployment: newDeployment("foo", 12, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 10, nil, nil, nil, nil), newRS: rs("foo-v1", 10, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{}, expectedNew: rs("foo-v1", 12, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{}, }, { name: "normal scaling event: 10 -> 5", deployment: newDeployment("foo", 5, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 10, nil, nil, nil, nil), newRS: rs("foo-v1", 10, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{}, expectedNew: rs("foo-v1", 5, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{}, }, { name: "proportional scaling: 5 -> 10", deployment: newDeployment("foo", 10, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 5, nil, nil, nil, nil), newRS: rs("foo-v2", 2, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v1", 3, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 4, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v1", 6, nil, oldTimestamp)}, }, { name: "proportional scaling: 5 -> 3", deployment: newDeployment("foo", 3, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 5, nil, nil, nil, nil), newRS: rs("foo-v2", 2, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v1", 3, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 1, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v1", 2, nil, oldTimestamp)}, }, { name: "proportional scaling: 9 -> 4", deployment: newDeployment("foo", 4, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 9, nil, nil, nil, nil), newRS: rs("foo-v2", 8, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v1", 1, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 4, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v1", 0, nil, oldTimestamp)}, }, { name: "proportional scaling: 7 -> 10", deployment: newDeployment("foo", 10, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 7, nil, nil, nil, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 3, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 3, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 4, nil, oldTimestamp), rs("foo-v1", 3, nil, olderTimestamp)}, }, { name: "proportional scaling: 13 -> 8", deployment: newDeployment("foo", 8, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 13, nil, nil, nil, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 8, nil, oldTimestamp), rs("foo-v1", 3, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 1, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 5, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, }, // Scales up the new replica set. { name: "leftover distribution: 3 -> 4", deployment: newDeployment("foo", 4, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 3, nil, nil, nil, nil), newRS: rs("foo-v3", 1, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 2, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, }, // Scales down the older replica set. { name: "leftover distribution: 3 -> 2", deployment: newDeployment("foo", 2, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 3, nil, nil, nil, nil), newRS: rs("foo-v3", 1, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 1, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, }, // Scales up the latest replica set first. { name: "proportional scaling (no new rs): 4 -> 5", deployment: newDeployment("foo", 5, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 4, nil, nil, nil, nil), newRS: nil, oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, expectedNew: nil, expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 3, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, }, // Scales down to zero { name: "proportional scaling: 6 -> 0", deployment: newDeployment("foo", 0, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 6, nil, nil, nil, nil), newRS: rs("foo-v3", 3, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 0, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, }, // Scales up from zero { name: "proportional scaling: 0 -> 6", deployment: newDeployment("foo", 6, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 6, nil, nil, nil, nil), newRS: rs("foo-v3", 0, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 6, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, wasntUpdated: map[string]bool{"foo-v2": true, "foo-v1": true}, }, // Scenario: deployment.spec.replicas == 3 ( foo-v1.spec.replicas == foo-v2.spec.replicas == foo-v3.spec.replicas == 1 ) // Deployment is scaled to 5. foo-v3.spec.replicas and foo-v2.spec.replicas should increment by 1 but foo-v2 fails to // update. { name: "failed rs update", deployment: newDeployment("foo", 5, nil, nil, nil, nil), oldDeployment: newDeployment("foo", 5, nil, nil, nil, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 2, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, wasntUpdated: map[string]bool{"foo-v3": true, "foo-v1": true}, desiredReplicasAnnotations: map[string]int32{"foo-v2": int32(3)}, }, { name: "deployment with surge pods", deployment: newDeployment("foo", 20, nil, maxSurge(2), nil, nil), oldDeployment: newDeployment("foo", 10, nil, maxSurge(2), nil, nil), newRS: rs("foo-v2", 6, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v1", 6, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 11, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v1", 11, nil, oldTimestamp)}, }, { name: "change both surge and size", deployment: newDeployment("foo", 50, nil, maxSurge(6), nil, nil), oldDeployment: newDeployment("foo", 10, nil, maxSurge(3), nil, nil), newRS: rs("foo-v2", 5, nil, newTimestamp), oldRSs: []*extensions.ReplicaSet{rs("foo-v1", 8, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 22, nil, newTimestamp), expectedOld: []*extensions.ReplicaSet{rs("foo-v1", 34, nil, oldTimestamp)}, }, { name: "change both size and template", deployment: updatedTemplate(14), oldDeployment: newDeployment("foo", 10, nil, nil, nil, map[string]string{"foo": "bar"}), newRS: nil, oldRSs: []*extensions.ReplicaSet{rs("foo-v2", 7, nil, newTimestamp), rs("foo-v1", 3, nil, oldTimestamp)}, expectedNew: nil, expectedOld: []*extensions.ReplicaSet{rs("foo-v2", 10, nil, newTimestamp), rs("foo-v1", 4, nil, oldTimestamp)}, }, } for _, test := range tests { _ = olderTimestamp t.Log(test.name) fake := fake.Clientset{} dc := &DeploymentController{ client: &fake, eventRecorder: &record.FakeRecorder{}, } if test.newRS != nil { desiredReplicas := *(test.oldDeployment.Spec.Replicas) if desired, ok := test.desiredReplicasAnnotations[test.newRS.Name]; ok { desiredReplicas = desired } deploymentutil.SetReplicasAnnotations(test.newRS, desiredReplicas, desiredReplicas+deploymentutil.MaxSurge(*test.oldDeployment)) } for i := range test.oldRSs { rs := test.oldRSs[i] if rs == nil { continue } desiredReplicas := *(test.oldDeployment.Spec.Replicas) if desired, ok := test.desiredReplicasAnnotations[rs.Name]; ok { desiredReplicas = desired } deploymentutil.SetReplicasAnnotations(rs, desiredReplicas, desiredReplicas+deploymentutil.MaxSurge(*test.oldDeployment)) } if err := dc.scale(test.deployment, test.newRS, test.oldRSs); err != nil { t.Errorf("%s: unexpected error: %v", test.name, err) continue } // Construct the nameToSize map that will hold all the sizes we got our of tests // Skip updating the map if the replica set wasn't updated since there will be // no update action for it. nameToSize := make(map[string]int32) if test.newRS != nil { nameToSize[test.newRS.Name] = *(test.newRS.Spec.Replicas) } for i := range test.oldRSs { rs := test.oldRSs[i] nameToSize[rs.Name] = *(rs.Spec.Replicas) } // Get all the UPDATE actions and update nameToSize with all the updated sizes. for _, action := range fake.Actions() { rs := action.(testclient.UpdateAction).GetObject().(*extensions.ReplicaSet) if !test.wasntUpdated[rs.Name] { nameToSize[rs.Name] = *(rs.Spec.Replicas) } } if test.expectedNew != nil && test.newRS != nil && *(test.expectedNew.Spec.Replicas) != nameToSize[test.newRS.Name] { t.Errorf("%s: expected new replicas: %d, got: %d", test.name, *(test.expectedNew.Spec.Replicas), nameToSize[test.newRS.Name]) continue } if len(test.expectedOld) != len(test.oldRSs) { t.Errorf("%s: expected %d old replica sets, got %d", test.name, len(test.expectedOld), len(test.oldRSs)) continue } for n := range test.oldRSs { rs := test.oldRSs[n] expected := test.expectedOld[n] if *(expected.Spec.Replicas) != nameToSize[rs.Name] { t.Errorf("%s: expected old (%s) replicas: %d, got: %d", test.name, rs.Name, *(expected.Spec.Replicas), nameToSize[rs.Name]) } } } }
func (dc *DeploymentController) scaleReplicaSet(rs *extensions.ReplicaSet, newScale int32, deployment *extensions.Deployment, scalingOperation string) (*extensions.ReplicaSet, error) { objCopy, err := api.Scheme.Copy(rs) if err != nil { return nil, err } rsCopy := objCopy.(*extensions.ReplicaSet) sizeNeedsUpdate := *(rsCopy.Spec.Replicas) != newScale annotationsNeedUpdate := deploymentutil.SetReplicasAnnotations(rsCopy, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment)) if sizeNeedsUpdate || annotationsNeedUpdate { *(rsCopy.Spec.Replicas) = newScale rs, err = dc.client.Extensions().ReplicaSets(rsCopy.Namespace).Update(rsCopy) if err == nil && sizeNeedsUpdate { dc.eventRecorder.Eventf(deployment, v1.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale) } } return rs, err }
// scale scales proportionally in order to mitigate risk. Otherwise, scaling up can increase the size // of the new replica set and scaling down can decrease the sizes of the old ones, both of which would // have the effect of hastening the rollout progress, which could produce a higher proportion of unavailable // replicas in the event of a problem with the rolled out template. Should run only on scaling events or // when a deployment is paused and not during the normal rollout process. func (dc *DeploymentController) scale(deployment *extensions.Deployment, newRS *extensions.ReplicaSet, oldRSs []*extensions.ReplicaSet) error { // If there is only one active replica set then we should scale that up to the full count of the // deployment. If there is no active replica set, then we should scale up the newest replica set. if activeOrLatest := deploymentutil.FindActiveOrLatest(newRS, oldRSs); activeOrLatest != nil { if *(activeOrLatest.Spec.Replicas) == *(deployment.Spec.Replicas) { return nil } _, _, err := dc.scaleReplicaSetAndRecordEvent(activeOrLatest, *(deployment.Spec.Replicas), deployment) return err } // If the new replica set is saturated, old replica sets should be fully scaled down. // This case handles replica set adoption during a saturated new replica set. if deploymentutil.IsSaturated(deployment, newRS) { for _, old := range controller.FilterActiveReplicaSets(oldRSs) { if _, _, err := dc.scaleReplicaSetAndRecordEvent(old, 0, deployment); err != nil { return err } } return nil } // There are old replica sets with pods and the new replica set is not saturated. // We need to proportionally scale all replica sets (new and old) in case of a // rolling deployment. if deploymentutil.IsRollingUpdate(deployment) { allRSs := controller.FilterActiveReplicaSets(append(oldRSs, newRS)) allRSsReplicas := deploymentutil.GetReplicaCountForReplicaSets(allRSs) allowedSize := int32(0) if *(deployment.Spec.Replicas) > 0 { allowedSize = *(deployment.Spec.Replicas) + deploymentutil.MaxSurge(*deployment) } // Number of additional replicas that can be either added or removed from the total // replicas count. These replicas should be distributed proportionally to the active // replica sets. deploymentReplicasToAdd := allowedSize - allRSsReplicas // The additional replicas should be distributed proportionally amongst the active // replica sets from the larger to the smaller in size replica set. Scaling direction // drives what happens in case we are trying to scale replica sets of the same size. // In such a case when scaling up, we should scale up newer replica sets first, and // when scaling down, we should scale down older replica sets first. var scalingOperation string switch { case deploymentReplicasToAdd > 0: sort.Sort(controller.ReplicaSetsBySizeNewer(allRSs)) scalingOperation = "up" case deploymentReplicasToAdd < 0: sort.Sort(controller.ReplicaSetsBySizeOlder(allRSs)) scalingOperation = "down" } // Iterate over all active replica sets and estimate proportions for each of them. // The absolute value of deploymentReplicasAdded should never exceed the absolute // value of deploymentReplicasToAdd. deploymentReplicasAdded := int32(0) nameToSize := make(map[string]int32) for i := range allRSs { rs := allRSs[i] // Estimate proportions if we have replicas to add, otherwise simply populate // nameToSize with the current sizes for each replica set. if deploymentReplicasToAdd != 0 { proportion := deploymentutil.GetProportion(rs, *deployment, deploymentReplicasToAdd, deploymentReplicasAdded) nameToSize[rs.Name] = *(rs.Spec.Replicas) + proportion deploymentReplicasAdded += proportion } else { nameToSize[rs.Name] = *(rs.Spec.Replicas) } } // Update all replica sets for i := range allRSs { rs := allRSs[i] // Add/remove any leftovers to the largest replica set. if i == 0 && deploymentReplicasToAdd != 0 { leftover := deploymentReplicasToAdd - deploymentReplicasAdded nameToSize[rs.Name] = nameToSize[rs.Name] + leftover if nameToSize[rs.Name] < 0 { nameToSize[rs.Name] = 0 } } // TODO: Use transactions when we have them. if _, err := dc.scaleReplicaSet(rs, nameToSize[rs.Name], deployment, scalingOperation); err != nil { // Return as soon as we fail, the deployment is requeued return err } } } return nil }
func (dc *DeploymentController) scaleReplicaSet(rs *extensions.ReplicaSet, newScale int32, deployment *extensions.Deployment, scalingOperation string) (bool, *extensions.ReplicaSet, error) { objCopy, err := api.Scheme.Copy(rs) if err != nil { return false, nil, err } rsCopy := objCopy.(*extensions.ReplicaSet) sizeNeedsUpdate := *(rsCopy.Spec.Replicas) != newScale // TODO: Do not mutate the replica set here, instead simply compare the annotation and if they mismatch // call SetReplicasAnnotations inside the following if clause. Then we can also move the deep-copy from // above inside the if too. annotationsNeedUpdate := deploymentutil.SetReplicasAnnotations(rsCopy, *(deployment.Spec.Replicas), *(deployment.Spec.Replicas)+deploymentutil.MaxSurge(*deployment)) scaled := false if sizeNeedsUpdate || annotationsNeedUpdate { *(rsCopy.Spec.Replicas) = newScale rs, err = dc.client.Extensions().ReplicaSets(rsCopy.Namespace).Update(rsCopy) if err == nil && sizeNeedsUpdate { scaled = true dc.eventRecorder.Eventf(deployment, v1.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale) } } return scaled, rs, err }
func TestScale(t *testing.T) { newTimestamp := unversioned.Date(2016, 5, 20, 2, 0, 0, 0, time.UTC) oldTimestamp := unversioned.Date(2016, 5, 20, 1, 0, 0, 0, time.UTC) olderTimestamp := unversioned.Date(2016, 5, 20, 0, 0, 0, 0, time.UTC) tests := []struct { name string deployment *exp.Deployment oldDeployment *exp.Deployment newRS *exp.ReplicaSet oldRSs []*exp.ReplicaSet expectedNew *exp.ReplicaSet expectedOld []*exp.ReplicaSet desiredReplicasAnnotations map[string]int32 }{ { name: "normal scaling event: 10 -> 12", deployment: newDeployment(12, nil), oldDeployment: newDeployment(10, nil), newRS: rs("foo-v1", 10, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{}, expectedNew: rs("foo-v1", 12, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{}, }, { name: "normal scaling event: 10 -> 5", deployment: newDeployment(5, nil), oldDeployment: newDeployment(10, nil), newRS: rs("foo-v1", 10, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{}, expectedNew: rs("foo-v1", 5, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{}, }, { name: "proportional scaling: 5 -> 10", deployment: newDeployment(10, nil), oldDeployment: newDeployment(5, nil), newRS: rs("foo-v2", 2, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v1", 3, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 4, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v1", 6, nil, oldTimestamp)}, }, { name: "proportional scaling: 5 -> 3", deployment: newDeployment(3, nil), oldDeployment: newDeployment(5, nil), newRS: rs("foo-v2", 2, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v1", 3, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 1, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v1", 2, nil, oldTimestamp)}, }, { name: "proportional scaling: 9 -> 4", deployment: newDeployment(4, nil), oldDeployment: newDeployment(9, nil), newRS: rs("foo-v2", 8, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v1", 1, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 4, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v1", 0, nil, oldTimestamp)}, }, { name: "proportional scaling: 7 -> 10", deployment: newDeployment(10, nil), oldDeployment: newDeployment(7, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 3, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 3, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 4, nil, oldTimestamp), rs("foo-v1", 3, nil, olderTimestamp)}, }, { name: "proportional scaling: 13 -> 8", deployment: newDeployment(8, nil), oldDeployment: newDeployment(13, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 8, nil, oldTimestamp), rs("foo-v1", 3, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 1, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 5, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, }, // Scales up the new replica set. { name: "leftover distribution: 3 -> 4", deployment: newDeployment(4, nil), oldDeployment: newDeployment(3, nil), newRS: rs("foo-v3", 1, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 2, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, }, // Scales down the older replica set. { name: "leftover distribution: 3 -> 2", deployment: newDeployment(2, nil), oldDeployment: newDeployment(3, nil), newRS: rs("foo-v3", 1, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 1, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, }, // Scales up the latest replica set first. { name: "proportional scaling (no new rs): 4 -> 5", deployment: newDeployment(5, nil), oldDeployment: newDeployment(4, nil), newRS: nil, oldRSs: []*exp.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, expectedNew: nil, expectedOld: []*exp.ReplicaSet{rs("foo-v2", 3, nil, oldTimestamp), rs("foo-v1", 2, nil, olderTimestamp)}, }, // Scales down to zero { name: "proportional scaling: 6 -> 0", deployment: newDeployment(0, nil), oldDeployment: newDeployment(6, nil), newRS: rs("foo-v3", 3, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 0, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, }, // Scales up from zero { name: "proportional scaling: 0 -> 6", deployment: newDeployment(6, nil), oldDeployment: newDeployment(0, nil), newRS: rs("foo-v3", 0, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 6, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 0, nil, oldTimestamp), rs("foo-v1", 0, nil, olderTimestamp)}, }, // Scenario: deployment.spec.replicas == 3 ( foo-v1.spec.replicas == foo-v2.spec.replicas == foo-v3.spec.replicas == 1 ) // Deployment is scaled to 5. foo-v3.spec.replicas and foo-v2.spec.replicas should increment by 1 but foo-v2 fails to // update. { name: "failed rs update", deployment: newDeployment(5, nil), oldDeployment: newDeployment(5, nil), newRS: rs("foo-v3", 2, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v2", 1, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, expectedNew: rs("foo-v3", 2, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v2", 2, nil, oldTimestamp), rs("foo-v1", 1, nil, olderTimestamp)}, desiredReplicasAnnotations: map[string]int32{"foo-v2": int32(3)}, }, { name: "deployment with surge pods", deployment: newDeploymentEnhanced(20, intstr.FromInt(2)), oldDeployment: newDeploymentEnhanced(10, intstr.FromInt(2)), newRS: rs("foo-v2", 6, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v1", 6, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 11, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v1", 11, nil, oldTimestamp)}, }, { name: "change both surge and size", deployment: newDeploymentEnhanced(50, intstr.FromInt(6)), oldDeployment: newDeploymentEnhanced(10, intstr.FromInt(3)), newRS: rs("foo-v2", 5, nil, newTimestamp), oldRSs: []*exp.ReplicaSet{rs("foo-v1", 8, nil, oldTimestamp)}, expectedNew: rs("foo-v2", 22, nil, newTimestamp), expectedOld: []*exp.ReplicaSet{rs("foo-v1", 34, nil, oldTimestamp)}, }, } for _, test := range tests { _ = olderTimestamp t.Log(test.name) fake := fake.Clientset{} dc := &DeploymentController{ client: &fake, eventRecorder: &record.FakeRecorder{}, } if test.newRS != nil { desiredReplicas := test.oldDeployment.Spec.Replicas if desired, ok := test.desiredReplicasAnnotations[test.newRS.Name]; ok { desiredReplicas = desired } deploymentutil.SetReplicasAnnotations(test.newRS, desiredReplicas, desiredReplicas+deploymentutil.MaxSurge(*test.oldDeployment)) } for i := range test.oldRSs { rs := test.oldRSs[i] if rs == nil { continue } desiredReplicas := test.oldDeployment.Spec.Replicas if desired, ok := test.desiredReplicasAnnotations[rs.Name]; ok { desiredReplicas = desired } deploymentutil.SetReplicasAnnotations(rs, desiredReplicas, desiredReplicas+deploymentutil.MaxSurge(*test.oldDeployment)) } if err := dc.scale(test.deployment, test.newRS, test.oldRSs); err != nil { t.Errorf("%s: unexpected error: %v", test.name, err) continue } if test.expectedNew != nil && test.newRS != nil && test.expectedNew.Spec.Replicas != test.newRS.Spec.Replicas { t.Errorf("%s: expected new replicas: %d, got: %d", test.name, test.expectedNew.Spec.Replicas, test.newRS.Spec.Replicas) continue } if len(test.expectedOld) != len(test.oldRSs) { t.Errorf("%s: expected %d old replica sets, got %d", test.name, len(test.expectedOld), len(test.oldRSs)) continue } for n := range test.oldRSs { rs := test.oldRSs[n] exp := test.expectedOld[n] if exp.Spec.Replicas != rs.Spec.Replicas { t.Errorf("%s: expected old (%s) replicas: %d, got: %d", test.name, rs.Name, exp.Spec.Replicas, rs.Spec.Replicas) } } } }
func (dc *DeploymentController) scaleReplicaSet(rs *extensions.ReplicaSet, newScale int32, deployment *extensions.Deployment, scalingOperation string) (*extensions.ReplicaSet, error) { // NOTE: This mutates the ReplicaSet passed in. Not sure if that's a good idea. rs.Spec.Replicas = newScale deploymentutil.SetReplicasAnnotations(rs, deployment.Spec.Replicas, deployment.Spec.Replicas+deploymentutil.MaxSurge(*deployment)) rs, err := dc.client.Extensions().ReplicaSets(rs.Namespace).Update(rs) if err == nil { dc.eventRecorder.Eventf(deployment, api.EventTypeNormal, "ScalingReplicaSet", "Scaled %s replica set %s to %d", scalingOperation, rs.Name, newScale) } return rs, err }