Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support kubectl scale --dry-run=server|client #89666

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 23 additions & 3 deletions staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go
Expand Up @@ -89,6 +89,8 @@ type ScaleOptions struct {
scaler scale.Scaler
unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
parent string
dryRunStrategy cmdutil.DryRunStrategy
dryRunVerifier *resource.DryRunVerifier

genericclioptions.IOStreams
}
Expand Down Expand Up @@ -134,6 +136,7 @@ func NewCmdScale(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobr
cmd.MarkFlagRequired("replicas")
cmd.Flags().DurationVar(&o.Timeout, "timeout", 0, "The length of time to wait before giving up on a scale operation, zero means don't wait. Any other values should contain a corresponding time unit (e.g. 1s, 2m, 3h).")
cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "identifying the resource to set a new size")
cmdutil.AddDryRunFlag(cmd)
return cmd
}

Expand All @@ -150,6 +153,20 @@ func (o *ScaleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []st
}
o.PrintObj = printer.PrintObj

o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
if err != nil {
return err
}
dynamicClient, err := f.DynamicClient()
if err != nil {
return err
}
discoveryClient, err := f.ToDiscoveryClient()
if err != nil {
return err
}
o.dryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)

o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
Expand Down Expand Up @@ -216,7 +233,7 @@ func (o *ScaleOptions) RunScale() error {
retry := scale.NewRetryParams(1*time.Second, 5*time.Minute)

var waitForReplicas *scale.RetryParams
if o.Timeout != 0 {
if o.Timeout != 0 && o.dryRunStrategy == cmdutil.DryRunNone {
waitForReplicas = scale.NewRetryParams(1*time.Second, timeout)
}

Expand All @@ -225,9 +242,13 @@ func (o *ScaleOptions) RunScale() error {
if err != nil {
return err
}
counter++

mapping := info.ResourceMapping()
if err := o.scaler.Scale(info.Namespace, info.Name, uint(o.Replicas), precondition, retry, waitForReplicas, mapping.Resource); err != nil {
if o.dryRunStrategy == cmdutil.DryRunClient {
return o.PrintObj(info.Object, o.Out)
}
if err := o.scaler.Scale(info.Namespace, info.Name, uint(o.Replicas), precondition, retry, waitForReplicas, mapping.Resource, o.dryRunStrategy == cmdutil.DryRunServer); err != nil {
return err
}

Expand All @@ -245,7 +266,6 @@ func (o *ScaleOptions) RunScale() error {
}
}

counter++
return o.PrintObj(info.Object, o.Out)
})
if err != nil {
Expand Down
26 changes: 17 additions & 9 deletions staging/src/k8s.io/kubectl/pkg/scale/scale.go
Expand Up @@ -37,10 +37,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) error
Scale(namespace, name string, newSize uint, 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) (updatedResourceVersion string, err error)
ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error)
}

// NewScaler get a scaler for a given resource
Expand Down Expand Up @@ -79,9 +79,9 @@ 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) wait.ConditionFunc {
func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint, updatedResourceVersion *string, gvr schema.GroupVersionResource, dryRun bool) wait.ConditionFunc {
return func() (bool, error) {
rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr)
rv, err := r.ScaleSimple(namespace, name, precondition, count, gvr, dryRun)
if updatedResourceVersion != nil {
*updatedResourceVersion = rv
}
Expand Down Expand Up @@ -115,7 +115,7 @@ 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) (updatedResourceVersion string, err error) {
func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint, gvr schema.GroupVersionResource, dryRun bool) (updatedResourceVersion string, err error) {
if preconditions != nil {
scale, err := s.scaleNamespacer.Scales(namespace).Get(context.TODO(), gvr.GroupResource(), name, metav1.GetOptions{})
if err != nil {
Expand All @@ -125,15 +125,23 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale
return "", err
}
scale.Spec.Replicas = int32(newSize)
updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, metav1.UpdateOptions{})
updateOptions := metav1.UpdateOptions{}
if dryRun {
updateOptions.DryRun = []string{metav1.DryRunAll}
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Update(context.TODO(), gvr.GroupResource(), scale, updateOptions)
if err != nil {
return "", err
}
return updatedScale.ResourceVersion, nil
}

patch := []byte(fmt.Sprintf(`{"spec":{"replicas":%d}}`, newSize))
updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, metav1.PatchOptions{})
patchOptions := metav1.PatchOptions{}
if dryRun {
patchOptions.DryRun = []string{metav1.DryRunAll}
}
updatedScale, err := s.scaleNamespacer.Scales(namespace).Patch(context.TODO(), gvr, name, types.MergePatchType, patch, patchOptions)
if err != nil {
return "", err
}
Expand All @@ -142,12 +150,12 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale

