diff --git a/docs/man/man1/kubectl-scale.1 b/docs/man/man1/kubectl-scale.1 index ebf31708110..cea0bafa123 100644 --- a/docs/man/man1/kubectl-scale.1 +++ b/docs/man/man1/kubectl-scale.1 @@ -3,7 +3,7 @@ .SH NAME .PP -kubectl scale \- Set a new size for a Replication Controller. +kubectl scale \- Set a new size for a Replication Controller, Job, or Deployment. .SH SYNOPSIS @@ -13,7 +13,7 @@ kubectl scale \- Set a new size for a Replication Controller. .SH DESCRIPTION .PP -Set a new size for a Replication Controller. +Set a new size for a Replication Controller, Job, or Deployment. .PP Scale also allows users to specify one or more preconditions for the scale action. @@ -25,11 +25,11 @@ scale is sent to the server. .SH OPTIONS .PP \fB\-\-current\-replicas\fP=\-1 - Precondition for current size. Requires that the current size of the replication controller match this value in order to scale. + Precondition for current size. Requires that the current size of the resource match this value in order to scale. .PP \fB\-f\fP, \fB\-\-filename\fP=[] - Filename, directory, or URL to a file identifying the replication controller to set a new size + Filename, directory, or URL to a file identifying the resource to set a new size .PP \fB\-o\fP, \fB\-\-output\fP="" @@ -148,16 +148,19 @@ scale is sent to the server. .nf # Scale replication controller named 'foo' to 3. -$ kubectl scale \-\-replicas=3 replicationcontrollers foo +$ kubectl scale \-\-replicas=3 rc/foo -# Scale a replication controller identified by type and name specified in "foo\-controller.yaml" to 3. -$ kubectl scale \-\-replicas=3 \-f foo\-controller.yaml +# Scale a resource identified by type and name specified in "foo.yaml" to 3. +$ kubectl scale \-\-replicas=3 \-f foo.yaml -# If the replication controller named foo's current size is 2, scale foo to 3. -$ kubectl scale \-\-current\-replicas=2 \-\-replicas=3 replicationcontrollers foo +# If the deployment named mysql's current size is 2, scale mysql to 3. +$ kubectl scale \-\-current\-replicas=2 \-\-replicas=3 deployment/mysql # Scale multiple replication controllers. -$ kubectl scale \-\-replicas=5 rc/foo rc/bar +$ kubectl scale \-\-replicas=5 rc/foo rc/bar rc/baz + +# Scale job named 'cron' to 3. +$ kubectl scale \-\-replicas=3 job/cron .fi .RE diff --git a/docs/user-guide/kubectl/kubectl.md b/docs/user-guide/kubectl/kubectl.md index 8609019a56b..41e802451d4 100644 --- a/docs/user-guide/kubectl/kubectl.md +++ b/docs/user-guide/kubectl/kubectl.md @@ -101,10 +101,10 @@ kubectl * [kubectl replace](kubectl_replace.md) - Replace a resource by filename or stdin. * [kubectl rolling-update](kubectl_rolling-update.md) - Perform a rolling update of the given ReplicationController. * [kubectl run](kubectl_run.md) - Run a particular image on the cluster. -* [kubectl scale](kubectl_scale.md) - Set a new size for a Replication Controller. +* [kubectl scale](kubectl_scale.md) - Set a new size for a Replication Controller, Job, or Deployment. * [kubectl version](kubectl_version.md) - Print the client and server version information. -###### Auto generated by spf13/cobra on 24-Nov-2015 +###### Auto generated by spf13/cobra on 25-Nov-2015 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl.md?pixel)]() diff --git a/docs/user-guide/kubectl/kubectl_scale.md b/docs/user-guide/kubectl/kubectl_scale.md index 4a870bb1783..3bf98437a7f 100644 --- a/docs/user-guide/kubectl/kubectl_scale.md +++ b/docs/user-guide/kubectl/kubectl_scale.md @@ -33,12 +33,12 @@ Documentation for other releases can be found at ## kubectl scale -Set a new size for a Replication Controller. +Set a new size for a Replication Controller, Job, or Deployment. ### Synopsis -Set a new size for a Replication Controller. +Set a new size for a Replication Controller, Job, or Deployment. Scale also allows users to specify one or more preconditions for the scale action. If --current-replicas or --resource-version is specified, it is validated before the @@ -53,23 +53,26 @@ kubectl scale [--resource-version=version] [--current-replicas=count] --replicas ``` # Scale replication controller named 'foo' to 3. -$ kubectl scale --replicas=3 replicationcontrollers foo +$ kubectl scale --replicas=3 rc/foo -# Scale a replication controller identified by type and name specified in "foo-controller.yaml" to 3. -$ kubectl scale --replicas=3 -f foo-controller.yaml +# Scale a resource identified by type and name specified in "foo.yaml" to 3. +$ kubectl scale --replicas=3 -f foo.yaml -# If the replication controller named foo's current size is 2, scale foo to 3. -$ kubectl scale --current-replicas=2 --replicas=3 replicationcontrollers foo +# If the deployment named mysql's current size is 2, scale mysql to 3. +$ kubectl scale --current-replicas=2 --replicas=3 deployment/mysql # Scale multiple replication controllers. -$ kubectl scale --replicas=5 rc/foo rc/bar +$ kubectl scale --replicas=5 rc/foo rc/bar rc/baz + +# Scale job named 'cron' to 3. +$ kubectl scale --replicas=3 job/cron ``` ### Options ``` - --current-replicas=-1: Precondition for current size. Requires that the current size of the replication controller match this value in order to scale. - -f, --filename=[]: Filename, directory, or URL to a file identifying the replication controller to set a new size + --current-replicas=-1: Precondition for current size. Requires that the current size of the resource match this value in order to scale. + -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to set a new size -o, --output="": Output mode. Use "-o name" for shorter output (resource/name). --replicas=-1: The new desired number of replicas. Required. --resource-version="": Precondition for resource version. Requires that the current resource version match this value in order to scale. diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 571909e917a..d8db2eacad5 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -219,6 +219,8 @@ runTests() { hpa_min_field=".spec.minReplicas" hpa_max_field=".spec.maxReplicas" hpa_cpu_field=".spec.cpuUtilization.targetPercentage" + job_parallelism_field=".spec.parallelism" + deployment_replicas=".spec.replicas" # Passing no arguments to create is an error ! kubectl create @@ -873,6 +875,23 @@ __EOF__ # Clean-up kubectl delete rc redis-{master,slave} "${kube_flags[@]}" + ### Scale a job + kubectl create -f docs/user-guide/job.yaml "${kube_flags[@]}" + # Command + kubectl scale --replicas=2 job/pi + # Post-condition: 2 replicas for pi + kube::test::get_object_assert 'job pi' "{{$job_parallelism_field}}" '2' + # Clean-up + kubectl delete job/pi "${kube_flags[@]}" + ### Scale a deployment + kubectl create -f examples/extensions/deployment.yaml "${kube_flags[@]}" + # Command + kubectl scale --current-replicas=3 --replicas=1 deployment/nginx-deployment + # Post-condition: 1 replica for nginx-deployment + kube::test::get_object_assert 'deployment nginx-deployment' "{{$deployment_replicas}}" '1' + # Clean-up + kubectl delete deployment/nginx-deployment "${kube_flags[@]}" + ### Expose replication controller as service # Pre-condition: 2 replicas kube::test::get_object_assert 'rc frontend' "{{$rc_replicas_field}}" '2' diff --git a/pkg/apis/extensions/helpers.go b/pkg/apis/extensions/helpers.go index 23e0abba673..f7be1a7dc4b 100644 --- a/pkg/apis/extensions/helpers.go +++ b/pkg/apis/extensions/helpers.go @@ -18,9 +18,9 @@ package extensions import ( "fmt" - "sort" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/util/sets" ) @@ -65,3 +65,21 @@ func PodSelectorAsSelector(ps *PodSelector) (labels.Selector, error) { sort.Sort(labels.ByKey(selector)) return selector, nil } + +// ScaleFromDeployment returns a scale subresource for a deployment. +func ScaleFromDeployment(deployment *Deployment) *Scale { + return &Scale{ + ObjectMeta: api.ObjectMeta{ + Name: deployment.Name, + Namespace: deployment.Namespace, + CreationTimestamp: deployment.CreationTimestamp, + }, + Spec: ScaleSpec{ + Replicas: deployment.Spec.Replicas, + }, + Status: ScaleStatus{ + Replicas: deployment.Status.Replicas, + Selector: deployment.Spec.Selector, + }, + } +} diff --git a/pkg/client/unversioned/conditions.go b/pkg/client/unversioned/conditions.go index 9117a6ff36f..d1d370f49a4 100644 --- a/pkg/client/unversioned/conditions.go +++ b/pkg/client/unversioned/conditions.go @@ -45,10 +45,10 @@ func ControllerHasDesiredReplicas(c Interface, controller *api.ReplicationContro // JobHasDesiredParallelism returns a condition that will be true if the desired parallelism count // for a job equals the current active counts or is less by an appropriate successful/unsuccessful count. -func JobHasDesiredParallelism(c Interface, job *extensions.Job) wait.ConditionFunc { +func JobHasDesiredParallelism(c ExtensionsInterface, job *extensions.Job) wait.ConditionFunc { return func() (bool, error) { - job, err := c.Extensions().Jobs(job.Namespace).Get(job.Name) + job, err := c.Jobs(job.Namespace).Get(job.Name) if err != nil { return false, err } @@ -62,3 +62,17 @@ func JobHasDesiredParallelism(c Interface, job *extensions.Job) wait.ConditionFu return progress == 0, nil } } + +// DeploymentHasDesiredReplicas returns a condition that will be true if and only if +// the desired replica count for a deployment equals its updated replicas count. +// (non-terminated pods that have the desired template spec). +func DeploymentHasDesiredReplicas(c ExtensionsInterface, deployment *extensions.Deployment) wait.ConditionFunc { + + return func() (bool, error) { + deployment, err := c.Deployments(deployment.Namespace).Get(deployment.Name) + if err != nil { + return false, err + } + return deployment.Status.UpdatedReplicas == deployment.Spec.Replicas, nil + } +} diff --git a/pkg/client/unversioned/testclient/testclient.go b/pkg/client/unversioned/testclient/testclient.go index 3fd3995a1f7..b7aaa08afcc 100644 --- a/pkg/client/unversioned/testclient/testclient.go +++ b/pkg/client/unversioned/testclient/testclient.go @@ -315,6 +315,11 @@ func (c *Fake) SwaggerSchema(version string) (*swagger.ApiDeclaration, error) { return &swagger.ApiDeclaration{}, nil } +// NewSimpleFakeExp returns a client that will respond with the provided objects +func NewSimpleFakeExp(objects ...runtime.Object) *FakeExperimental { + return &FakeExperimental{Fake: NewSimpleFake(objects...)} +} + type FakeExperimental struct { *Fake } diff --git a/pkg/kubectl/cmd/scale.go b/pkg/kubectl/cmd/scale.go index 19471b4ef64..18e9fedbffd 100644 --- a/pkg/kubectl/cmd/scale.go +++ b/pkg/kubectl/cmd/scale.go @@ -36,23 +36,26 @@ type ScaleOptions struct { } const ( - scale_long = `Set a new size for a Replication Controller. + scale_long = `Set a new size for a Replication Controller, Job, or Deployment. Scale also allows users to specify one or more preconditions for the scale action. If --current-replicas or --resource-version is specified, it is validated before the scale is attempted, and it is guaranteed that the precondition holds true when the scale is sent to the server.` scale_example = `# Scale replication controller named 'foo' to 3. -$ kubectl scale --replicas=3 replicationcontrollers foo +$ kubectl scale --replicas=3 rc/foo -# Scale a replication controller identified by type and name specified in "foo-controller.yaml" to 3. -$ kubectl scale --replicas=3 -f foo-controller.yaml +# Scale a resource identified by type and name specified in "foo.yaml" to 3. +$ kubectl scale --replicas=3 -f foo.yaml -# If the replication controller named foo's current size is 2, scale foo to 3. -$ kubectl scale --current-replicas=2 --replicas=3 replicationcontrollers foo +# If the deployment named mysql's current size is 2, scale mysql to 3. +$ kubectl scale --current-replicas=2 --replicas=3 deployment/mysql # Scale multiple replication controllers. -$ kubectl scale --replicas=5 rc/foo rc/bar` +$ kubectl scale --replicas=5 rc/foo rc/bar rc/baz + +# Scale job named 'cron' to 3. +$ kubectl scale --replicas=3 job/cron` ) // NewCmdScale returns a cobra command with the appropriate configuration and flags to run scale @@ -63,7 +66,7 @@ func NewCmdScale(f *cmdutil.Factory, out io.Writer) *cobra.Command { Use: "scale [--resource-version=version] [--current-replicas=count] --replicas=COUNT (-f FILENAME | TYPE NAME)", // resize is deprecated Aliases: []string{"resize"}, - Short: "Set a new size for a Replication Controller.", + Short: "Set a new size for a Replication Controller, Job, or Deployment.", Long: scale_long, Example: scale_example, Run: func(cmd *cobra.Command, args []string) { @@ -74,13 +77,13 @@ func NewCmdScale(f *cmdutil.Factory, out io.Writer) *cobra.Command { }, } cmd.Flags().String("resource-version", "", "Precondition for resource version. Requires that the current resource version match this value in order to scale.") - cmd.Flags().Int("current-replicas", -1, "Precondition for current size. Requires that the current size of the replication controller match this value in order to scale.") + cmd.Flags().Int("current-replicas", -1, "Precondition for current size. Requires that the current size of the resource match this value in order to scale.") cmd.Flags().Int("replicas", -1, "The new desired number of replicas. Required.") cmd.MarkFlagRequired("replicas") cmd.Flags().Duration("timeout", 0, "The length of time to wait before giving up on a scale operation, zero means don't wait.") cmdutil.AddOutputFlagsForMutation(cmd) - usage := "Filename, directory, or URL to a file identifying the replication controller to set a new size" + usage := "Filename, directory, or URL to a file identifying the resource to set a new size" kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, usage) return cmd } @@ -127,11 +130,11 @@ func RunScale(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri resourceVersion := cmdutil.GetFlagString(cmd, "resource-version") if len(resourceVersion) != 0 && len(infos) > 1 { - return fmt.Errorf("cannot use --resource-version with multiple controllers") + return fmt.Errorf("cannot use --resource-version with multiple resources") } currentSize := cmdutil.GetFlagInt(cmd, "current-replicas") if currentSize != -1 && len(infos) > 1 { - return fmt.Errorf("cannot use --current-replicas with multiple controllers") + return fmt.Errorf("cannot use --current-replicas with multiple resources") } precondition := &kubectl.ScalePrecondition{Size: currentSize, ResourceVersion: resourceVersion} retry := kubectl.NewRetryParams(kubectl.Interval, kubectl.Timeout) diff --git a/pkg/kubectl/scale.go b/pkg/kubectl/scale.go index 1080e2bc56f..802d784d451 100644 --- a/pkg/kubectl/scale.go +++ b/pkg/kubectl/scale.go @@ -28,74 +28,7 @@ import ( "k8s.io/kubernetes/pkg/util/wait" ) -// ScalePrecondition describes a condition that must be true for the scale to take place -// If CurrentSize == -1, it is ignored. -// If CurrentResourceVersion is the empty string, it is ignored. -// Otherwise they must equal the values in the replication controller for it to be valid. -type ScalePrecondition struct { - Size int - ResourceVersion string -} - -// A PreconditionError is returned when a replication controller fails to match -// the scale preconditions passed to kubectl. -type PreconditionError struct { - Precondition string - ExpectedValue string - ActualValue string -} - -func (pe PreconditionError) Error() string { - return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue) -} - -type ControllerScaleErrorType int - -const ( - ControllerScaleGetFailure ControllerScaleErrorType = iota - ControllerScaleUpdateFailure - ControllerScaleUpdateInvalidFailure -) - -// A ControllerScaleError is returned when a scale request passes -// preconditions but fails to actually scale the controller. -type ControllerScaleError struct { - FailureType ControllerScaleErrorType - ResourceVersion string - ActualError error -} - -func (c ControllerScaleError) Error() string { - return fmt.Sprintf( - "Scaling the controller failed with: %s; Current resource version %s", - c.ActualError, c.ResourceVersion) -} - -// ValidateReplicationController ensures that the preconditions match. Returns nil if they are valid, an error otherwise -func (precondition *ScalePrecondition) ValidateReplicationController(controller *api.ReplicationController) error { - if precondition.Size != -1 && controller.Spec.Replicas != precondition.Size { - return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(controller.Spec.Replicas)} - } - if precondition.ResourceVersion != "" && controller.ResourceVersion != precondition.ResourceVersion { - return PreconditionError{"resource version", precondition.ResourceVersion, controller.ResourceVersion} - } - return nil -} - -// ValidateJob ensures that the preconditions match. Returns nil if they are valid, an error otherwise -func (precondition *ScalePrecondition) ValidateJob(job *extensions.Job) error { - if precondition.Size != -1 && job.Spec.Parallelism == nil { - return PreconditionError{"parallelism", strconv.Itoa(precondition.Size), "nil"} - } - if precondition.Size != -1 && *job.Spec.Parallelism != precondition.Size { - return PreconditionError{"parallelism", strconv.Itoa(precondition.Size), strconv.Itoa(*job.Spec.Parallelism)} - } - if precondition.ResourceVersion != "" && job.ResourceVersion != precondition.ResourceVersion { - return PreconditionError{"resource version", precondition.ResourceVersion, job.ResourceVersion} - } - return nil -} - +// Scaler provides an interface for resources that can be scaled. type Scaler interface { // Scale scales the named resource after checking preconditions. It optionally // retries in the event of resource version mismatch (if retry is not nil), @@ -111,16 +44,54 @@ func ScalerFor(kind string, c client.Interface) (Scaler, error) { case "ReplicationController": return &ReplicationControllerScaler{c}, nil case "Job": - return &JobScaler{c}, nil + return &JobScaler{c.Extensions()}, nil + case "Deployment": + return &DeploymentScaler{c.Extensions()}, nil } return nil, fmt.Errorf("no scaler has been implemented for %q", kind) } -type ReplicationControllerScaler struct { - c client.Interface +// ScalePrecondition describes a condition that must be true for the scale to take place +// If CurrentSize == -1, it is ignored. +// If CurrentResourceVersion is the empty string, it is ignored. +// Otherwise they must equal the values in the resource for it to be valid. +type ScalePrecondition struct { + Size int + ResourceVersion string } -type JobScaler struct { - c client.Interface + +// A PreconditionError is returned when a resource fails to match +// the scale preconditions passed to kubectl. +type PreconditionError struct { + Precondition string + ExpectedValue string + ActualValue string +} + +func (pe PreconditionError) Error() string { + return fmt.Sprintf("Expected %s to be %s, was %s", pe.Precondition, pe.ExpectedValue, pe.ActualValue) +} + +type ScaleErrorType int + +const ( + ScaleGetFailure ScaleErrorType = iota + ScaleUpdateFailure + ScaleUpdateInvalidFailure +) + +// A ScaleError is returned when a scale request passes +// preconditions but fails to actually scale the controller. +type ScaleError struct { + FailureType ScaleErrorType + ResourceVersion string + ActualError error +} + +func (c ScaleError) Error() string { + return fmt.Sprintf( + "Scaling the resource failed with: %v; Current resource version %s", + c.ActualError, c.ResourceVersion) } // RetryParams encapsulates the retry parameters used by kubectl's scaler. @@ -136,15 +107,15 @@ func NewRetryParams(interval, timeout time.Duration) *RetryParams { func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name string, count uint) wait.ConditionFunc { return func() (bool, error) { err := r.ScaleSimple(namespace, name, precondition, count) - switch e, _ := err.(ControllerScaleError); err.(type) { + switch e, _ := err.(ScaleError); err.(type) { case nil: return true, nil - case ControllerScaleError: + case ScaleError: // if it's invalid we shouldn't keep waiting - if e.FailureType == ControllerScaleUpdateInvalidFailure { + if e.FailureType == ScaleUpdateInvalidFailure { return false, err } - if e.FailureType == ControllerScaleUpdateFailure { + if e.FailureType == ScaleUpdateFailure { return false, nil } } @@ -152,10 +123,25 @@ func ScaleCondition(r Scaler, precondition *ScalePrecondition, namespace, name s } } +// ValidateReplicationController ensures that the preconditions match. Returns nil if they are valid, an error otherwise +func (precondition *ScalePrecondition) ValidateReplicationController(controller *api.ReplicationController) error { + if precondition.Size != -1 && controller.Spec.Replicas != precondition.Size { + return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(controller.Spec.Replicas)} + } + if len(precondition.ResourceVersion) != 0 && controller.ResourceVersion != precondition.ResourceVersion { + return PreconditionError{"resource version", precondition.ResourceVersion, controller.ResourceVersion} + } + return nil +} + +type ReplicationControllerScaler struct { + c client.Interface +} + func (scaler *ReplicationControllerScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint) error { controller, err := scaler.c.ReplicationControllers(namespace).Get(name) if err != nil { - return ControllerScaleError{ControllerScaleGetFailure, "Unknown", err} + return ScaleError{ScaleGetFailure, "Unknown", err} } if preconditions != nil { if err := preconditions.ValidateReplicationController(controller); err != nil { @@ -166,11 +152,10 @@ func (scaler *ReplicationControllerScaler) ScaleSimple(namespace, name string, p // TODO: do retry on 409 errors here? if _, err := scaler.c.ReplicationControllers(namespace).Update(controller); err != nil { if errors.IsInvalid(err) { - return ControllerScaleError{ControllerScaleUpdateInvalidFailure, controller.ResourceVersion, err} + return ScaleError{ScaleUpdateInvalidFailure, controller.ResourceVersion, err} } - return ControllerScaleError{ControllerScaleUpdateFailure, controller.ResourceVersion, err} + return ScaleError{ScaleUpdateFailure, controller.ResourceVersion, err} } - // TODO: do a better job of printing objects here. return nil } @@ -200,11 +185,29 @@ func (scaler *ReplicationControllerScaler) Scale(namespace, name string, newSize return nil } +// ValidateJob ensures that the preconditions match. Returns nil if they are valid, an error otherwise. +func (precondition *ScalePrecondition) ValidateJob(job *extensions.Job) error { + if precondition.Size != -1 && job.Spec.Parallelism == nil { + return PreconditionError{"parallelism", strconv.Itoa(precondition.Size), "nil"} + } + if precondition.Size != -1 && *job.Spec.Parallelism != precondition.Size { + return PreconditionError{"parallelism", strconv.Itoa(precondition.Size), strconv.Itoa(*job.Spec.Parallelism)} + } + if len(precondition.ResourceVersion) != 0 && job.ResourceVersion != precondition.ResourceVersion { + return PreconditionError{"resource version", precondition.ResourceVersion, job.ResourceVersion} + } + return nil +} + +type JobScaler struct { + c client.ExtensionsInterface +} + // ScaleSimple is responsible for updating job's parallelism. func (scaler *JobScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint) error { - job, err := scaler.c.Extensions().Jobs(namespace).Get(name) + job, err := scaler.c.Jobs(namespace).Get(name) if err != nil { - return ControllerScaleError{ControllerScaleGetFailure, "Unknown", err} + return ScaleError{ScaleGetFailure, "Unknown", err} } if preconditions != nil { if err := preconditions.ValidateJob(job); err != nil { @@ -213,12 +216,11 @@ func (scaler *JobScaler) ScaleSimple(namespace, name string, preconditions *Scal } parallelism := int(newSize) job.Spec.Parallelism = ¶llelism - if _, err := scaler.c.Extensions().Jobs(namespace).Update(job); err != nil { + if _, err := scaler.c.Jobs(namespace).Update(job); err != nil { if errors.IsInvalid(err) { - return ControllerScaleError{ControllerScaleUpdateInvalidFailure, job.ResourceVersion, err} + return ScaleError{ScaleUpdateInvalidFailure, job.ResourceVersion, err} } - return ControllerScaleError{ControllerScaleUpdateFailure, job.ResourceVersion, err} - + return ScaleError{ScaleUpdateFailure, job.ResourceVersion, err} } return nil } @@ -239,7 +241,7 @@ func (scaler *JobScaler) Scale(namespace, name string, newSize uint, preconditio return err } if waitForReplicas != nil { - job, err := scaler.c.Extensions().Jobs(namespace).Get(name) + job, err := scaler.c.Jobs(namespace).Get(name) if err != nil { return err } @@ -248,3 +250,65 @@ func (scaler *JobScaler) Scale(namespace, name string, newSize uint, preconditio } return nil } + +// ValidateDeployment ensures that the preconditions match. Returns nil if they are valid, an error otherwise. +func (precondition *ScalePrecondition) ValidateDeployment(deployment *extensions.Deployment) error { + if precondition.Size != -1 && deployment.Spec.Replicas != precondition.Size { + return PreconditionError{"replicas", strconv.Itoa(precondition.Size), strconv.Itoa(deployment.Spec.Replicas)} + } + if len(precondition.ResourceVersion) != 0 && deployment.ResourceVersion != precondition.ResourceVersion { + return PreconditionError{"resource version", precondition.ResourceVersion, deployment.ResourceVersion} + } + return nil +} + +type DeploymentScaler struct { + c client.ExtensionsInterface +} + +// ScaleSimple is responsible for updating a deployment's desired replicas count. +func (scaler *DeploymentScaler) ScaleSimple(namespace, name string, preconditions *ScalePrecondition, newSize uint) error { + deployment, err := scaler.c.Deployments(namespace).Get(name) + if err != nil { + return ScaleError{ScaleGetFailure, "Unknown", err} + } + if preconditions != nil { + if err := preconditions.ValidateDeployment(deployment); err != nil { + return err + } + } + scale := extensions.ScaleFromDeployment(deployment) + scale.Spec.Replicas = int(newSize) + if _, err := scaler.c.Scales(namespace).Update("Deployment", scale); err != nil { + if errors.IsInvalid(err) { + return ScaleError{ScaleUpdateInvalidFailure, deployment.ResourceVersion, err} + } + return ScaleError{ScaleUpdateFailure, deployment.ResourceVersion, err} + } + return nil +} + +// Scale updates a deployment 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 (scaler *DeploymentScaler) Scale(namespace, name string, newSize uint, preconditions *ScalePrecondition, retry, waitForReplicas *RetryParams) error { + if preconditions == nil { + preconditions = &ScalePrecondition{-1, ""} + } + if retry == nil { + // Make it try only once, immediately + retry = &RetryParams{Interval: time.Millisecond, Timeout: time.Millisecond} + } + cond := ScaleCondition(scaler, preconditions, namespace, name, newSize) + if err := wait.Poll(retry.Interval, retry.Timeout, cond); err != nil { + return err + } + if waitForReplicas != nil { + deployment, err := scaler.c.Deployments(namespace).Get(name) + if err != nil { + return err + } + return wait.Poll(waitForReplicas.Interval, waitForReplicas.Timeout, + client.DeploymentHasDesiredReplicas(scaler.c, deployment)) + } + return nil +} diff --git a/pkg/kubectl/scale_test.go b/pkg/kubectl/scale_test.go index 8bf64e70b07..58a9d4e413d 100644 --- a/pkg/kubectl/scale_test.go +++ b/pkg/kubectl/scale_test.go @@ -48,45 +48,6 @@ func (c *ErrorReplicationControllerClient) ReplicationControllers(namespace stri return &ErrorReplicationControllers{testclient.FakeReplicationControllers{Fake: &c.Fake, Namespace: namespace}, c.invalid} } -type ErrorJobs struct { - testclient.FakeJobs - invalid bool -} - -func (c *ErrorJobs) Update(job *extensions.Job) (*extensions.Job, error) { - if c.invalid { - return nil, kerrors.NewInvalid(job.Kind, job.Name, nil) - } - return nil, errors.New("Job update failure") -} - -func (c *ErrorJobs) Get(name string) (*extensions.Job, error) { - zero := 0 - return &extensions.Job{ - Spec: extensions.JobSpec{ - Parallelism: &zero, - }, - }, nil -} - -type ErrorJobClient struct { - testclient.FakeExperimental - invalid bool -} - -func (c *ErrorJobClient) Jobs(namespace string) client.JobInterface { - return &ErrorJobs{testclient.FakeJobs{Fake: &c.FakeExperimental, Namespace: namespace}, c.invalid} -} - -type ErrorExtensionsClient struct { - testclient.Fake - invalid bool -} - -func (c *ErrorExtensionsClient) Extensions() client.ExtensionsInterface { - return &ErrorJobClient{testclient.FakeExperimental{&c.Fake}, c.invalid} -} - func TestReplicationControllerScaleRetry(t *testing.T) { fake := &ErrorReplicationControllerClient{Fake: testclient.Fake{}, invalid: false} scaler := ReplicationControllerScaler{fake} @@ -124,8 +85,8 @@ func TestReplicationControllerScaleInvalid(t *testing.T) { if pass { t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass) } - e, ok := err.(ControllerScaleError) - if err == nil || !ok || e.FailureType != ControllerScaleUpdateInvalidFailure { + e, ok := err.(ScaleError) + if err == nil || !ok || e.FailureType != ScaleUpdateInvalidFailure { t.Errorf("Expected error on invalid update failure, got %v", err) } } @@ -286,8 +247,38 @@ func TestValidateReplicationController(t *testing.T) { } } +type ErrorJobs struct { + testclient.FakeJobs + invalid bool +} + +func (c *ErrorJobs) Update(job *extensions.Job) (*extensions.Job, error) { + if c.invalid { + return nil, kerrors.NewInvalid(job.Kind, job.Name, nil) + } + return nil, errors.New("Job update failure") +} + +func (c *ErrorJobs) Get(name string) (*extensions.Job, error) { + zero := 0 + return &extensions.Job{ + Spec: extensions.JobSpec{ + Parallelism: &zero, + }, + }, nil +} + +type ErrorJobClient struct { + testclient.FakeExperimental + invalid bool +} + +func (c *ErrorJobClient) Jobs(namespace string) client.JobInterface { + return &ErrorJobs{testclient.FakeJobs{Fake: &c.FakeExperimental, Namespace: namespace}, c.invalid} +} + func TestJobScaleRetry(t *testing.T) { - fake := &ErrorExtensionsClient{Fake: testclient.Fake{}, invalid: false} + fake := &ErrorJobClient{FakeExperimental: testclient.FakeExperimental{}, invalid: false} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -311,7 +302,7 @@ func TestJobScaleRetry(t *testing.T) { } func TestJobScale(t *testing.T) { - fake := &testclient.Fake{} + fake := &testclient.FakeExperimental{Fake: &testclient.Fake{}} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -331,7 +322,7 @@ func TestJobScale(t *testing.T) { } func TestJobScaleInvalid(t *testing.T) { - fake := &ErrorExtensionsClient{Fake: testclient.Fake{}, invalid: true} + fake := &ErrorJobClient{FakeExperimental: testclient.FakeExperimental{}, invalid: true} scaler := JobScaler{fake} preconditions := ScalePrecondition{-1, ""} count := uint(3) @@ -343,8 +334,8 @@ func TestJobScaleInvalid(t *testing.T) { if pass { t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass) } - e, ok := err.(ControllerScaleError) - if err == nil || !ok || e.FailureType != ControllerScaleUpdateInvalidFailure { + e, ok := err.(ScaleError) + if err == nil || !ok || e.FailureType != ScaleUpdateInvalidFailure { t.Errorf("Expected error on invalid update failure, got %v", err) } } @@ -356,7 +347,7 @@ func TestJobScaleFailsPreconditions(t *testing.T) { Parallelism: &ten, }, }) - scaler := JobScaler{fake} + scaler := JobScaler{&testclient.FakeExperimental{fake}} preconditions := ScalePrecondition{2, ""} count := uint(3) name := "foo" @@ -496,3 +487,267 @@ func TestValidateJob(t *testing.T) { } } } + +type ErrorScales struct { + testclient.FakeScales + invalid bool +} + +func (c *ErrorScales) Update(kind string, scale *extensions.Scale) (*extensions.Scale, error) { + if c.invalid { + return nil, kerrors.NewInvalid(scale.Kind, scale.Name, nil) + } + return nil, errors.New("scale update failure") +} + +func (c *ErrorScales) Get(kind, name string) (*extensions.Scale, error) { + return &extensions.Scale{ + Spec: extensions.ScaleSpec{ + Replicas: 0, + }, + }, nil +} + +type ErrorDeployments struct { + testclient.FakeDeployments + invalid bool +} + +func (c *ErrorDeployments) Update(deployment *extensions.Deployment) (*extensions.Deployment, error) { + if c.invalid { + return nil, kerrors.NewInvalid(deployment.Kind, deployment.Name, nil) + } + return nil, errors.New("deployment update failure") +} + +func (c *ErrorDeployments) Get(name string) (*extensions.Deployment, error) { + return &extensions.Deployment{ + Spec: extensions.DeploymentSpec{ + Replicas: 0, + }, + }, nil +} + +type ErrorDeploymentClient struct { + testclient.FakeExperimental + invalid bool +} + +func (c *ErrorDeploymentClient) Deployments(namespace string) client.DeploymentInterface { + return &ErrorDeployments{testclient.FakeDeployments{Fake: &c.FakeExperimental, Namespace: namespace}, c.invalid} +} + +func (c *ErrorDeploymentClient) Scales(namespace string) client.ScaleInterface { + return &ErrorScales{testclient.FakeScales{Fake: &c.FakeExperimental, Namespace: namespace}, c.invalid} +} + +func TestDeploymentScaleRetry(t *testing.T) { + fake := &ErrorDeploymentClient{FakeExperimental: testclient.FakeExperimental{Fake: &testclient.Fake{}}, invalid: false} + scaler := &DeploymentScaler{fake} + preconditions := &ScalePrecondition{-1, ""} + count := uint(3) + name := "foo" + namespace := "default" + + scaleFunc := ScaleCondition(scaler, preconditions, namespace, name, count) + pass, err := scaleFunc() + if pass != false { + t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass) + } + if err != nil { + t.Errorf("Did not expect an error on update failure, got %v", err) + } + preconditions = &ScalePrecondition{3, ""} + scaleFunc = ScaleCondition(scaler, preconditions, namespace, name, count) + pass, err = scaleFunc() + if err == nil { + t.Errorf("Expected error on precondition failure") + } +} + +func TestDeploymentScale(t *testing.T) { + fake := &testclient.FakeExperimental{Fake: &testclient.Fake{}} + scaler := DeploymentScaler{fake} + preconditions := ScalePrecondition{-1, ""} + count := uint(3) + name := "foo" + scaler.Scale("default", name, count, &preconditions, nil, nil) + + actions := fake.Actions() + if len(actions) != 2 { + t.Errorf("unexpected actions: %v, expected 2 actions (get, update)", actions) + } + if action, ok := actions[0].(testclient.GetAction); !ok || action.GetResource() != "deployments" || action.GetName() != name { + t.Errorf("unexpected action: %v, expected get-replicationController %s", actions[0], name) + } + // TODO: The testclient needs to support subresources + if action, ok := actions[1].(testclient.UpdateAction); !ok || action.GetResource() != "Deployment" || action.GetObject().(*extensions.Scale).Spec.Replicas != int(count) { + t.Errorf("unexpected action %v, expected update-deployment-scale with replicas = %d", actions[1], count) + } +} + +func TestDeploymentScaleInvalid(t *testing.T) { + fake := &ErrorDeploymentClient{FakeExperimental: testclient.FakeExperimental{Fake: &testclient.Fake{}}, invalid: true} + scaler := DeploymentScaler{fake} + preconditions := ScalePrecondition{-1, ""} + count := uint(3) + name := "foo" + namespace := "default" + + scaleFunc := ScaleCondition(&scaler, &preconditions, namespace, name, count) + pass, err := scaleFunc() + if pass { + t.Errorf("Expected an update failure to return pass = false, got pass = %v", pass) + } + e, ok := err.(ScaleError) + if err == nil || !ok || e.FailureType != ScaleUpdateInvalidFailure { + t.Errorf("Expected error on invalid update failure, got %v", err) + } +} + +func TestDeploymentScaleFailsPreconditions(t *testing.T) { + fake := testclient.NewSimpleFake(&extensions.Deployment{ + Spec: extensions.DeploymentSpec{ + Replicas: 10, + }, + }) + scaler := DeploymentScaler{&testclient.FakeExperimental{fake}} + preconditions := ScalePrecondition{2, ""} + count := uint(3) + name := "foo" + scaler.Scale("default", name, count, &preconditions, nil, nil) + + actions := fake.Actions() + if len(actions) != 1 { + t.Errorf("unexpected actions: %v, expected 1 actions (get)", actions) + } + if action, ok := actions[0].(testclient.GetAction); !ok || action.GetResource() != "deployments" || action.GetName() != name { + t.Errorf("unexpected action: %v, expected get-deployment %s", actions[0], name) + } +} + +func TestValidateDeployment(t *testing.T) { + zero, ten, twenty := 0, 10, 20 + tests := []struct { + preconditions ScalePrecondition + deployment extensions.Deployment + expectError bool + test string + }{ + { + preconditions: ScalePrecondition{-1, ""}, + expectError: false, + test: "defaults", + }, + { + preconditions: ScalePrecondition{-1, ""}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: extensions.DeploymentSpec{ + Replicas: ten, + }, + }, + expectError: false, + test: "defaults 2", + }, + { + preconditions: ScalePrecondition{0, ""}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: extensions.DeploymentSpec{ + Replicas: zero, + }, + }, + expectError: false, + test: "size matches", + }, + { + preconditions: ScalePrecondition{-1, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: extensions.DeploymentSpec{ + Replicas: ten, + }, + }, + expectError: false, + test: "resource version matches", + }, + { + preconditions: ScalePrecondition{10, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: extensions.DeploymentSpec{ + Replicas: ten, + }, + }, + expectError: false, + test: "both match", + }, + { + preconditions: ScalePrecondition{10, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + Spec: extensions.DeploymentSpec{ + Replicas: twenty, + }, + }, + expectError: true, + test: "size different", + }, + { + preconditions: ScalePrecondition{10, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "foo", + }, + }, + expectError: true, + test: "no replicas", + }, + { + preconditions: ScalePrecondition{10, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "bar", + }, + Spec: extensions.DeploymentSpec{ + Replicas: ten, + }, + }, + expectError: true, + test: "version different", + }, + { + preconditions: ScalePrecondition{10, "foo"}, + deployment: extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + ResourceVersion: "bar", + }, + Spec: extensions.DeploymentSpec{ + Replicas: twenty, + }, + }, + expectError: true, + test: "both different", + }, + } + for _, test := range tests { + err := test.preconditions.ValidateDeployment(&test.deployment) + if err != nil && !test.expectError { + t.Errorf("unexpected error: %v (%s)", err, test.test) + } + if err == nil && test.expectError { + t.Errorf("unexpected non-error: %v (%s)", err, test.test) + } + } +} diff --git a/pkg/registry/deployment/etcd/etcd.go b/pkg/registry/deployment/etcd/etcd.go index bfb76d0ba3d..026d224135e 100644 --- a/pkg/registry/deployment/etcd/etcd.go +++ b/pkg/registry/deployment/etcd/etcd.go @@ -132,20 +132,7 @@ func (r *ScaleREST) Get(ctx api.Context, name string) (runtime.Object, error) { if err != nil { return nil, errors.NewNotFound("scale", name) } - return &extensions.Scale{ - ObjectMeta: api.ObjectMeta{ - Name: name, - Namespace: deployment.Namespace, - CreationTimestamp: deployment.CreationTimestamp, - }, - Spec: extensions.ScaleSpec{ - Replicas: deployment.Spec.Replicas, - }, - Status: extensions.ScaleStatus{ - Replicas: deployment.Status.Replicas, - Selector: deployment.Spec.Selector, - }, - }, nil + return extensions.ScaleFromDeployment(deployment), nil } func (r *ScaleREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { @@ -170,18 +157,5 @@ func (r *ScaleREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, if err != nil { return nil, false, errors.NewConflict("scale", scale.Name, err) } - return &extensions.Scale{ - ObjectMeta: api.ObjectMeta{ - Name: deployment.Name, - Namespace: deployment.Namespace, - CreationTimestamp: deployment.CreationTimestamp, - }, - Spec: extensions.ScaleSpec{ - Replicas: deployment.Spec.Replicas, - }, - Status: extensions.ScaleStatus{ - Replicas: deployment.Status.Replicas, - Selector: deployment.Spec.Selector, - }, - }, false, nil + return extensions.ScaleFromDeployment(deployment), false, nil }