Merge pull request #136945 from ardaguclu/fix-kubectl-scale

Reflect expected replica count to the output of kubectl scale

Kubernetes-commit: 63d25f42db76f5b198ad289da9c24eb8ce9e2492
This commit is contained in:
Kubernetes Publisher 2026-03-09 19:53:22 +05:30
commit 8ff045b4dc
3 changed files with 94 additions and 37 deletions

View file

@ -24,6 +24,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
@ -238,13 +239,22 @@ func (o *ScaleOptions) RunScale() error {
for _, info := range infos {
mapping := info.ResourceMapping()
if o.dryRunStrategy == cmdutil.DryRunClient {
if err = updateReplicas(info, int64(o.Replicas)); err != nil {
return err
}
if err := o.PrintObj(info.Object, o.Out); err != nil {
return err
}
continue
}
if err := o.scaler.Scale(info.Namespace, info.Name, uint(o.Replicas), precondition, retry, waitForReplicas, mapping.Resource, o.dryRunStrategy == cmdutil.DryRunServer); err != nil {
actualSize := new(int32)
if err := o.scaler.Scale(info.Namespace, info.Name, uint(o.Replicas), actualSize, precondition, retry, waitForReplicas, mapping.Resource, o.dryRunStrategy == cmdutil.DryRunServer); err != nil {
return err
}
if err = updateReplicas(info, int64(*actualSize)); err != nil {
return err
}
@ -279,3 +289,23 @@ func scaler(f cmdutil.Factory) (scale.Scaler, error) {
return scale.NewScaler(scalesGetter), nil
}
// updateReplicas updates spec.replicas for built-in scalable types.
// replicas needs to be in int64, as SetNestedField only supports this type (and float64).
func updateReplicas(info *resource.Info, replicas int64) error {
unstructuredObj, ok := info.Object.(*unstructured.Unstructured)
if !ok {
return nil
}
// Only update for built-in types where spec.replicas is guaranteed:
// - apps group: Deployment, ReplicaSet, StatefulSet
// - core group: ReplicationController
// Skip other groups (e.g., CRDs) as they may define replicas at a different path.
gvk := unstructuredObj.GroupVersionKind()
if gvk.Group != "apps" && gvk.Group != "" {
return nil
}
return unstructured.SetNestedField(unstructuredObj.Object, replicas, "spec", "replicas")
}

View file

@ -39,10 +39,10 @@ type Scaler interface {
// retries in the event of resource version mismatch (if retry is not nil),
// and optionally waits until the status of the resource matches newSize (if wait is not nil)
// TODO: Make the implementation of this watch-based (#56075) once #31345 is fixed.
Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error
Scale(namespace, name string, newSize uint, actualSize *int32, preconditions *ScalePrecondition, retry, wait *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error
// ScaleSimple does a simple one-shot attempt at scaling - not useful on its own, but
// a necessary building block for Scale
ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error)
ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, actualSize int32, err error)
}
// NewScaler get a scaler for a given resource
@ -81,12 +81,15 @@ func NewRetryParams(interval, timeout time.Duration) *RetryParams {
}
// ScaleCondition is a closure around Scale that facilitates retries via util.wait
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource, dryRun bool) wait.ConditionWithContextFunc {
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, actualSize *int32, gvr schema.GroupVersionResource, dryRun bool) wait.ConditionWithContextFunc {
return func(context.Context) (bool, error) {
rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr, dryRun)
rv, size, err := r.ScaleSimple(namespace, name, precondition, count, gvr, dryRun)
if updatedResourceVersion != nil {
*updatedResourceVersion = rv
}
if actualSize != nil {
*actualSize = size
}
// Retry only on update conflicts.
if apierrors.IsConflict(err) {
return false, nil
@ -117,14 +120,14 @@ type genericScaler struct {
var _ Scaler = &genericScaler{}
// ScaleSimple updates a scale of a given resource. It returns the resourceVersion of the scale if the update was successful.
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error) {
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, actualSize int32, err error) {
if preconditions != nil {
scale, err := s.scaleNamespacer.Scales(namespace).Get(context.TODO(), gvr.GroupResource(), name, metav1.GetOptions{})
if err != nil {
return "", err
return "", 0, err
}
if err = preconditions.validate(scale); err != nil {
return "", err
return "", 0, err
}
scale.Spec.Replicas = int32(newSize)
updateOptions := metav1.UpdateOptions{}
@ -133,9 +136,9 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, updateOptions)
if err != nil {
return "", err
return "", 0, err
}
return updatedScale.ResourceVersion, nil
return updatedScale.ResourceVersion, updatedScale.Spec.Replicas, nil
}
// objectForReplicas is used for encoding scale patch
@ -151,7 +154,7 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale
}
patch, err := json.Marshal(&spec)
if err != nil {
return "", err
return "", 0, err
}
patchOptions := metav1.PatchOptions{}
if dryRun {
@ -159,19 +162,19 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, patchOptions)
if err != nil {
return "", err
return "", 0, err
}
return updatedScale.ResourceVersion, nil
return updatedScale.ResourceVersion, updatedScale.Spec.Replicas, nil
}
// Scale updates a scale of a given resource to a new size, with optional precondition check (if preconditions is not nil),
// optional retries (if retry is not nil), and then optionally waits for the status to reach desired count.
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error {
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, actualSize *int32, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams, gvr schema.GroupVersionResource, dryRun bool) error {
if retry == nil {
// make it try only once, immediately
retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond}
}
cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr, dryRun)
cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, actualSize, gvr, dryRun)
if err := wait.PollUntilContextTimeout(context.Background(), retry.Interval, retry.Timeout, true, cond); err != nil {
return err
}