// 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) error {
func (s *genericScaler) Scale(namespace, resourceName string, newSize uint, 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)
cond := ScaleCondition(s, preconditions, namespace, resourceName, newSize, nil, gvr, dryRun)
if err := wait.PollImmediate(retry.Interval, retry.Timeout, cond); err != nil {
return err
}
Expand Down
44 changes: 22 additions & 22 deletions staging/src/k8s.io/kubectl/pkg/scale/scale_test.go
Expand Up @@ -68,7 +68,7 @@ func TestReplicationControllerScaleRetry(t *testing.T) {
name := "foo-v1"
namespace := metav1.NamespaceDefault

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr, false)
pass, err := scaleFunc()
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -77,7 +77,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)
scaleFunc = ScaleCondition(scaler, &preconditions, namespace, name, count, nil, rcgvr, false)
_, err = scaleFunc()
if err == nil {
t.Errorf("Expected error on precondition failure")
Expand All @@ -104,7 +104,7 @@ func TestReplicationControllerScaleInvalid(t *testing.T) {
name := "foo-v1"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rcgvr, false)
pass, err := scaleFunc()
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -129,7 +129,7 @@ func TestReplicationControllerScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo-v1"
err := scaler.Scale("default", name, count, nil, nil, nil, rcgvr)
err := scaler.Scale("default", name, count, nil, nil, nil, rcgvr, false)

if err != nil {
t.Fatalf("unexpected error occurred = %v while scaling the resource", err)
Expand All @@ -152,7 +152,7 @@ func TestReplicationControllerScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rcgvr)
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rcgvr, false)
if err == nil {
t.Fatal("expected to get an error but none was returned")
}
Expand All @@ -178,7 +178,7 @@ func TestDeploymentScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr, false)
pass, err := scaleFunc()
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -187,7 +187,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)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, deploygvr, false)
_, err = scaleFunc()
if err == nil {
t.Error("Expected error on precondition failure")
Expand All @@ -209,7 +209,7 @@ func TestDeploymentScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, deploygvr)
err := scaler.Scale("default", name, count, nil, nil, nil, deploygvr, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -235,7 +235,7 @@ func TestDeploymentScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, deploygvr, false)
pass, err := scaleFunc()
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -261,7 +261,7 @@ func TestDeploymentScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, deploygvr)
err := scaler.Scale("default", name, count, &preconditions, nil, nil, deploygvr, false)
if err == nil {
t.Fatal("exptected to get an error but none was returned")
}
Expand All @@ -282,7 +282,7 @@ func TestStatefulSetScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, stsgvr)
err := scaler.Scale("default", name, count, nil, nil, nil, stsgvr, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -308,7 +308,7 @@ func TestStatefulSetScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr, false)
pass, err := scaleFunc()
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -317,7 +317,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)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, stsgvr, false)
_, err = scaleFunc()
if err == nil {
t.Error("Expected error on precondition failure")
Expand All @@ -344,7 +344,7 @@ func TestStatefulSetScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, stsgvr, false)
pass, err := scaleFunc()
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -370,7 +370,7 @@ func TestStatefulSetScaleFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, stsgvr)
err := scaler.Scale("default", name, count, &preconditions, nil, nil, stsgvr, false)
if err == nil {
t.Fatal("expected to get an error but none was returned")
}
Expand All @@ -391,7 +391,7 @@ func TestReplicaSetScale(t *testing.T) {
scaler := NewScaler(scaleClient)
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, nil, nil, nil, rsgvr)
err := scaler.Scale("default", name, count, nil, nil, nil, rsgvr, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -417,7 +417,7 @@ func TestReplicaSetScaleRetry(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr, false)
pass, err := scaleFunc()
if pass != false {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -426,7 +426,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)
scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count, nil, rsgvr, false)
_, err = scaleFunc()
if err == nil {
t.Error("Expected error on precondition failure")
Expand All @@ -453,7 +453,7 @@ func TestReplicaSetScaleInvalid(t *testing.T) {
name := "foo"
namespace := "default"

scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr)
scaleFunc := ScaleCondition(scaler, nil, namespace, name, count, nil, rsgvr, false)
pass, err := scaleFunc()
if pass {
t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass)
Expand All @@ -479,7 +479,7 @@ func TestReplicaSetsGetterFailsPreconditions(t *testing.T) {
preconditions := ScalePrecondition{2, ""}
count := uint(3)
name := "foo"
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rsgvr)
err := scaler.Scale("default", name, count, &preconditions, nil, nil, rsgvr, false)
if err == nil {
t.Fatal("expected to get an error but non was returned")
}
Expand Down Expand Up @@ -575,7 +575,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)
resVersion, 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")
Expand Down Expand Up @@ -665,7 +665,7 @@ 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)
err := target.Scale("default", scenario.resName, uint(scenario.newSize), scenario.precondition, nil, scenario.waitForReplicas, scenario.targetGVR, false)

