From efed958779d564357a9965fccf43f64ee1feb972 Mon Sep 17 00:00:00 2001 From: "Julian V. Modesto" Date: Mon, 30 Mar 2020 17:33:15 -0400 Subject: [PATCH] Support kubectl scale --dry-run=server|client --- .../src/k8s.io/kubectl/pkg/cmd/scale/scale.go | 26 +++++++++-- staging/src/k8s.io/kubectl/pkg/scale/scale.go | 26 +++++++---- .../k8s.io/kubectl/pkg/scale/scale_test.go | 44 +++++++++---------- test/cmd/apps.sh | 10 +++++ test/utils/update_resources.go | 2 +- 5 files changed, 73 insertions(+), 35 deletions(-) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go b/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go index 8c9b0156094..64bcb39a1c6 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/scale/scale.go @@ -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 } @@ -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 } @@ -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 @@ -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) } @@ -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 } @@ -245,7 +266,6 @@ func (o *ScaleOptions) RunScale() error { } } - counter++ return o.PrintObj(info.Object, o.Out) }) if err != nil { diff --git a/staging/src/k8s.io/kubectl/pkg/scale/scale.go b/staging/src/k8s.io/kubectl/pkg/scale/scale.go index 0836e68fa39..4054d09ddf7 100644 --- a/staging/src/k8s.io/kubectl/pkg/scale/scale.go +++ b/staging/src/k8s.io/kubectl/pkg/scale/scale.go @@ -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 @@ -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 } @@ -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 { @@ -125,7 +125,11 @@ 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 } @@ -133,7 +137,11 @@ func (s *genericScaler) ScaleSimple(namespace, name string, preconditions *Scale } 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 } @@ -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 } diff --git a/staging/src/k8s.io/kubectl/pkg/scale/scale_test.go b/staging/src/k8s.io/kubectl/pkg/scale/scale_test.go index 4ea5761e040..f9c9720eb8b 100644 --- a/staging/src/k8s.io/kubectl/pkg/scale/scale_test.go +++ b/staging/src/k8s.io/kubectl/pkg/scale/scale_test.go @@ -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) @@ -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") @@ -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) @@ -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) @@ -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") } @@ -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) @@ -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") @@ -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) } @@ -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) @@ -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") } @@ -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) } @@ -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) @@ -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") @@ -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) @@ -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") } @@ -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) } @@ -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) @@ -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") @@ -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) @@ -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") } @@ -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") @@ -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") diff --git a/test/cmd/apps.sh b/test/cmd/apps.sh index b880dab4938..af608071019 100755 --- a/test/cmd/apps.sh +++ b/test/cmd/apps.sh @@ -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 @@ -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' diff --git a/test/utils/update_resources.go b/test/utils/update_resources.go index eab599c3ff6..09f4691988b 100644 --- a/test/utils/update_resources.go +++ b/test/utils/update_resources.go @@ -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)