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

This commit is contained in:
Julian V. Modesto 2020-03-30 17:33:15 -04:00
parent 933c303592
commit efed958779
5 changed files with 73 additions and 35 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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")

View File

@ -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'

View File

@ -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)