diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 53e6f73dea..257d8b47eb 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -8024,6 +8024,65 @@ } ] }, + { + "path": "/api/v1/namespaces/{namespace}/replicationcontrollers/{name}/status", + "description": "API at /api/v1", + "operations": [ + { + "type": "v1.ReplicationController", + "method": "PUT", + "summary": "replace status of the specified ReplicationController", + "nickname": "replaceNamespacedReplicationControllerStatus", + "parameters": [ + { + "type": "string", + "paramType": "query", + "name": "pretty", + "description": "If 'true', then the output is pretty printed.", + "required": false, + "allowMultiple": false + }, + { + "type": "v1.ReplicationController", + "paramType": "body", + "name": "body", + "description": "", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "namespace", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "allowMultiple": false + }, + { + "type": "string", + "paramType": "path", + "name": "name", + "description": "name of the ReplicationController", + "required": true, + "allowMultiple": false + } + ], + "responseMessages": [ + { + "code": 200, + "message": "OK", + "responseModel": "v1.ReplicationController" + } + ], + "produces": [ + "application/json" + ], + "consumes": [ + "*/*" + ] + } + ] + }, { "path": "/api/v1/namespaces/{namespace}/resourcequotas", "description": "API at /api/v1", diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 6bfd788147..4087634324 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1352,6 +1352,15 @@ func ValidateReplicationControllerUpdate(oldController, controller *api.Replicat return allErrs } +// ValidateReplicationControllerStatusUpdate tests if required fields in the replication controller are set. +func ValidateReplicationControllerStatusUpdate(oldController, controller *api.ReplicationController) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.Replicas), "status.replicas")...) + allErrs = append(allErrs, ValidatePositiveField(int64(controller.Status.ObservedGeneration), "status.observedGeneration")...) + return allErrs +} + // Validates that the given selector is non-empty. func ValidateNonEmptySelector(selectorMap map[string]string, fieldName string) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 6cce29831b..426a02aa4a 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2101,6 +2101,89 @@ func TestValidateService(t *testing.T) { } } +func TestValidateReplicationControllerStatusUpdate(t *testing.T) { + validSelector := map[string]string{"a": "b"} + validPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + } + type rcUpdateTest struct { + old api.ReplicationController + update api.ReplicationController + } + successCases := []rcUpdateTest{ + { + old: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.ReplicationControllerSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + Status: api.ReplicationControllerStatus{ + Replicas: 2, + }, + }, + update: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.ReplicationControllerSpec{ + Replicas: 3, + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + Status: api.ReplicationControllerStatus{ + Replicas: 4, + }, + }, + }, + } + for _, successCase := range successCases { + successCase.old.ObjectMeta.ResourceVersion = "1" + successCase.update.ObjectMeta.ResourceVersion = "1" + if errs := ValidateReplicationControllerStatusUpdate(&successCase.old, &successCase.update); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := map[string]rcUpdateTest{ + "negative replicas": { + old: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.ReplicationControllerSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + Status: api.ReplicationControllerStatus{ + Replicas: 3, + }, + }, + update: api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.ReplicationControllerSpec{ + Replicas: 2, + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + Status: api.ReplicationControllerStatus{ + Replicas: -3, + }, + }, + }, + } + for testName, errorCase := range errorCases { + if errs := ValidateReplicationControllerStatusUpdate(&errorCase.old, &errorCase.update); len(errs) == 0 { + t.Errorf("expected failure: %s", testName) + } + } + +} + func TestValidateReplicationControllerUpdate(t *testing.T) { validSelector := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ @@ -2475,7 +2558,8 @@ func TestValidateReplicationController(t *testing.T) { field != "spec.replicas" && field != "spec.template.labels" && field != "metadata.annotations" && - field != "metadata.labels" { + field != "metadata.labels" && + field != "status.replicas" { t.Errorf("%s: missing prefix for: %v", k, errs[i]) } } diff --git a/pkg/client/unversioned/replication_controllers.go b/pkg/client/unversioned/replication_controllers.go index 81add2c058..b49c6eae19 100644 --- a/pkg/client/unversioned/replication_controllers.go +++ b/pkg/client/unversioned/replication_controllers.go @@ -34,6 +34,7 @@ type ReplicationControllerInterface interface { Get(name string) (*api.ReplicationController, error) Create(ctrl *api.ReplicationController) (*api.ReplicationController, error) Update(ctrl *api.ReplicationController) (*api.ReplicationController, error) + UpdateStatus(ctrl *api.ReplicationController) (*api.ReplicationController, error) Delete(name string) error Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) } @@ -77,6 +78,13 @@ func (c *replicationControllers) Update(controller *api.ReplicationController) ( return } +// UpdateStatus updates an existing replication controller status +func (c *replicationControllers) UpdateStatus(controller *api.ReplicationController) (result *api.ReplicationController, err error) { + result = &api.ReplicationController{} + err = c.r.Put().Namespace(c.ns).Resource("replicationControllers").Name(controller.Name).SubResource("status").Body(controller).Do().Into(result) + return +} + // Delete deletes an existing replication controller. func (c *replicationControllers) Delete(name string) error { return c.r.Delete().Namespace(c.ns).Resource("replicationControllers").Name(name).Do().Error() diff --git a/pkg/client/unversioned/replication_controllers_test.go b/pkg/client/unversioned/replication_controllers_test.go index 6908f42a65..75a4c720b0 100644 --- a/pkg/client/unversioned/replication_controllers_test.go +++ b/pkg/client/unversioned/replication_controllers_test.go @@ -124,6 +124,36 @@ func TestUpdateController(t *testing.T) { c.Validate(t, receivedController, err) } +func TestUpdateStatusController(t *testing.T) { + ns := api.NamespaceDefault + requestController := &api.ReplicationController{ + ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: testapi.Default.ResourcePath(getRCResourceName(), ns, "foo") + "/status", Query: buildQueryValues(nil)}, + Response: Response{ + StatusCode: 200, + Body: &api.ReplicationController{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "foo": "bar", + "name": "baz", + }, + }, + Spec: api.ReplicationControllerSpec{ + Replicas: 2, + Template: &api.PodTemplateSpec{}, + }, + Status: api.ReplicationControllerStatus{ + Replicas: 2, + }, + }, + }, + } + receivedController, err := c.Setup(t).ReplicationControllers(ns).UpdateStatus(requestController) + c.Validate(t, receivedController, err) +} func TestDeleteController(t *testing.T) { ns := api.NamespaceDefault c := &testClient{ diff --git a/pkg/client/unversioned/testclient/fake_replication_controllers.go b/pkg/client/unversioned/testclient/fake_replication_controllers.go index 34307e3579..857a38acdd 100644 --- a/pkg/client/unversioned/testclient/fake_replication_controllers.go +++ b/pkg/client/unversioned/testclient/fake_replication_controllers.go @@ -66,6 +66,14 @@ func (c *FakeReplicationControllers) Update(controller *api.ReplicationControlle return obj.(*api.ReplicationController), err } +func (c *FakeReplicationControllers) UpdateStatus(controller *api.ReplicationController) (*api.ReplicationController, error) { + obj, err := c.Fake.Invokes(NewUpdateSubresourceAction("replicationcontrollers", "status", c.Namespace, controller), &api.ReplicationController{}) + if obj == nil { + return nil, err + } + return obj.(*api.ReplicationController), err +} + func (c *FakeReplicationControllers) Delete(name string) error { _, err := c.Fake.Invokes(NewDeleteAction("replicationcontrollers", c.Namespace, name), &api.ReplicationController{}) return err diff --git a/pkg/controller/replication/replication_controller_test.go b/pkg/controller/replication/replication_controller_test.go index 96a91d8638..edd9ffcfc6 100644 --- a/pkg/controller/replication/replication_controller_test.go +++ b/pkg/controller/replication/replication_controller_test.go @@ -250,7 +250,7 @@ func TestStatusUpdatesWithoutReplicasChange(t *testing.T) { rc.Status.ObservedGeneration = rc.Generation updatedRc := runtime.EncodeOrDie(testapi.Default.Codec(), rc) - fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name), "PUT", &updatedRc) + fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name)+"/status", "PUT", &updatedRc) } func TestControllerUpdateReplicas(t *testing.T) { @@ -288,7 +288,7 @@ func TestControllerUpdateReplicas(t *testing.T) { rc.Status = api.ReplicationControllerStatus{Replicas: 4, ObservedGeneration: 1} decRc := runtime.EncodeOrDie(testapi.Default.Codec(), rc) - fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name), "PUT", &decRc) + fakeHandler.ValidateRequest(t, testapi.Default.ResourcePath(replicationControllerResourceName(), rc.Namespace, rc.Name)+"/status", "PUT", &decRc) validateSyncReplication(t, &fakePodControl, 1, 0) } diff --git a/pkg/controller/replication/replication_controller_utils.go b/pkg/controller/replication/replication_controller_utils.go index 13858358a6..b9a352ebc3 100644 --- a/pkg/controller/replication/replication_controller_utils.go +++ b/pkg/controller/replication/replication_controller_utils.go @@ -43,7 +43,7 @@ func updateReplicaCount(rcClient client.ReplicationControllerInterface, controll controller.Name, controller.Status.Replicas, numReplicas, controller.Spec.Replicas, controller.Status.ObservedGeneration, generation) rc.Status = api.ReplicationControllerStatus{Replicas: numReplicas, ObservedGeneration: generation} - _, updateErr = rcClient.Update(rc) + _, updateErr = rcClient.UpdateStatus(rc) if updateErr == nil || i >= statusUpdateRetries { return updateErr } diff --git a/pkg/master/master.go b/pkg/master/master.go index 619436db2e..6cd65bf677 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -551,7 +551,7 @@ func (m *Master) init(c *Config) { }) m.serviceNodePortAllocator = serviceNodePortRegistry - controllerStorage := controlleretcd.NewREST(dbClient("replicationControllers")) + controllerStorage, controllerStatusStorage := controlleretcd.NewREST(dbClient("replicationControllers")) // TODO: Factor out the core API registration m.storage = map[string]rest.Storage{ @@ -567,12 +567,13 @@ func (m *Master) init(c *Config) { "podTemplates": podTemplateStorage, - "replicationControllers": controllerStorage, - "services": service.NewStorage(m.serviceRegistry, m.endpointRegistry, serviceClusterIPAllocator, serviceNodePortAllocator), - "endpoints": endpointsStorage, - "nodes": nodeStorage, - "nodes/status": nodeStatusStorage, - "events": eventStorage, + "replicationControllers": controllerStorage, + "replicationControllers/status": controllerStatusStorage, + "services": service.NewStorage(m.serviceRegistry, m.endpointRegistry, serviceClusterIPAllocator, serviceNodePortAllocator), + "endpoints": endpointsStorage, + "nodes": nodeStorage, + "nodes/status": nodeStatusStorage, + "events": eventStorage, "limitRanges": limitRangeStorage, "resourceQuotas": resourceQuotaStorage, diff --git a/pkg/registry/controller/etcd/etcd.go b/pkg/registry/controller/etcd/etcd.go index d6c8a73547..720a066886 100644 --- a/pkg/registry/controller/etcd/etcd.go +++ b/pkg/registry/controller/etcd/etcd.go @@ -32,7 +32,7 @@ type REST struct { } // NewREST returns a RESTStorage object that will work against replication controllers. -func NewREST(s storage.Interface) *REST { +func NewREST(s storage.Interface) (*REST, *StatusREST) { prefix := "/controllers" store := &etcdgeneric.Etcd{ NewFunc: func() runtime.Object { return &api.ReplicationController{} }, @@ -67,5 +67,22 @@ func NewREST(s storage.Interface) *REST { Storage: s, } - return &REST{store} + statusStore := *store + statusStore.UpdateStrategy = controller.StatusStrategy + + return &REST{store}, &StatusREST{store: &statusStore} +} + +// StatusREST implements the REST endpoint for changing the status of a replication controller +type StatusREST struct { + store *etcdgeneric.Etcd +} + +func (r *StatusREST) New() runtime.Object { + return &api.ReplicationController{} +} + +// 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/controller/etcd/etcd_test.go b/pkg/registry/controller/etcd/etcd_test.go index 00e583c4db..37bae1a94b 100644 --- a/pkg/registry/controller/etcd/etcd_test.go +++ b/pkg/registry/controller/etcd/etcd_test.go @@ -27,9 +27,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, "") - return NewREST(etcdStorage), fakeClient + storage, statusStorage := NewREST(etcdStorage) + return storage, statusStorage, fakeClient } // createController is a helper function that returns a controller with the updated resource version. @@ -74,7 +75,7 @@ func validNewController() *api.ReplicationController { var validController = validNewController() func TestCreate(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) controller := validNewController() controller.ObjectMeta = api.ObjectMeta{} @@ -93,7 +94,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 @@ -124,13 +125,13 @@ 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(validNewController()) } func TestGenerationNumber(t *testing.T) { - storage, _ := newStorage(t) + storage, _, _ := newStorage(t) modifiedSno := *validNewController() modifiedSno.Generation = 100 modifiedSno.Status.ObservedGeneration = 10 @@ -179,19 +180,19 @@ func TestGenerationNumber(t *testing.T) { } func TestGet(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestGet(validNewController()) } func TestList(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestList(validNewController()) } func TestWatch(t *testing.T) { - storage, fakeClient := newStorage(t) + storage, _, fakeClient := newStorage(t) test := registrytest.New(t, fakeClient, storage.Etcd) test.TestWatch( validController, @@ -220,3 +221,5 @@ func TestWatch(t *testing.T) { }, ) } + +//TODO TestUpdateStatus diff --git a/pkg/registry/controller/strategy.go b/pkg/registry/controller/strategy.go index 1abd72deef..4989f185fa 100644 --- a/pkg/registry/controller/strategy.go +++ b/pkg/registry/controller/strategy.go @@ -50,17 +50,14 @@ func (rcStrategy) PrepareForCreate(obj runtime.Object) { controller.Status = api.ReplicationControllerStatus{} controller.Generation = 1 - controller.Status.ObservedGeneration = 0 } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. func (rcStrategy) PrepareForUpdate(obj, old runtime.Object) { - // TODO: once RC has a status sub-resource we can enable this. - //newController := obj.(*api.ReplicationController) - //oldController := old.(*api.ReplicationController) - //newController.Status = oldController.Status newController := obj.(*api.ReplicationController) oldController := old.(*api.ReplicationController) + // update is not allowed to set status + newController.Status = oldController.Status // Any changes to the spec increment the generation number, any changes to the // status should reflect the generation number of the corresponding object. We push @@ -125,3 +122,20 @@ func MatchController(label labels.Selector, field fields.Selector) generic.Match }, } } + +type rcStatusStrategy struct { + rcStrategy +} + +var StatusStrategy = rcStatusStrategy{Strategy} + +func (rcStatusStrategy) PrepareForUpdate(obj, old runtime.Object) { + newRc := obj.(*api.ReplicationController) + oldRc := old.(*api.ReplicationController) + // update is not allowed to set spec + newRc.Spec = oldRc.Spec +} + +func (rcStatusStrategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList { + return validation.ValidateReplicationControllerStatusUpdate(old.(*api.ReplicationController), obj.(*api.ReplicationController)) +} diff --git a/pkg/registry/experimental/controller/etcd/etcd.go b/pkg/registry/experimental/controller/etcd/etcd.go index a05b2d4d31..37bbf42a1f 100644 --- a/pkg/registry/experimental/controller/etcd/etcd.go +++ b/pkg/registry/experimental/controller/etcd/etcd.go @@ -38,7 +38,9 @@ type ContainerStorage struct { } func NewStorage(s storage.Interface) ContainerStorage { - rcRegistry := controller.NewRegistry(etcd.NewREST(s)) + // scale does not set status, only updates spec so we ignore the status + controllerREST, _ := etcd.NewREST(s) + rcRegistry := controller.NewRegistry(controllerREST) return ContainerStorage{ ReplicationController: &RcREST{},