diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index dd0835ac0f1..7cede8a9b87 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -51,6 +51,19 @@ func ValidateHorizontalPodAutoscalerName(name string, prefix bool) (bool, string return apivalidation.ValidateReplicationControllerName(name, prefix) } +func validateResourceConsumption(consumption *extensions.ResourceConsumption, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + resource := consumption.Resource.String() + if resource != string(api.ResourceMemory) && resource != string(api.ResourceCPU) { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".resource", resource, "resource not supported by autoscaler")) + } + quantity := consumption.Quantity.Value() + if quantity < 0 { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".quantity", quantity, "must be non-negative")) + } + return allErrs +} + func validateHorizontalPodAutoscalerSpec(autoscaler extensions.HorizontalPodAutoscalerSpec) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if autoscaler.MinReplicas < 0 { @@ -87,6 +100,19 @@ func ValidateHorizontalPodAutoscalerUpdate(newAutoscler, oldAutoscaler *extensio return allErrs } +func ValidateHorizontalPodAutoscalerStatusUpdate(controller, oldController *extensions.HorizontalPodAutoscaler) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) + + status := controller.Status + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.CurrentReplicas), "currentReplicas")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(status.DesiredReplicas), "desiredReplicas")...) + if status.CurrentConsumption != nil { + allErrs = append(allErrs, validateResourceConsumption(status.CurrentConsumption, "currentConsumption")...) + } + return allErrs +} + func ValidateThirdPartyResourceUpdate(old, update *extensions.ThirdPartyResource) errs.ValidationErrorList { return ValidateThirdPartyResource(update) } diff --git a/pkg/client/unversioned/horizontalpodautoscaler.go b/pkg/client/unversioned/horizontalpodautoscaler.go index a2c5150d2ed..93352c6aff7 100644 --- a/pkg/client/unversioned/horizontalpodautoscaler.go +++ b/pkg/client/unversioned/horizontalpodautoscaler.go @@ -36,6 +36,7 @@ type HorizontalPodAutoscalerInterface interface { Delete(name string, options *api.DeleteOptions) error Create(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error) Update(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error) + UpdateStatus(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } @@ -94,6 +95,13 @@ func (c *horizontalPodAutoscalers) Update(horizontalPodAutoscaler *extensions.Ho return } +// UpdateStatus takes the representation of a horizontalPodAutoscaler and updates it. Returns the server's representation of the horizontalPodAutoscaler, and an error, if it occurs. +func (c *horizontalPodAutoscalers) UpdateStatus(horizontalPodAutoscaler *extensions.HorizontalPodAutoscaler) (result *extensions.HorizontalPodAutoscaler, err error) { + result = &extensions.HorizontalPodAutoscaler{} + err = c.client.Put().Namespace(c.ns).Resource("horizontalPodAutoscalers").Name(horizontalPodAutoscaler.Name).SubResource("status").Body(horizontalPodAutoscaler).Do().Into(result) + return +} + // Watch returns a watch.Interface that watches the requested horizontalPodAutoscalers. func (c *horizontalPodAutoscalers) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { return c.client.Get(). diff --git a/pkg/client/unversioned/horizontalpodautoscaler_test.go b/pkg/client/unversioned/horizontalpodautoscaler_test.go index 60abbdea38a..3595cd15d56 100644 --- a/pkg/client/unversioned/horizontalpodautoscaler_test.go +++ b/pkg/client/unversioned/horizontalpodautoscaler_test.go @@ -120,6 +120,23 @@ func TestHorizontalPodAutoscalerUpdate(t *testing.T) { c.Validate(t, response, err) } +func TestHorizontalPodAutoscalerUpdateStatus(t *testing.T) { + ns := api.NamespaceDefault + horizontalPodAutoscaler := &extensions.HorizontalPodAutoscaler{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: ns, + ResourceVersion: "1", + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: testapi.Extensions.ResourcePath(getHorizontalPodAutoscalersResoureName(), ns, "abc") + "/status", Query: buildQueryValues(nil)}, + Response: Response{StatusCode: 200, Body: horizontalPodAutoscaler}, + } + response, err := c.Setup(t).Experimental().HorizontalPodAutoscalers(ns).UpdateStatus(horizontalPodAutoscaler) + c.Validate(t, response, err) +} + func TestHorizontalPodAutoscalerDelete(t *testing.T) { ns := api.NamespaceDefault c := &testClient{ diff --git a/pkg/client/unversioned/testclient/fake_horizontal_pod_autoscalers.go b/pkg/client/unversioned/testclient/fake_horizontal_pod_autoscalers.go index 9a0051f1794..bc901f9d4a0 100644 --- a/pkg/client/unversioned/testclient/fake_horizontal_pod_autoscalers.go +++ b/pkg/client/unversioned/testclient/fake_horizontal_pod_autoscalers.go @@ -72,6 +72,14 @@ func (c *FakeHorizontalPodAutoscalers) Update(a *extensions.HorizontalPodAutosca return obj.(*extensions.HorizontalPodAutoscaler), err } +func (c *FakeHorizontalPodAutoscalers) UpdateStatus(a *extensions.HorizontalPodAutoscaler) (*extensions.HorizontalPodAutoscaler, error) { + obj, err := c.Fake.Invokes(NewUpdateSubresourceAction("horizontalpodautoscalers", "status", c.Namespace, a), &extensions.HorizontalPodAutoscaler{}) + if obj == nil { + return nil, err + } + return obj.(*extensions.HorizontalPodAutoscaler), err +} + func (c *FakeHorizontalPodAutoscalers) Delete(name string, options *api.DeleteOptions) error { _, err := c.Fake.Invokes(NewDeleteAction("horizontalpodautoscalers", c.Namespace, name), &extensions.HorizontalPodAutoscaler{}) return err diff --git a/pkg/controller/podautoscaler/horizontal.go b/pkg/controller/podautoscaler/horizontal.go index a1228943643..13f703a7be6 100644 --- a/pkg/controller/podautoscaler/horizontal.go +++ b/pkg/controller/podautoscaler/horizontal.go @@ -147,7 +147,7 @@ func (a *HorizontalController) reconcileAutoscaler(hpa extensions.HorizontalPodA hpa.Status.LastScaleTimestamp = &now } - _, err = a.client.Experimental().HorizontalPodAutoscalers(hpa.Namespace).Update(&hpa) + _, err = a.client.Experimental().HorizontalPodAutoscalers(hpa.Namespace).UpdateStatus(&hpa) if err != nil { a.eventRecorder.Event(&hpa, "FailedUpdateStatus", err.Error()) return fmt.Errorf("failed to update status for %s: %v", hpa.Name, err) diff --git a/pkg/master/master.go b/pkg/master/master.go index 439f04609b9..eea51fa7af0 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -1042,7 +1042,7 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { dbClient := func(resource string) storage.Interface { return c.StorageDestinations.get("extensions", resource) } - autoscalerStorage := horizontalpodautoscaleretcd.NewREST(dbClient("horizonalpodautoscalers")) + autoscalerStorage, autoscalerStatusStorage := horizontalpodautoscaleretcd.NewREST(dbClient("horizonalpodautoscalers")) thirdPartyResourceStorage := thirdpartyresourceetcd.NewREST(dbClient("thirdpartyresources")) daemonSetStorage, daemonSetStatusStorage := daemonetcd.NewREST(dbClient("daemonsets")) deploymentStorage := deploymentetcd.NewStorage(dbClient("deployments")) @@ -1061,17 +1061,18 @@ func (m *Master) experimental(c *Config) *apiserver.APIGroupVersion { }, 10*time.Second) }() storage := map[string]rest.Storage{ - strings.ToLower("replicationControllers"): controllerStorage.ReplicationController, - strings.ToLower("replicationControllers/scale"): controllerStorage.Scale, - strings.ToLower("horizontalpodautoscalers"): autoscalerStorage, - strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage, - strings.ToLower("daemonsets"): daemonSetStorage, - strings.ToLower("daemonsets/status"): daemonSetStatusStorage, - strings.ToLower("deployments"): deploymentStorage.Deployment, - strings.ToLower("deployments/scale"): deploymentStorage.Scale, - strings.ToLower("jobs"): jobStorage, - strings.ToLower("jobs/status"): jobStatusStorage, - strings.ToLower("ingress"): ingressStorage, + strings.ToLower("replicationControllers"): controllerStorage.ReplicationController, + strings.ToLower("replicationControllers/scale"): controllerStorage.Scale, + strings.ToLower("horizontalpodautoscalers"): autoscalerStorage, + strings.ToLower("horizontalpodautoscalers/status"): autoscalerStatusStorage, + strings.ToLower("thirdpartyresources"): thirdPartyResourceStorage, + strings.ToLower("daemonsets"): daemonSetStorage, + strings.ToLower("daemonsets/status"): daemonSetStatusStorage, + strings.ToLower("deployments"): deploymentStorage.Deployment, + strings.ToLower("deployments/scale"): deploymentStorage.Scale, + strings.ToLower("jobs"): jobStorage, + strings.ToLower("jobs/status"): jobStatusStorage, + strings.ToLower("ingress"): ingressStorage, } expMeta := latest.GroupOrDie("extensions") diff --git a/pkg/registry/horizontalpodautoscaler/etcd/etcd.go b/pkg/registry/horizontalpodautoscaler/etcd/etcd.go index 557416e8d29..8512a907d5c 100644 --- a/pkg/registry/horizontalpodautoscaler/etcd/etcd.go +++ b/pkg/registry/horizontalpodautoscaler/etcd/etcd.go @@ -33,7 +33,7 @@ type REST struct { } // NewREST returns a RESTStorage object that will work against horizontal pod autoscalers. -func NewREST(s storage.Interface) *REST { +func NewREST(s storage.Interface) (*REST, *StatusREST) { prefix := "/horizontalpodautoscalers" store := &etcdgeneric.Etcd{ NewFunc: func() runtime.Object { return &extensions.HorizontalPodAutoscaler{} }, @@ -67,5 +67,21 @@ func NewREST(s storage.Interface) *REST { Storage: s, } - return &REST{store} + statusStore := *store + statusStore.UpdateStrategy = horizontalpodautoscaler.StatusStrategy + return &REST{store}, &StatusREST{store: &statusStore} +} + +// StatusREST implements the REST endpoint for changing the status of a daemonset +type StatusREST struct { + store *etcdgeneric.Etcd +} + +func (r *StatusREST) New() runtime.Object { + return &extensions.HorizontalPodAutoscaler{} +} + +// Update alters the status subset of an object. +func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) } diff --git a/pkg/registry/horizontalpodautoscaler/etcd/etcd_test.go b/pkg/registry/horizontalpodautoscaler/etcd/etcd_test.go index 0d5644f5a71..027e8b500f0 100644 --- a/pkg/registry/horizontalpodautoscaler/etcd/etcd_test.go +++ b/pkg/registry/horizontalpodautoscaler/etcd/etcd_test.go @@ -31,9 +31,10 @@ import ( "k8s.io/kubernetes/pkg/tools" ) -func newStorage(t *testing.T) (*REST, *tools.FakeEtcdClient) { +func newStorage(t *testing.T) (*REST, *StatusREST, *tools.FakeEtcdClient) { etcdStorage, fakeClient := registrytest.NewEtcdStorage(t, "extensions") - return NewREST(etcdStorage), fakeClient + storage, statusStorage := NewREST(etcdStorage) + return storage, statusStorage, fakeClient } func validNewHorizontalPodAutoscaler(name string) *extensions.HorizontalPodAutoscaler { @@ -54,7 +55,7 @@ func validNewHorizontalPodAutoscaler(name string) *extensions.HorizontalPodAutos } func TestCreate(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) autoscaler := validNewHorizontalPodAutoscaler("foo") autoscaler.ObjectMeta = api.ObjectMeta{} @@ -67,7 +68,7 @@ func TestCreate(t *testing.T) { } func TestUpdate(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestUpdate( // valid @@ -82,25 +83,25 @@ func TestUpdate(t *testing.T) { } func TestDelete(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestDelete(validNewHorizontalPodAutoscaler("foo")) } func TestGet(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestGet(validNewHorizontalPodAutoscaler("foo")) } func TestList(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestList(validNewHorizontalPodAutoscaler("foo")) } func TestWatch(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestWatch( validNewHorizontalPodAutoscaler("foo"), @@ -119,3 +120,5 @@ func TestWatch(t *testing.T) { }, ) } + +// TODO TestUpdateStatus diff --git a/pkg/registry/horizontalpodautoscaler/strategy.go b/pkg/registry/horizontalpodautoscaler/strategy.go index e5da945dbe3..955da299c4c 100644 --- a/pkg/registry/horizontalpodautoscaler/strategy.go +++ b/pkg/registry/horizontalpodautoscaler/strategy.go @@ -46,7 +46,10 @@ func (autoscalerStrategy) NamespaceScoped() bool { // PrepareForCreate clears fields that are not allowed to be set by end users on creation. func (autoscalerStrategy) PrepareForCreate(obj runtime.Object) { - _ = obj.(*extensions.HorizontalPodAutoscaler) + newHPA := obj.(*extensions.HorizontalPodAutoscaler) + + // create cannot set status + newHPA.Status = extensions.HorizontalPodAutoscalerStatus{} } // Validate validates a new autoscaler. @@ -62,7 +65,10 @@ func (autoscalerStrategy) AllowCreateOnUpdate() bool { // PrepareForUpdate clears fields that are not allowed to be set by end users on update. func (autoscalerStrategy) PrepareForUpdate(obj, old runtime.Object) { - _ = obj.(*extensions.HorizontalPodAutoscaler) + newHPA := obj.(*extensions.HorizontalPodAutoscaler) + oldHPA := obj.(*extensions.HorizontalPodAutoscaler) + // Update is not allowed to set status + newHPA.Status = oldHPA.Status } // ValidateUpdate is the default update validation for an end user. @@ -91,3 +97,20 @@ func MatchAutoscaler(label labels.Selector, field fields.Selector) generic.Match }, } } + +type autoscalerStatusStrategy struct { + autoscalerStrategy +} + +var StatusStrategy = autoscalerStatusStrategy{Strategy} + +func (autoscalerStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + newAutoscaler := obj.(*extensions.HorizontalPodAutoscaler) + oldAutoscaler := old.(*extensions.HorizontalPodAutoscaler) + // status changes are not allowed to update spec + newAutoscaler.Spec = oldAutoscaler.Spec +} + +func (autoscalerStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) errs.ValidationErrorList { + return validation.ValidateHorizontalPodAutoscalerStatusUpdate(obj.(*extensions.HorizontalPodAutoscaler), old.(*extensions.HorizontalPodAutoscaler)) +}