if scenario.expectError && err == nil {
t.Fatal("expected an error but was not returned")
Expand Down
10 changes: 10 additions & 0 deletions test/cmd/apps.sh
Expand Up @@ -584,6 +584,10 @@ run_rs_tests() {
### Scale replica set frontend with current-replicas and replicas
# Pre-condition: 3 replicas
kube::test::get_object_assert 'rs frontend' "{{${rs_replicas_field:?}}}" '3'
# Dry-run Command
kubectl scale --dry-run=client --current-replicas=3 --replicas=2 replicasets frontend "${kube_flags[@]:?}"
kubectl scale --dry-run=server --current-replicas=3 --replicas=2 replicasets frontend "${kube_flags[@]:?}"
kube::test::get_object_assert 'rs frontend' "{{${rs_replicas_field:?}}}" '3'
# Command
kubectl scale --current-replicas=3 --replicas=2 replicasets frontend "${kube_flags[@]:?}"
# Post-condition: 2 replicas
Expand All @@ -596,6 +600,12 @@ run_rs_tests() {
kube::test::get_object_assert 'deploy scale-1' "{{.spec.replicas}}" '1'
kube::test::get_object_assert 'deploy scale-2' "{{.spec.replicas}}" '1'
kube::test::get_object_assert 'deploy scale-3' "{{.spec.replicas}}" '1'
# Test kubectl scale --all with dry run
kubectl scale deploy --replicas=3 --all --dry-run=client
kubectl scale deploy --replicas=3 --all --dry-run=server
kube::test::get_object_assert 'deploy scale-1' "{{.spec.replicas}}" '1'
kube::test::get_object_assert 'deploy scale-2' "{{.spec.replicas}}" '1'
kube::test::get_object_assert 'deploy scale-3' "{{.spec.replicas}}" '1'
# Test kubectl scale --selector
kubectl scale deploy --replicas=2 -l run=hello
kube::test::get_object_assert 'deploy scale-1' "{{.spec.replicas}}" '2'
Expand Down
2 changes: 1 addition & 1 deletion test/utils/update_resources.go
Expand Up @@ -52,7 +52,7 @@ func ScaleResourceWithRetries(scalesGetter scaleclient.ScalesGetter, namespace,
ResourceVersion: "",
}
waitForReplicas := scale.NewRetryParams(waitRetryInterval, waitRetryTimeout)
cond := RetryErrorCondition(scale.ScaleCondition(scaler, preconditions, namespace, name, size, nil, gvr))
cond := RetryErrorCondition(scale.ScaleCondition(scaler, preconditions, namespace, name, size, nil, gvr, false))
err := wait.PollImmediate(updateRetryInterval, updateRetryTimeout, cond)
if err == nil {
err = scale.WaitForScaleHasDesiredReplicas(scalesGetter, gvr.GroupResource(), name, namespace, size, waitForReplicas)
Expand Down