View file

@ -69,7 +69,8 @@ func TestReplicationControllerScaleRetry(t *testing.T) {
name := "foo-v1"
namespace := metav1.NamespaceDefault
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr, false)
actualSize := new(int32)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, actualSize, rcgvr, false)
pass, err := scaleFunc(context.Background())
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -78,7 +79,7 @@ func TestReplicationControllerScaleRetry(t *testing.T) {
t.Errorf("Did not expect an error on update conflict failure, got %v", err)
}
preconditions := ScalePrecondition{3, ""}
scaleFunc = ScaleCondition(scaler, &preconditions, namespace, name, count, nil, rcgvr, false)
scaleFunc = ScaleCondition(scaler, &preconditions, namespace, name, count, nil, actualSize, rcgvr, false)
_, err = scaleFunc(context.Background())
if err == nil {
t.Errorf("Expected error on precondition failure")
@ -105,7 +106,7 @@ func TestReplicationControllerScaleInvalid(t *testing.T) {
name := "foo-v1"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, rcgvr, false)
pass, err := scaleFunc(context.Background())
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -130,11 +131,15 @@ func TestReplicationControllerScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo-v1"
err := scaler.Scale("default", name, count, nil, nil, nil, rcgvr, false)
actualSize := new(int32)
err := scaler.Scale("default", name, count, actualSize, nil, nil, nil, rcgvr, false)
if err != nil {
t.Fatalf("unexpected error occurred = %v while scaling the resource", err)
}
if *actualSize != 3 {
t.Errorf("expected actualSize to be 3, got %d", *actualSize)
}
actions := scaleClient.Actions()
if len(actions) != len(scaleClientExpectedAction) {
t.Errorf("unexpected actions: %v, expected %d actions got %d", actions, len(scaleClientExpectedAction), len(actions))
@ -153,7 +158,7 @@ func TestReplicationControllerScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rcgvr, false)
err := scaler.Scale("default", name, count, nil, &preconditions, nil, nil, rcgvr, false)
if err == nil {
t.Fatal("expected to get an error but none was returned")
}
@ -179,7 +184,7 @@ func TestDeploymentScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, deploygvr, false)
pass, err := scaleFunc(context.Background())
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -188,7 +193,7 @@ func TestDeploymentScaleRetry(t *testing.T) {
t.Errorf("Did not expect an error on update failure, got %v", err)
}
preconditions := &ScalePrecondition{3, ""}
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, deploygvr, false)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, nil, deploygvr, false)
_, err = scaleFunc(context.Background())
if err == nil {
t.Error("Expected error on precondition failure")
@ -210,10 +215,14 @@ func TestDeploymentScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, deploygvr, false)
actualSize := new(int32)
err := scaler.Scale("default", name, count, actualSize, nil, nil, nil, deploygvr, false)
if err != nil {
t.Fatal(err)
}
if *actualSize != 3 {
t.Errorf("expected actualSize to be 3, got %d", *actualSize)
}
actions := scaleClient.Actions()
if len(actions) != len(scaleClientExpectedAction) {
t.Errorf("unexpected actions: %v, expected %d actions got %d", actions, len(scaleClientExpectedAction), len(actions))
@ -236,7 +245,7 @@ func TestDeploymentScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, deploygvr, false)
pass, err := scaleFunc(context.Background())
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -262,7 +271,7 @@ func TestDeploymentScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, deploygvr, false)
err := scaler.Scale("default", name, count, nil, &preconditions, nil, nil, deploygvr, false)
if err == nil {
t.Fatal("exptected to get an error but none was returned")
}
@ -283,10 +292,14 @@ func TestStatefulSetScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, stsgvr, false)
actualSize := new(int32)
err := scaler.Scale("default", name, count, actualSize, nil, nil, nil, stsgvr, false)
if err != nil {
t.Fatal(err)
}
if *actualSize != 3 {
t.Errorf("expected actualSize to be 3, got %d", *actualSize)
}
actions := scaleClient.Actions()
if len(actions) != len(scaleClientExpectedAction) {
t.Errorf("unexpected actions: %v, expected %d actions got %d", actions, len(scaleClientExpectedAction), len(actions))
@ -309,7 +322,7 @@ func TestStatefulSetScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, stsgvr, false)
pass, err := scaleFunc(context.Background())
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -318,7 +331,7 @@ func TestStatefulSetScaleRetry(t *testing.T) {
t.Errorf("Did not expect an error on update failure, got %v", err)
}
preconditions := &ScalePrecondition{3, ""}
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, stsgvr, false)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, nil, stsgvr, false)
_, err = scaleFunc(context.Background())
if err == nil {
t.Error("Expected error on precondition failure")
@ -345,7 +358,7 @@ func TestStatefulSetScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, stsgvr, false)
pass, err := scaleFunc(context.Background())
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -371,7 +384,7 @@ func TestStatefulSetScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, stsgvr, false)
err := scaler.Scale("default", name, count, nil, &preconditions, nil, nil, stsgvr, false)
if err == nil {
t.Fatal("expected to get an error but none was returned")
}
@ -392,10 +405,14 @@ func TestReplicaSetScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, rsgvr, false)
actualSize := new(int32)
err := scaler.Scale("default", name, count, actualSize, nil, nil, nil, rsgvr, false)
if err != nil {
t.Fatal(err)
}
if *actualSize != 3 {
t.Errorf("expected actualSize to be 3, got %d", *actualSize)
}
actions := scaleClient.Actions()
if len(actions) != len(scaleClientExpectedAction) {
t.Errorf("unexpected actions: %v, expected %d actions got %d", actions, len(scaleClientExpectedAction), len(actions))
@ -418,7 +435,7 @@ func TestReplicaSetScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, rsgvr, false)
pass, err := scaleFunc(context.Background())
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -427,7 +444,7 @@ func TestReplicaSetScaleRetry(t *testing.T) {
t.Errorf("Did not expect an error on update failure, got %v", err)
}
preconditions := &ScalePrecondition{3, ""}
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, rsgvr, false)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, nil, rsgvr, false)
_, err = scaleFunc(context.Background())
if err == nil {
t.Error("Expected error on precondition failure")
@ -454,7 +471,7 @@ func TestReplicaSetScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr, false)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, nil, rsgvr, false)
pass, err := scaleFunc(context.Background())
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
@ -480,7 +497,7 @@ func TestReplicaSetsGetterFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rsgvr, false)
err := scaler.Scale("default", name, count, nil, &preconditions, nil, nil, rsgvr, false)
if err == nil {
t.Fatal("expected to get an error but non was returned")
}
@ -576,7 +593,7 @@ func TestGenericScaleSimple(t *testing.T) {
t.Run(fmt.Sprintf("running scenario %d: %s", index+1, scenario.name), func(t *testing.T) {
target := NewScaler(scenario.scaleGetter)
resVersion, err := target.ScaleSimple("default", scenario.resName, scenario.precondition, uint(scenario.newSize), scenario.targetGVR, false)
resVersion, actualSize, err := target.ScaleSimple("default", scenario.resName, scenario.precondition, uint(scenario.newSize), scenario.targetGVR, false)
if scenario.expectError && err == nil {
t.Fatal("expected an error but was not returned")
@ -584,6 +601,9 @@ func TestGenericScaleSimple(t *testing.T) {
if !scenario.expectError && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !scenario.expectError && actualSize != int32(scenario.newSize) {
t.Errorf("expected actualSize to be %d, got %d", scenario.newSize, actualSize)
}
if resVersion != "" {
t.Fatalf("unexpected resource version returned = %s, wanted = %s", resVersion, "")
}
@ -666,7 +686,8 @@ func TestGenericScale(t *testing.T) {
t.Run(scenario.name, func(t *testing.T) {
target := NewScaler(scenario.scaleGetter)
err := target.Scale("default", scenario.resName, uint(scenario.newSize), scenario.precondition, nil, scenario.waitForReplicas, scenario.targetGVR, false)
actualSize := new(int32)
err := target.Scale("default", scenario.resName, uint(scenario.newSize), actualSize, scenario.precondition, nil, scenario.waitForReplicas, scenario.targetGVR, false)
if scenario.expectError && err == nil {
t.Fatal("expected an error but was not returned")
@ -674,6 +695,9 @@ func TestGenericScale(t *testing.T) {
if !scenario.expectError && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !scenario.expectError && *actualSize != int32(scenario.newSize) {
t.Errorf("expected actualSize to be %d, got %d", scenario.newSize, *actualSize)
}
})
}