diff --git a/pkg/api/register.go b/pkg/api/register.go index ef8991d327..b98e96a713 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -50,7 +50,6 @@ func init() { &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, @@ -86,7 +85,6 @@ func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index d1b78a9d7b..bfb97303cf 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1390,16 +1390,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index ff7a50e56d..2a79913a64 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -848,6 +848,9 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } + if err := s.Convert(&in.Labels, &out.Labels, 0); err != nil { + return err + } return nil }, func(in *ResourceQuota, out *newer.ResourceQuota, s conversion.Scope) error { @@ -863,29 +866,7 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } - return nil - }, - - func(in *newer.ResourceQuotaUsage, out *ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil - }, - func(in *ResourceQuotaUsage, out *newer.ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 53867530c7..faa32dd3fd 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -57,7 +57,6 @@ func init() { &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, @@ -93,7 +92,6 @@ func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 0bd7a80c8d..161a5b0347 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -1159,6 +1159,7 @@ type ResourceQuotaStatus struct { // ResourceQuota sets aggregate quota restrictions enforced per namespace type ResourceQuota struct { TypeMeta `json:",inline"` + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize resource quotas"` // Spec defines the desired quota Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota"` @@ -1167,15 +1168,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index e763b235e6..6969aa88e5 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -768,6 +768,9 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } + if err := s.Convert(&in.Labels, &out.Labels, 0); err != nil { + return err + } return nil }, func(in *ResourceQuota, out *newer.ResourceQuota, s conversion.Scope) error { @@ -783,29 +786,7 @@ func init() { if err := s.Convert(&in.Status, &out.Status, 0); err != nil { return err } - return nil - }, - - func(in *newer.ResourceQuotaUsage, out *ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil - }, - func(in *ResourceQuotaUsage, out *newer.ResourceQuotaUsage, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.TypeMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + if err := s.Convert(&in.Labels, &out.ObjectMeta.Labels, 0); err != nil { return err } return nil diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 64499f038a..39ddf91946 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -57,7 +57,6 @@ func init() { &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, @@ -93,7 +92,6 @@ func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index a9c2784f6c..ad967a6fae 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -1221,7 +1221,7 @@ type ResourceQuotaStatus struct { // ResourceQuota sets aggregate quota restrictions enforced per namespace type ResourceQuota struct { TypeMeta `json:",inline"` - + Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize resource quotas"` // Spec defines the desired quota Spec ResourceQuotaSpec `json:"spec,omitempty" description:"spec defines the desired quota"` @@ -1229,15 +1229,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index eb38ed3634..cb550b9c5b 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -51,7 +51,6 @@ func init() { &LimitRangeList{}, &ResourceQuota{}, &ResourceQuotaList{}, - &ResourceQuotaUsage{}, &Namespace{}, &NamespaceList{}, &Secret{}, @@ -87,7 +86,6 @@ func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} func (*ResourceQuota) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {} -func (*ResourceQuotaUsage) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index d8d2f4fd1e..f89b2c525f 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1308,16 +1308,6 @@ type ResourceQuota struct { Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` } -// ResourceQuotaUsage captures system observed quota status per namespace -// It is used to enforce atomic updates of a backing ResourceQuota.Status field in storage -type ResourceQuotaUsage struct { - TypeMeta `json:",inline"` - ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#metadata"` - - // Status defines the actual enforced quota and its current usage - Status ResourceQuotaStatus `json:"status,omitempty" description:"status defines the actual enforced quota and current usage; https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/api-conventions.md#spec-and-status"` -} - // ResourceQuotaList is a list of ResourceQuota items type ResourceQuotaList struct { TypeMeta `json:",inline"` diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 26737b859b..170b2adcda 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -972,6 +972,36 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro return allErrs } +// ValidateResourceQuotaUpdate tests to see if the update is legal for an end user to make. +// newResourceQuota is updated with fields that cannot be changed. +func ValidateResourceQuotaUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldResourceQuota.ObjectMeta, &newResourceQuota.ObjectMeta).Prefix("metadata")...) + for k := range newResourceQuota.Spec.Hard { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + newResourceQuota.Status = oldResourceQuota.Status + return allErrs +} + +// ValidateResourceQuotaStatusUpdate tests to see if the status update is legal for an end user to make. +// newResourceQuota is updated with fields that cannot be changed. +func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldResourceQuota.ObjectMeta, &newResourceQuota.ObjectMeta).Prefix("metadata")...) + if newResourceQuota.ResourceVersion == "" { + allErrs = append(allErrs, fmt.Errorf("ResourceVersion must be specified")) + } + for k := range newResourceQuota.Status.Hard { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + for k := range newResourceQuota.Status.Used { + allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + } + newResourceQuota.Spec = oldResourceQuota.Spec + return allErrs +} + // ValidateNamespace tests if required fields are set. func ValidateNamespace(namespace *api.Namespace) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} diff --git a/pkg/client/client.go b/pkg/client/client.go index d12bebd741..6d88b12d1e 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -39,7 +39,6 @@ type Interface interface { EventNamespacer LimitRangesNamespacer ResourceQuotasNamespacer - ResourceQuotaUsagesNamespacer SecretsNamespacer NamespacesInterface } @@ -76,10 +75,6 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface { return newResourceQuotas(c, namespace) } -func (c *Client) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface { - return newResourceQuotaUsages(c, namespace) -} - func (c *Client) Secrets(namespace string) SecretsInterface { return newSecrets(c, namespace) } diff --git a/pkg/client/fake.go b/pkg/client/fake.go index 6f0bb6ada9..343adf95dd 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -34,22 +34,22 @@ type FakeAction struct { // Fake implements Interface. Meant to be embedded into a struct to get a default // implementation. This makes faking out just the method you want to test easier. type Fake struct { - Actions []FakeAction - PodsList api.PodList - CtrlList api.ReplicationControllerList - Ctrl api.ReplicationController - ServiceList api.ServiceList - EndpointsList api.EndpointsList - MinionsList api.NodeList - EventsList api.EventList - LimitRangesList api.LimitRangeList - ResourceQuotasList api.ResourceQuotaList - ResourceQuotaUsage api.ResourceQuotaUsage - NamespacesList api.NamespaceList - SecretList api.SecretList - Secret api.Secret - Err error - Watch watch.Interface + Actions []FakeAction + PodsList api.PodList + CtrlList api.ReplicationControllerList + Ctrl api.ReplicationController + ServiceList api.ServiceList + EndpointsList api.EndpointsList + MinionsList api.NodeList + EventsList api.EventList + LimitRangesList api.LimitRangeList + ResourceQuotaStatus api.ResourceQuota + ResourceQuotasList api.ResourceQuotaList + NamespacesList api.NamespaceList + SecretList api.SecretList + Secret api.Secret + Err error + Watch watch.Interface } func (c *Fake) LimitRanges(namespace string) LimitRangeInterface { @@ -60,10 +60,6 @@ func (c *Fake) ResourceQuotas(namespace string) ResourceQuotaInterface { return &FakeResourceQuotas{Fake: c, Namespace: namespace} } -func (c *Fake) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface { - return &FakeResourceQuotaUsages{Fake: c, Namespace: namespace} -} - func (c *Fake) ReplicationControllers(namespace string) ReplicationControllerInterface { return &FakeReplicationControllers{Fake: c, Namespace: namespace} } diff --git a/pkg/client/fake_resource_quota_usages.go b/pkg/client/fake_resource_quota_usages.go deleted file mode 100644 index 102fac1b27..0000000000 --- a/pkg/client/fake_resource_quota_usages.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// FakeResourceQuotaUsages implements ResourceQuotaUsageInterface. Meant to be embedded into a struct to get a default -// implementation. This makes faking out just the methods you want to test easier. -type FakeResourceQuotaUsages struct { - Fake *Fake - Namespace string -} - -func (c *FakeResourceQuotaUsages) Create(resourceQuotaUsage *api.ResourceQuotaUsage) error { - c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-resourceQuotaUsage"}) - c.Fake.ResourceQuotaUsage = *resourceQuotaUsage - return nil -} diff --git a/pkg/client/fake_resource_quotas.go b/pkg/client/fake_resource_quotas.go index e24c160d49..e5ded99edb 100644 --- a/pkg/client/fake_resource_quotas.go +++ b/pkg/client/fake_resource_quotas.go @@ -54,6 +54,12 @@ func (c *FakeResourceQuotas) Update(resourceQuota *api.ResourceQuota) (*api.Reso return &api.ResourceQuota{}, nil } +func (c *FakeResourceQuotas) Status(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-status-resourceQuota", Value: resourceQuota.Name}) + c.Fake.ResourceQuotaStatus = *resourceQuota + return &api.ResourceQuota{}, nil +} + func (c *FakeResourceQuotas) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-resourceQuota", Value: resourceVersion}) return c.Fake.Watch, nil diff --git a/pkg/client/resource_quota_usages.go b/pkg/client/resource_quota_usages.go deleted file mode 100644 index 1ba2c5e5e8..0000000000 --- a/pkg/client/resource_quota_usages.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "fmt" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// ResourceQuotaUsagesNamespacer has methods to work with ResourceQuotaUsage resources in a namespace -type ResourceQuotaUsagesNamespacer interface { - ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface -} - -// ResourceQuotaUsageInterface has methods to work with ResourceQuotaUsage resources. -type ResourceQuotaUsageInterface interface { - Create(resourceQuotaUsage *api.ResourceQuotaUsage) error -} - -// resourceQuotaUsages implements ResourceQuotaUsagesNamespacer interface -type resourceQuotaUsages struct { - r *Client - ns string -} - -// newResourceQuotaUsages returns a resourceQuotaUsages -func newResourceQuotaUsages(c *Client, namespace string) *resourceQuotaUsages { - return &resourceQuotaUsages{ - r: c, - ns: namespace, - } -} - -// Create takes the representation of a resourceQuotaUsage. Returns an error if the usage was not applied -func (c *resourceQuotaUsages) Create(resourceQuotaUsage *api.ResourceQuotaUsage) (err error) { - if len(resourceQuotaUsage.ResourceVersion) == 0 { - err = fmt.Errorf("invalid update object, missing resource version: %v", resourceQuotaUsage) - return - } - err = c.r.Post().Namespace(c.ns).Resource("resourceQuotaUsages").Body(resourceQuotaUsage).Do().Error() - return -} diff --git a/pkg/client/resource_quota_usages_test.go b/pkg/client/resource_quota_usages_test.go deleted file mode 100644 index eb2c65f1bc..0000000000 --- a/pkg/client/resource_quota_usages_test.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package client - -import ( - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" -) - -func TestResourceQuotaUsageCreate(t *testing.T) { - ns := api.NamespaceDefault - resourceQuotaUsage := &api.ResourceQuotaUsage{ - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "foo", - ResourceVersion: "1", - }, - Status: api.ResourceQuotaStatus{ - Hard: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100"), - api.ResourceMemory: resource.MustParse("10000"), - api.ResourcePods: resource.MustParse("10"), - api.ResourceServices: resource.MustParse("10"), - api.ResourceReplicationControllers: resource.MustParse("10"), - api.ResourceQuotas: resource.MustParse("10"), - }, - }, - } - c := &testClient{ - Request: testRequest{ - Method: "POST", - Path: buildResourcePath(ns, "/resourceQuotaUsages"), - Query: buildQueryValues(ns, nil), - Body: resourceQuotaUsage, - }, - Response: Response{StatusCode: 200, Body: resourceQuotaUsage}, - } - - err := c.Setup().ResourceQuotaUsages(ns).Create(resourceQuotaUsage) - if err != nil { - t.Errorf("Unexpected error %v", err) - } -} - -func TestInvalidResourceQuotaUsageCreate(t *testing.T) { - ns := api.NamespaceDefault - resourceQuotaUsage := &api.ResourceQuotaUsage{ - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "foo", - }, - Status: api.ResourceQuotaStatus{ - Hard: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100"), - api.ResourceMemory: resource.MustParse("10000"), - api.ResourcePods: resource.MustParse("10"), - api.ResourceServices: resource.MustParse("10"), - api.ResourceReplicationControllers: resource.MustParse("10"), - api.ResourceQuotas: resource.MustParse("10"), - }, - }, - } - c := &testClient{ - Request: testRequest{ - Method: "POST", - Path: buildResourcePath(ns, "/resourceQuotaUsages"), - Query: buildQueryValues(ns, nil), - Body: resourceQuotaUsage, - }, - Response: Response{StatusCode: 200, Body: resourceQuotaUsage}, - } - - err := c.Setup().ResourceQuotaUsages(ns).Create(resourceQuotaUsage) - if err == nil { - t.Errorf("Expected error due to missing ResourceVersion") - } -} diff --git a/pkg/client/resource_quotas.go b/pkg/client/resource_quotas.go index cced1e40f6..5aa97e9993 100644 --- a/pkg/client/resource_quotas.go +++ b/pkg/client/resource_quotas.go @@ -37,6 +37,7 @@ type ResourceQuotaInterface interface { Delete(name string) error Create(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) Update(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) + Status(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) } @@ -84,7 +85,7 @@ func (c *resourceQuotas) Create(resourceQuota *api.ResourceQuota) (result *api.R return } -// Update takes the representation of a resourceQuota to update. Returns the server's representation of the resourceQuota, and an error, if it occurs. +// Update takes the representation of a resourceQuota to update spec. Returns the server's representation of the resourceQuota, and an error, if it occurs. func (c *resourceQuotas) Update(resourceQuota *api.ResourceQuota) (result *api.ResourceQuota, err error) { result = &api.ResourceQuota{} if len(resourceQuota.ResourceVersion) == 0 { @@ -95,6 +96,17 @@ func (c *resourceQuotas) Update(resourceQuota *api.ResourceQuota) (result *api.R return } +// Status takes the representation of a resourceQuota to update status. Returns the server's representation of the resourceQuota, and an error, if it occurs. +func (c *resourceQuotas) Status(resourceQuota *api.ResourceQuota) (result *api.ResourceQuota, err error) { + result = &api.ResourceQuota{} + if len(resourceQuota.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", resourceQuota) + return + } + err = c.r.Put().Namespace(c.ns).Resource("resourceQuotas").Name(resourceQuota.Name).SubResource("status").Body(resourceQuota).Do().Into(result) + return +} + // Watch returns a watch.Interface that watches the requested resource func (c *resourceQuotas) Watch(label, field labels.Selector, resourceVersion string) (watch.Interface, error) { return c.r.Get(). diff --git a/pkg/client/resource_quotas_test.go b/pkg/client/resource_quotas_test.go index e5b406a0b1..010dd8ed5b 100644 --- a/pkg/client/resource_quotas_test.go +++ b/pkg/client/resource_quotas_test.go @@ -139,6 +139,33 @@ func TestResourceQuotaUpdate(t *testing.T) { c.Validate(t, response, err) } +func TestResourceQuotaStatusUpdate(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + ResourceVersion: "1", + }, + Status: api.ResourceQuotaStatus{ + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("10"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("10"), + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/resourceQuotas/abc/status"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + response, err := c.Setup().ResourceQuotas(ns).Status(resourceQuota) + c.Validate(t, response, err) +} + func TestInvalidResourceQuotaUpdate(t *testing.T) { ns := api.NamespaceDefault resourceQuota := &api.ResourceQuota{ diff --git a/pkg/master/master.go b/pkg/master/master.go index 21dd5ada98..574ce24bde 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -51,8 +51,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" podetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod/etcd" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage" + resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" @@ -377,7 +376,8 @@ func (m *Master) init(c *Config) { eventRegistry := event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())) limitRangeRegistry := limitrange.NewEtcdRegistry(c.EtcdHelper) - resourceQuotaRegistry := resourcequota.NewEtcdRegistry(c.EtcdHelper) + + resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewREST(c.EtcdHelper) secretRegistry := secret.NewEtcdRegistry(c.EtcdHelper) m.namespaceRegistry = namespace.NewEtcdRegistry(c.EtcdHelper) @@ -418,11 +418,11 @@ func (m *Master) init(c *Config) { "nodes": nodeStorage, "events": event.NewREST(eventRegistry), - "limitRanges": limitrange.NewREST(limitRangeRegistry), - "resourceQuotas": resourcequota.NewREST(resourceQuotaRegistry), - "resourceQuotaUsages": resourcequotausage.NewREST(resourceQuotaRegistry), - "namespaces": namespace.NewREST(m.namespaceRegistry), - "secrets": secret.NewREST(secretRegistry), + "limitRanges": limitrange.NewREST(limitRangeRegistry), + "resourceQuotas": resourceQuotaStorage, + "resourceQuotas/status": resourceQuotaStatusStorage, + "namespaces": namespace.NewREST(m.namespaceRegistry), + "secrets": secret.NewREST(secretRegistry), } apiVersions := []string{"v1beta1", "v1beta2"} diff --git a/pkg/registry/resourcequota/etcd/etcd.go b/pkg/registry/resourcequota/etcd/etcd.go new file mode 100644 index 0000000000..5e71cdc328 --- /dev/null +++ b/pkg/registry/resourcequota/etcd/etcd.go @@ -0,0 +1,121 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" +) + +// rest implements a RESTStorage for resourcequotas against etcd +type REST struct { + store *etcdgeneric.Etcd +} + +// NewREST returns a RESTStorage object that will work against ResourceQuota objects. +func NewREST(h tools.EtcdHelper) (*REST, *StatusREST) { + prefix := "/registry/resourcequotas" + store := &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.ResourceQuota{} }, + NewListFunc: func() runtime.Object { return &api.ResourceQuotaList{} }, + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, prefix) + }, + KeyFunc: func(ctx api.Context, name string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, prefix, name) + }, + ObjectNameFunc: func(obj runtime.Object) (string, error) { + return obj.(*api.ResourceQuota).Name, nil + }, + PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher { + return resourcequota.MatchResourceQuota(label, field) + }, + EndpointName: "resourcequotas", + + Helper: h, + } + + store.CreateStrategy = resourcequota.Strategy + store.UpdateStrategy = resourcequota.Strategy + store.ReturnDeletedObject = true + + statusStore := *store + statusStore.UpdateStrategy = resourcequota.StatusStrategy + + return &REST{store: store}, &StatusREST{store: &statusStore} +} + +// New returns a new object +func (r *REST) New() runtime.Object { + return r.store.NewFunc() +} + +// NewList returns a new list object +func (r *REST) NewList() runtime.Object { + return r.store.NewListFunc() +} + +// List obtains a list of resourcequotas with labels that match selector. +func (r *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { + return r.store.List(ctx, label, field) +} + +// Watch begins watching for new, changed, or deleted resourcequotas. +func (r *REST) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return r.store.Watch(ctx, label, field, resourceVersion) +} + +// Get gets a specific resourcequota specified by its ID. +func (r *REST) Get(ctx api.Context, name string) (runtime.Object, error) { + return r.store.Get(ctx, name) +} + +// Create creates a resourcequota based on a specification. +func (r *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { + return r.store.Create(ctx, obj) +} + +// Update changes a resourcequota specification. +func (r *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { + return r.store.Update(ctx, obj) +} + +// Delete deletes an existing resourcequota specified by its name. +func (r *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { + return r.store.Delete(ctx, name) +} + +// StatusREST implements the REST endpoint for changing the status of a resourcequota. +type StatusREST struct { + store *etcdgeneric.Etcd +} + +func (r *StatusREST) New() runtime.Object { + return &api.ResourceQuota{} +} + +// 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/resourcequota/etcd/etcd_test.go b/pkg/registry/resourcequota/etcd/etcd_test.go new file mode 100644 index 0000000000..770e1ceb62 --- /dev/null +++ b/pkg/registry/resourcequota/etcd/etcd_test.go @@ -0,0 +1,678 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package etcd + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/coreos/go-etcd/etcd" +) + +func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient := tools.NewFakeEtcdClient(t) + fakeEtcdClient.TestIndex = true + helper := tools.EtcdHelper{Client: fakeEtcdClient, Codec: latest.Codec, ResourceVersioner: tools.RuntimeVersionAdapter{latest.ResourceVersioner}} + return fakeEtcdClient, helper +} + +func newStorage(t *testing.T) (*REST, *StatusREST, *tools.FakeEtcdClient, tools.EtcdHelper) { + fakeEtcdClient, h := newHelper(t) + storage, statusStorage := NewREST(h) + return storage, statusStorage, fakeEtcdClient, h +} + +func validNewResourceQuota() *api.ResourceQuota { + return &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: api.ResourceQuotaSpec{ + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("4Gi"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("10"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("1"), + }, + }, + } +} + +func validChangedResourceQuota() *api.ResourceQuota { + resourcequota := validNewResourceQuota() + resourcequota.ResourceVersion = "1" + resourcequota.Labels = map[string]string{ + "foo": "bar", + } + return resourcequota +} + +func TestStorage(t *testing.T) { + storage, _, _, _ := newStorage(t) + resourcequota.NewRegistry(storage) +} + +func TestCreate(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage, _ := NewREST(helper) + test := resttest.New(t, storage, fakeEtcdClient.SetError) + resourcequota := validNewResourceQuota() + resourcequota.ObjectMeta = api.ObjectMeta{} + test.TestCreate( + // valid + resourcequota, + // invalid + &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "_-a123-a_"}, + }, + ) +} + +func expectResourceQuota(t *testing.T, out runtime.Object) (*api.ResourceQuota, bool) { + resourcequota, ok := out.(*api.ResourceQuota) + if !ok || resourcequota == nil { + t.Errorf("Expected an api.ResourceQuota object, was %#v", out) + return nil, false + } + return resourcequota, true +} + +func TestCreateRegistryError(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Err = fmt.Errorf("test error") + storage, _ := NewREST(helper) + + resourcequota := validNewResourceQuota() + _, err := storage.Create(api.NewDefaultContext(), resourcequota) + if err != fakeEtcdClient.Err { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestCreateSetsFields(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + storage, _ := NewREST(helper) + resourcequota := validNewResourceQuota() + _, err := storage.Create(api.NewDefaultContext(), resourcequota) + if err != fakeEtcdClient.Err { + t.Fatalf("unexpected error: %v", err) + } + + actual := &api.ResourceQuota{} + if err := helper.ExtractObj("/registry/resourcequotas/default/foo", actual, false); err != nil { + t.Fatalf("unexpected extraction error: %v", err) + } + if actual.Name != resourcequota.Name { + t.Errorf("unexpected resourcequota: %#v", actual) + } + if len(actual.UID) == 0 { + t.Errorf("expected resourcequota UID to be set: %#v", actual) + } +} + +func TestListError(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Err = fmt.Errorf("test error") + storage, _ := NewREST(helper) + resourcequotas, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything()) + if err != fakeEtcdClient.Err { + t.Fatalf("Expected %#v, Got %#v", fakeEtcdClient.Err, err) + } + if resourcequotas != nil { + t.Errorf("Unexpected non-nil resourcequota list: %#v", resourcequotas) + } +} + +func TestListEmptyResourceQuotaList(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.ChangeIndex = 1 + fakeEtcdClient.Data["/registry/resourcequotas"] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: fakeEtcdClient.NewError(tools.EtcdErrorCodeNotFound), + } + + storage, _ := NewREST(helper) + resourcequotas, err := storage.List(api.NewContext(), labels.Everything(), fields.Everything()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(resourcequotas.(*api.ResourceQuotaList).Items) != 0 { + t.Errorf("Unexpected non-zero resourcequota list: %#v", resourcequotas) + } + if resourcequotas.(*api.ResourceQuotaList).ResourceVersion != "1" { + t.Errorf("Unexpected resource version: %#v", resourcequotas) + } +} + +func TestListResourceQuotaList(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/resourcequotas/default"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + } + storage, _ := NewREST(helper) + resourcequotasObj, err := storage.List(api.NewDefaultContext(), labels.Everything(), fields.Everything()) + resourcequotas := resourcequotasObj.(*api.ResourceQuotaList) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(resourcequotas.Items) != 2 { + t.Errorf("Unexpected resourcequota list: %#v", resourcequotas) + } + if resourcequotas.Items[0].Name != "foo" { + t.Errorf("Unexpected resourcequota: %#v", resourcequotas.Items[0]) + } + if resourcequotas.Items[1].Name != "bar" { + t.Errorf("Unexpected resourcequota: %#v", resourcequotas.Items[1]) + } +} + +func TestListResourceQuotaListSelection(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/resourcequotas/default"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + {Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "qux", + Labels: map[string]string{"label": "qux"}, + }, + })}, + {Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "zot"}, + })}, + }, + }, + }, + } + storage, _ := NewREST(helper) + ctx := api.NewDefaultContext() + + table := []struct { + label, field string + expectedIDs util.StringSet + }{ + { + expectedIDs: util.NewStringSet("foo", "qux", "zot"), + }, { + field: "name=zot", + expectedIDs: util.NewStringSet("zot"), + }, { + label: "label=qux", + expectedIDs: util.NewStringSet("qux"), + }, + } + + for index, item := range table { + label, err := labels.Parse(item.label) + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + field, err := fields.ParseSelector(item.field) + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + resourcequotasObj, err := storage.List(ctx, label, field) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequotas := resourcequotasObj.(*api.ResourceQuotaList) + + set := util.NewStringSet() + for i := range resourcequotas.Items { + set.Insert(resourcequotas.Items[i].Name) + } + if e, a := len(item.expectedIDs), len(set); e != a { + t.Errorf("%v: Expected %v, got %v", index, item.expectedIDs, set) + } + } +} + +func TestResourceQuotaDecode(t *testing.T) { + storage, _ := NewREST(tools.EtcdHelper{}) + expected := validNewResourceQuota() + body, err := latest.Codec.Encode(expected) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual := storage.New() + if err := latest.Codec.DecodeInto(body, actual); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !api.Semantic.DeepEqual(expected, actual) { + t.Errorf("mismatch: %s", util.ObjectDiff(expected, actual)) + } +} + +func TestGet(t *testing.T) { + expect := validNewResourceQuota() + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.Data["/registry/resourcequotas/test/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, expect), + }, + }, + } + storage, _ := NewREST(helper) + obj, err := storage.Get(api.WithNamespace(api.NewContext(), "test"), "foo") + resourcequota := obj.(*api.ResourceQuota) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if e, a := expect, resourcequota; !api.Semantic.DeepEqual(e, a) { + t.Errorf("Unexpected resourcequota: %s", util.ObjectDiff(e, a)) + } +} + +func TestDeleteResourceQuota(t *testing.T) { + fakeEtcdClient, helper := newHelper(t) + fakeEtcdClient.ChangeIndex = 1 + fakeEtcdClient.Data["/registry/resourcequotas/default/foo"] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Status: api.ResourceQuotaStatus{}, + }), + ModifiedIndex: 1, + CreatedIndex: 1, + }, + }, + } + storage, _ := NewREST(helper) + _, err := storage.Delete(api.NewDefaultContext(), "foo") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestEtcdGetDifferentNamespace ensures same-name resourcequotas in different namespaces do not clash +func TestEtcdGetDifferentNamespace(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + + ctx1 := api.NewDefaultContext() + ctx2 := api.WithNamespace(api.NewContext(), "other") + + key1, _ := registry.store.KeyFunc(ctx1, "foo") + key2, _ := registry.store.KeyFunc(ctx2, "foo") + + fakeClient.Set(key1, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Namespace: "default", Name: "foo"}}), 0) + fakeClient.Set(key2, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Namespace: "other", Name: "foo"}}), 0) + + obj, err := registry.Get(ctx1, "foo") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequota1 := obj.(*api.ResourceQuota) + if resourcequota1.Name != "foo" { + t.Errorf("Unexpected resourcequota: %#v", resourcequota1) + } + if resourcequota1.Namespace != "default" { + t.Errorf("Unexpected resourcequota: %#v", resourcequota1) + } + + obj, err = registry.Get(ctx2, "foo") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequota2 := obj.(*api.ResourceQuota) + if resourcequota2.Name != "foo" { + t.Errorf("Unexpected resourcequota: %#v", resourcequota2) + } + if resourcequota2.Namespace != "other" { + t.Errorf("Unexpected resourcequota: %#v", resourcequota2) + } + +} + +func TestEtcdGet(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key, _ := registry.store.KeyFunc(ctx, "foo") + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "foo"}}), 0) + obj, err := registry.Get(ctx, "foo") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequota := obj.(*api.ResourceQuota) + if resourcequota.Name != "foo" { + t.Errorf("Unexpected resourcequota: %#v", resourcequota) + } +} + +func TestEtcdGetNotFound(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key, _ := registry.store.KeyFunc(ctx, "foo") + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: nil, + }, + E: tools.EtcdErrorNotFound, + } + _, err := registry.Get(ctx, "foo") + if !errors.IsNotFound(err) { + t.Errorf("Unexpected error returned: %#v", err) + } +} + +func TestEtcdCreateFailsWithoutNamespace(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + fakeClient.TestIndex = true + resourcequota := validNewResourceQuota() + resourcequota.Namespace = "" + _, err := registry.Create(api.NewContext(), resourcequota) + // Accept "namespace" or "Namespace". + if err == nil || !strings.Contains(err.Error(), "amespace") { + t.Fatalf("expected error that namespace was missing from context, got: %v", err) + } +} + +func TestEtcdCreateAlreadyExisting(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key, _ := registry.store.KeyFunc(ctx, "foo") + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "foo"}}), + }, + }, + E: nil, + } + _, err := registry.Create(ctx, validNewResourceQuota()) + if !errors.IsAlreadyExists(err) { + t.Errorf("Unexpected error returned: %#v", err) + } +} + +func TestEtcdUpdateStatus(t *testing.T) { + registry, status, fakeClient, helper := newStorage(t) + ctx := api.NewDefaultContext() + fakeClient.TestIndex = true + + key, _ := registry.store.KeyFunc(ctx, "foo") + resourcequotaStart := validNewResourceQuota() + fakeClient.Set(key, runtime.EncodeOrDie(latest.Codec, resourcequotaStart), 1) + + resourcequotaIn := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + ResourceVersion: "1", + }, + Status: api.ResourceQuotaStatus{ + Used: api.ResourceList{ + api.ResourceCPU: resource.MustParse("1"), + api.ResourceMemory: resource.MustParse("1Gi"), + api.ResourcePods: resource.MustParse("1"), + api.ResourceServices: resource.MustParse("1"), + api.ResourceReplicationControllers: resource.MustParse("1"), + api.ResourceQuotas: resource.MustParse("1"), + }, + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("4Gi"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("10"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("1"), + }, + }, + } + + expected := *resourcequotaStart + expected.ResourceVersion = "2" + expected.Labels = resourcequotaIn.Labels + expected.Status = resourcequotaIn.Status + + _, _, err := status.Update(ctx, resourcequotaIn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + var resourcequotaOut api.ResourceQuota + if err := helper.ExtractObj(key, &resourcequotaOut, false); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if !api.Semantic.DeepEqual(expected, resourcequotaOut) { + t.Errorf("unexpected object: %s", util.ObjectDiff(expected, resourcequotaOut)) + } +} + +func TestEtcdEmptyList(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key := registry.store.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{}, + }, + }, + E: nil, + } + + obj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequotas := obj.(*api.ResourceQuotaList) + if len(resourcequotas.Items) != 0 { + t.Errorf("Unexpected resourcequota list: %#v", resourcequotas) + } +} + +func TestEtcdListNotFound(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key := registry.store.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: tools.EtcdErrorNotFound, + } + obj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequotas := obj.(*api.ResourceQuotaList) + if len(resourcequotas.Items) != 0 { + t.Errorf("Unexpected resourcequota list: %#v", resourcequotas) + } +} + +func TestEtcdList(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + key := registry.store.KeyRootFunc(ctx) + fakeClient.Data[key] = tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Nodes: []*etcd.Node{ + { + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }), + }, + { + Value: runtime.EncodeOrDie(latest.Codec, &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "bar"}, + }), + }, + }, + }, + }, + E: nil, + } + obj, err := registry.List(ctx, labels.Everything(), fields.Everything()) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + resourcequotas := obj.(*api.ResourceQuotaList) + + if len(resourcequotas.Items) != 2 || resourcequotas.Items[0].Name != "foo" || resourcequotas.Items[1].Name != "bar" { + t.Errorf("Unexpected resourcequota list: %#v", resourcequotas) + } +} + +func TestEtcdWatchResourceQuotas(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + watching, err := registry.Watch(ctx, + labels.Everything(), + fields.Everything(), + "1", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + fakeClient.WaitForWatchCompletion() + + select { + case _, ok := <-watching.ResultChan(): + if !ok { + t.Errorf("watching channel should be open") + } + default: + } + fakeClient.WatchInjectError <- nil + if _, ok := <-watching.ResultChan(); ok { + t.Errorf("watching channel should be closed") + } + watching.Stop() +} + +func TestEtcdWatchResourceQuotasMatch(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + watching, err := registry.Watch(ctx, + labels.SelectorFromSet(labels.Set{"name": "foo"}), + fields.Everything(), + "1", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + fakeClient.WaitForWatchCompletion() + + resourcequota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{ + "name": "foo", + }, + }, + } + resourcequotaBytes, _ := latest.Codec.Encode(resourcequota) + fakeClient.WatchResponse <- &etcd.Response{ + Action: "create", + Node: &etcd.Node{ + Value: string(resourcequotaBytes), + }, + } + select { + case _, ok := <-watching.ResultChan(): + if !ok { + t.Errorf("watching channel should be open") + } + case <-time.After(time.Millisecond * 100): + t.Error("unexpected timeout from result channel") + } + watching.Stop() +} + +func TestEtcdWatchResourceQuotasNotMatch(t *testing.T) { + registry, _, fakeClient, _ := newStorage(t) + ctx := api.NewDefaultContext() + watching, err := registry.Watch(ctx, + labels.SelectorFromSet(labels.Set{"name": "foo"}), + fields.Everything(), + "1", + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + fakeClient.WaitForWatchCompletion() + + resourcequota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "bar", + Labels: map[string]string{ + "name": "bar", + }, + }, + } + resourcequotaBytes, _ := latest.Codec.Encode(resourcequota) + fakeClient.WatchResponse <- &etcd.Response{ + Action: "create", + Node: &etcd.Node{ + Value: string(resourcequotaBytes), + }, + } + + select { + case <-watching.ResultChan(): + t.Error("unexpected result from result channel") + case <-time.After(time.Millisecond * 100): + // expected case + } +} diff --git a/pkg/registry/resourcequota/registry.go b/pkg/registry/resourcequota/registry.go index caeca32556..8ab0f38812 100644 --- a/pkg/registry/resourcequota/registry.go +++ b/pkg/registry/resourcequota/registry.go @@ -17,59 +17,84 @@ limitations under the License. package resourcequota import ( - "fmt" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" - etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" + "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -// Registry implements operations to modify ResourceQuota objects +// Registry is an interface implemented by things that know how to store ResourceQuota objects. type Registry interface { - generic.Registry - resourcequotausage.Registry + // ListResourceQuotas obtains a list of pods having labels which match selector. + ListResourceQuotas(ctx api.Context, selector labels.Selector) (*api.ResourceQuotaList, error) + // Watch for new/changed/deleted pods + WatchResourceQuotas(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + // Get a specific pod + GetResourceQuota(ctx api.Context, podID string) (*api.ResourceQuota, error) + // Create a pod based on a specification. + CreateResourceQuota(ctx api.Context, pod *api.ResourceQuota) error + // Update an existing pod + UpdateResourceQuota(ctx api.Context, pod *api.ResourceQuota) error + // Delete an existing pod + DeleteResourceQuota(ctx api.Context, podID string) error } -// registry implements custom changes to generic.Etcd. -type registry struct { - *etcdgeneric.Etcd +// Storage is an interface for a standard REST Storage backend +// TODO: move me somewhere common +type Storage interface { + apiserver.RESTDeleter + apiserver.RESTLister + apiserver.RESTGetter + apiserver.ResourceWatcher + + Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) + Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) } -// ApplyStatus atomically updates the ResourceQuotaStatus based on the observed ResourceQuotaUsage -func (r *registry) ApplyStatus(ctx api.Context, usage *api.ResourceQuotaUsage) error { - obj, err := r.Get(ctx, usage.Name) +// storage puts strong typing around storage calls +type storage struct { + Storage +} + +// NewRegistry returns a new Registry interface for the given Storage. Any mismatched +// types will panic. +func NewRegistry(s Storage) Registry { + return &storage{s} +} + +func (s *storage) ListResourceQuotas(ctx api.Context, label labels.Selector) (*api.ResourceQuotaList, error) { + obj, err := s.List(ctx, label, fields.Everything()) if err != nil { - return err + return nil, err } - - if len(usage.ResourceVersion) == 0 { - return fmt.Errorf("A resource observation must have a resourceVersion specified to ensure atomic updates") - } - - // set the status - resourceQuota := obj.(*api.ResourceQuota) - resourceQuota.ResourceVersion = usage.ResourceVersion - resourceQuota.Status = usage.Status - return r.UpdateWithName(ctx, resourceQuota.Name, resourceQuota) + return obj.(*api.ResourceQuotaList), nil } -// NewEtcdRegistry returns a registry which will store ResourceQuota in the given helper -func NewEtcdRegistry(h tools.EtcdHelper) Registry { - return ®istry{ - Etcd: &etcdgeneric.Etcd{ - NewFunc: func() runtime.Object { return &api.ResourceQuota{} }, - NewListFunc: func() runtime.Object { return &api.ResourceQuotaList{} }, - EndpointName: "resourcequotas", - KeyRootFunc: func(ctx api.Context) string { - return etcdgeneric.NamespaceKeyRootFunc(ctx, "/registry/resourcequotas") - }, - KeyFunc: func(ctx api.Context, id string) (string, error) { - return etcdgeneric.NamespaceKeyFunc(ctx, "/registry/resourcequotas", id) - }, - Helper: h, - }, - } +func (s *storage) WatchResourceQuotas(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { + return s.Watch(ctx, label, field, resourceVersion) +} + +func (s *storage) GetResourceQuota(ctx api.Context, podID string) (*api.ResourceQuota, error) { + obj, err := s.Get(ctx, podID) + if err != nil { + return nil, err + } + return obj.(*api.ResourceQuota), nil +} + +func (s *storage) CreateResourceQuota(ctx api.Context, pod *api.ResourceQuota) error { + _, err := s.Create(ctx, pod) + return err +} + +func (s *storage) UpdateResourceQuota(ctx api.Context, pod *api.ResourceQuota) error { + _, _, err := s.Update(ctx, pod) + return err +} + +func (s *storage) DeleteResourceQuota(ctx api.Context, podID string) error { + _, err := s.Delete(ctx, podID) + return err } diff --git a/pkg/registry/resourcequota/registry_test.go b/pkg/registry/resourcequota/registry_test.go deleted file mode 100644 index 7222cb776d..0000000000 --- a/pkg/registry/resourcequota/registry_test.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resourcequota - -import ( - "reflect" - "testing" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" - etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - - "github.com/coreos/go-etcd/etcd" -) - -func NewTestLimitRangeEtcdRegistry(t *testing.T) (*tools.FakeEtcdClient, generic.Registry) { - f := tools.NewFakeEtcdClient(t) - f.TestIndex = true - h := tools.EtcdHelper{f, testapi.Codec(), tools.RuntimeVersionAdapter{testapi.MetadataAccessor()}} - return f, NewEtcdRegistry(h) -} - -func TestResourceQuotaCreate(t *testing.T) { - resourceQuota := &api.ResourceQuota{ - ObjectMeta: api.ObjectMeta{ - Name: "abc", - Namespace: "default", - }, - Spec: api.ResourceQuotaSpec{ - Hard: api.ResourceList{ - api.ResourceCPU: resource.MustParse("100"), - api.ResourceMemory: resource.MustParse("10000"), - api.ResourcePods: resource.MustParse("10"), - api.ResourceServices: resource.MustParse("10"), - api.ResourceReplicationControllers: resource.MustParse("10"), - api.ResourceQuotas: resource.MustParse("10"), - }, - }, - } - - nodeWithResourceQuota := tools.EtcdResponseWithError{ - R: &etcd.Response{ - Node: &etcd.Node{ - Value: runtime.EncodeOrDie(testapi.Codec(), resourceQuota), - ModifiedIndex: 1, - CreatedIndex: 1, - }, - }, - E: nil, - } - - emptyNode := tools.EtcdResponseWithError{ - R: &etcd.Response{}, - E: tools.EtcdErrorNotFound, - } - - ctx := api.NewDefaultContext() - key := "abc" - path, err := etcdgeneric.NamespaceKeyFunc(ctx, "/registry/resourcequotas", key) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - table := map[string]struct { - existing tools.EtcdResponseWithError - expect tools.EtcdResponseWithError - toCreate runtime.Object - errOK func(error) bool - }{ - "normal": { - existing: emptyNode, - expect: nodeWithResourceQuota, - toCreate: resourceQuota, - errOK: func(err error) bool { return err == nil }, - }, - "preExisting": { - existing: nodeWithResourceQuota, - expect: nodeWithResourceQuota, - toCreate: resourceQuota, - errOK: errors.IsAlreadyExists, - }, - } - - for name, item := range table { - fakeClient, registry := NewTestLimitRangeEtcdRegistry(t) - fakeClient.Data[path] = item.existing - err := registry.CreateWithName(ctx, key, item.toCreate) - if !item.errOK(err) { - t.Errorf("%v: unexpected error: %v, %v", name, err, path) - } - - if e, a := item.expect, fakeClient.Data[path]; !reflect.DeepEqual(e, a) { - t.Errorf("%v:\n%s", name, util.ObjectDiff(e, a)) - } - } -} diff --git a/pkg/registry/resourcequota/rest.go b/pkg/registry/resourcequota/rest.go index 0938b8b1a2..f43c0f971d 100644 --- a/pkg/registry/resourcequota/rest.go +++ b/pkg/registry/resourcequota/rest.go @@ -26,131 +26,71 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" - "github.com/GoogleCloudPlatform/kubernetes/pkg/util" - "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" ) -// REST provides the RESTStorage access patterns to work with ResourceQuota objects. -type REST struct { - registry generic.Registry +// resourcequotaStrategy implements behavior for ResourceQuota objects +type resourcequotaStrategy struct { + runtime.ObjectTyper + api.NameGenerator } -// NewREST returns a new REST. You must use a registry created by -// NewEtcdRegistry unless you're testing. -func NewREST(registry generic.Registry) *REST { - return &REST{ - registry: registry, - } +// Strategy is the default logic that applies when creating and updating ResourceQuota +// objects via the REST API. +var Strategy = resourcequotaStrategy{api.Scheme, api.SimpleNameGenerator} + +// NamespaceScoped is true for resourcequotas. +func (resourcequotaStrategy) NamespaceScoped() bool { + return true } -// Create a ResourceQuota object -func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { - resourceQuota, ok := obj.(*api.ResourceQuota) - if !ok { - return nil, fmt.Errorf("invalid object type") - } - - if !api.ValidNamespace(ctx, &resourceQuota.ObjectMeta) { - return nil, errors.NewConflict("resourceQuota", resourceQuota.Namespace, fmt.Errorf("ResourceQuota.Namespace does not match the provided context")) - } - - if len(resourceQuota.Name) == 0 { - resourceQuota.Name = string(util.NewUUID()) - } - - // callers are not able to set status, instead, it is supplied via a control loop - resourceQuota.Status = api.ResourceQuotaStatus{} - - if errs := validation.ValidateResourceQuota(resourceQuota); len(errs) > 0 { - return nil, errors.NewInvalid("resourceQuota", resourceQuota.Name, errs) - } - api.FillObjectMetaSystemFields(ctx, &resourceQuota.ObjectMeta) - - err := rs.registry.CreateWithName(ctx, resourceQuota.Name, resourceQuota) - if err != nil { - return nil, err - } - return rs.registry.Get(ctx, resourceQuota.Name) +// ResetBeforeCreate clears fields that are not allowed to be set by end users on creation. +func (resourcequotaStrategy) ResetBeforeCreate(obj runtime.Object) { + resourcequota := obj.(*api.ResourceQuota) + resourcequota.Status = api.ResourceQuotaStatus{} } -// Update updates a ResourceQuota object. -func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, bool, error) { - resourceQuota, ok := obj.(*api.ResourceQuota) - if !ok { - return nil, false, fmt.Errorf("invalid object type") - } - - if !api.ValidNamespace(ctx, &resourceQuota.ObjectMeta) { - return nil, false, errors.NewConflict("resourceQuota", resourceQuota.Namespace, fmt.Errorf("ResourceQuota.Namespace does not match the provided context")) - } - - oldObj, err := rs.registry.Get(ctx, resourceQuota.Name) - if err != nil { - return nil, false, err - } - - editResourceQuota := oldObj.(*api.ResourceQuota) - - // set the editable fields on the existing object - editResourceQuota.Labels = resourceQuota.Labels - editResourceQuota.ResourceVersion = resourceQuota.ResourceVersion - editResourceQuota.Annotations = resourceQuota.Annotations - editResourceQuota.Spec = resourceQuota.Spec - - if errs := validation.ValidateResourceQuota(editResourceQuota); len(errs) > 0 { - return nil, false, errors.NewInvalid("resourceQuota", editResourceQuota.Name, errs) - } - - if err := rs.registry.UpdateWithName(ctx, editResourceQuota.Name, editResourceQuota); err != nil { - return nil, false, err - } - out, err := rs.registry.Get(ctx, editResourceQuota.Name) - return out, false, err +// Validate validates a new resourcequota. +func (resourcequotaStrategy) Validate(obj runtime.Object) errors.ValidationErrorList { + resourcequota := obj.(*api.ResourceQuota) + return validation.ValidateResourceQuota(resourcequota) } -// Delete deletes the ResourceQuota with the specified name -func (rs *REST) Delete(ctx api.Context, name string) (runtime.Object, error) { - obj, err := rs.registry.Get(ctx, name) - if err != nil { - return nil, err +// AllowCreateOnUpdate is false for resourcequotas. +func (resourcequotaStrategy) AllowCreateOnUpdate() bool { + return false +} + +// ValidateUpdate is the default update validation for an end user. +func (resourcequotaStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + return validation.ValidateResourceQuotaUpdate(obj.(*api.ResourceQuota), old.(*api.ResourceQuota)) +} + +type resourcequotaStatusStrategy struct { + resourcequotaStrategy +} + +var StatusStrategy = resourcequotaStatusStrategy{Strategy} + +func (resourcequotaStatusStrategy) ValidateUpdate(obj, old runtime.Object) errors.ValidationErrorList { + return validation.ValidateResourceQuotaStatusUpdate(obj.(*api.ResourceQuota), old.(*api.ResourceQuota)) +} + +// MatchResourceQuota returns a generic matcher for a given label and field selector. +func MatchResourceQuota(label labels.Selector, field fields.Selector) generic.Matcher { + return generic.MatcherFunc(func(obj runtime.Object) (bool, error) { + resourcequotaObj, ok := obj.(*api.ResourceQuota) + if !ok { + return false, fmt.Errorf("not a resourcequota") + } + fields := ResourceQuotaToSelectableFields(resourcequotaObj) + return label.Matches(labels.Set(resourcequotaObj.Labels)) && field.Matches(fields), nil + }) +} + +// ResourceQuotaToSelectableFields returns a label set that represents the object +// TODO: fields are not labels, and the validation rules for them do not apply. +func ResourceQuotaToSelectableFields(resourcequota *api.ResourceQuota) labels.Set { + return labels.Set{ + "name": resourcequota.Name, } - _, ok := obj.(*api.ResourceQuota) - if !ok { - return nil, fmt.Errorf("invalid object type") - } - return rs.registry.Delete(ctx, name) -} - -// Get gets a ResourceQuota with the specified name -func (rs *REST) Get(ctx api.Context, name string) (runtime.Object, error) { - obj, err := rs.registry.Get(ctx, name) - if err != nil { - return nil, err - } - resourceQuota, ok := obj.(*api.ResourceQuota) - if !ok { - return nil, fmt.Errorf("invalid object type") - } - return resourceQuota, err -} - -func (rs *REST) getAttrs(obj runtime.Object) (objLabels labels.Set, objFields fields.Set, err error) { - return labels.Set{}, fields.Set{}, nil -} - -func (rs *REST) List(ctx api.Context, label labels.Selector, field fields.Selector) (runtime.Object, error) { - return rs.registry.ListPredicate(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}) -} - -func (rs *REST) Watch(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) { - return rs.registry.WatchPredicate(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}, resourceVersion) -} - -// New returns a new api.ResourceQuota -func (*REST) New() runtime.Object { - return &api.ResourceQuota{} -} - -func (*REST) NewList() runtime.Object { - return &api.ResourceQuotaList{} } diff --git a/pkg/registry/resourcequota/rest_test.go b/pkg/registry/resourcequota/rest_test.go index 3b039b723c..f54eb3a003 100644 --- a/pkg/registry/resourcequota/rest_test.go +++ b/pkg/registry/resourcequota/rest_test.go @@ -17,163 +17,42 @@ limitations under the License. package resourcequota import ( - "fmt" - "reflect" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" - "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) -func makeRegistry(resourceList runtime.Object) (*registrytest.GenericRegistry, *REST) { - registry := registrytest.NewGeneric(resourceList) - rest := NewREST(registry) - return registry, rest -} - -func TestGet(t *testing.T) { - registry, rest := makeRegistry(&api.ResourceQuotaList{}) - registry.Object = &api.ResourceQuota{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Namespace: api.NamespaceDefault, - }, +func TestResourceQuotaStrategy(t *testing.T) { + if !Strategy.NamespaceScoped() { + t.Errorf("ResourceQuota should be namespace scoped") } - ctx := api.NewDefaultContext() - obj, err := rest.Get(ctx, "foo") - if err != nil { - t.Errorf("unexpected error: %v", err) + if Strategy.AllowCreateOnUpdate() { + t.Errorf("ResourceQuota should not allow create on update") } - if obj == nil { - t.Errorf("unexpected nil object") - } - registry.Object = nil - registry.Err = errors.NewNotFound("ResourceQuota", "bar") - - obj, err = rest.Get(ctx, "bar") - if err == nil { - t.Errorf("unexpected non-error") - } - if obj != nil { - t.Errorf("unexpected object: %v", obj) - } - -} - -func TestList(t *testing.T) { - _, rest := makeRegistry(&api.ResourceQuotaList{ - Items: []api.ResourceQuota{ - { - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Namespace: api.NamespaceDefault, - }, - }, - }, - }) - - ctx := api.NewDefaultContext() - obj, err := rest.List(ctx, labels.Set{}.AsSelector(), fields.Set{}.AsSelector()) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if obj == nil { - t.Errorf("unexpected nil object") - } - list, ok := obj.(*api.ResourceQuotaList) - if !ok || len(list.Items) != 1 { - t.Errorf("unexpected list object: %v", obj) - } - - obj, err = rest.List(ctx, labels.Set{"foo": "bar"}.AsSelector(), fields.Set{}.AsSelector()) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if obj == nil { - t.Errorf("unexpected nil object") - } - list, ok = obj.(*api.ResourceQuotaList) - if !ok || len(list.Items) != 0 { - t.Errorf("unexpected list object: %v", obj) - } -} - -func TestUpdate(t *testing.T) { - registry, rest := makeRegistry(&api.ResourceQuotaList{}) - resourceStatus := api.ResourceQuotaStatus{ - Hard: api.ResourceList{ - api.ResourceCPU: *resource.NewQuantity(10.0, resource.BinarySI), - }, - } - registry.Object = &api.ResourceQuota{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Namespace: api.NamespaceDefault, - Labels: map[string]string{ - "bar": "foo", - }, - }, - Status: resourceStatus, - } - invalidUpdates := []struct { - obj runtime.Object - err error - }{ - {&api.Pod{}, nil}, - {&api.ResourceQuota{ObjectMeta: api.ObjectMeta{Namespace: "$%#%"}}, nil}, - {&api.ResourceQuota{ - ObjectMeta: api.ObjectMeta{ - Namespace: api.NamespaceDefault, - }, - }, fmt.Errorf("test error")}, - } - for _, test := range invalidUpdates { - registry.Err = test.err - ctx := api.NewDefaultContext() - _, _, err := rest.Update(ctx, test.obj) - if err == nil { - t.Errorf("unexpected non-error for: %v", test.obj) - } - registry.Err = nil - } - - ctx := api.NewDefaultContext() - update := &api.ResourceQuota{ - ObjectMeta: api.ObjectMeta{ - Name: "foo", - Namespace: api.NamespaceDefault, - Labels: map[string]string{ - "foo": "bar", - }, - }, - Spec: api.ResourceQuotaSpec{ - Hard: api.ResourceList{ - api.ResourceCPU: *resource.NewQuantity(10.0, resource.BinarySI), - }, - }, + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{Name: "foo"}, Status: api.ResourceQuotaStatus{ + Used: api.ResourceList{ + api.ResourceCPU: resource.MustParse("1"), + api.ResourceMemory: resource.MustParse("1Gi"), + api.ResourcePods: resource.MustParse("1"), + api.ResourceServices: resource.MustParse("1"), + api.ResourceReplicationControllers: resource.MustParse("1"), + api.ResourceQuotas: resource.MustParse("1"), + }, Hard: api.ResourceList{ - api.ResourceCPU: *resource.NewQuantity(20.0, resource.BinarySI), + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("4Gi"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("10"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("1"), }, }, } - obj, _, err := rest.Update(ctx, update) - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if !reflect.DeepEqual(obj.(*api.ResourceQuota).Labels, update.Labels) { - t.Errorf("unexpected update object, labels don't match: %v vs %v", obj.(*api.ResourceQuota).Labels, update.Labels) - } - if !reflect.DeepEqual(obj.(*api.ResourceQuota).Spec, update.Spec) { - t.Errorf("unexpected update object, specs don't match: %v vs %v", obj.(*api.ResourceQuota).Spec, update.Spec) - } - if !reflect.DeepEqual(obj.(*api.ResourceQuota).Status, registry.Object.(*api.ResourceQuota).Status) { - t.Errorf("unexpected update object, status wasn't preserved: %v vs %v", obj.(*api.ResourceQuota).Status, registry.Object.(*api.ResourceQuota).Status) + Strategy.ResetBeforeCreate(resourceQuota) + if resourceQuota.Status.Used != nil { + t.Errorf("ResourceQuota does not allow setting status on create") } } diff --git a/pkg/registry/resourcequotausage/doc.go b/pkg/registry/resourcequotausage/doc.go deleted file mode 100644 index 9892fc3cba..0000000000 --- a/pkg/registry/resourcequotausage/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package resourcequotausage provides Registry interface and it's REST -// implementation for storing ResourceQuotaUsage api objects. -package resourcequotausage diff --git a/pkg/registry/resourcequotausage/registry.go b/pkg/registry/resourcequotausage/registry.go deleted file mode 100644 index 0210067272..0000000000 --- a/pkg/registry/resourcequotausage/registry.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resourcequotausage - -import ( - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" -) - -// Registry contains the functions needed to support a ResourceQuotaUsage -type Registry interface { - // ApplyStatus should update the ResourceQuota.Status with latest observed state. - // This should be atomic, and idempotent based on the ResourceVersion - ApplyStatus(ctx api.Context, usage *api.ResourceQuotaUsage) error -} diff --git a/pkg/registry/resourcequotausage/rest.go b/pkg/registry/resourcequotausage/rest.go deleted file mode 100644 index b2d9a51454..0000000000 --- a/pkg/registry/resourcequotausage/rest.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resourcequotausage - -import ( - "fmt" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" -) - -// REST implements the RESTStorage interface for ResourceQuotaUsage -type REST struct { - registry Registry -} - -// NewREST creates a new REST backed by the given registry. -func NewREST(registry Registry) *REST { - return &REST{ - registry: registry, - } -} - -// New returns a new resource observation object -func (*REST) New() runtime.Object { - return &api.ResourceQuotaUsage{} -} - -// Create takes the incoming ResourceQuotaUsage and applies the latest status atomically to a ResourceQuota -func (b *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, error) { - resourceQuotaUsage, ok := obj.(*api.ResourceQuotaUsage) - if !ok { - return nil, fmt.Errorf("incorrect type: %#v", obj) - } - if err := b.registry.ApplyStatus(ctx, resourceQuotaUsage); err != nil { - return nil, err - } - return &api.Status{Status: api.StatusSuccess}, nil -} diff --git a/pkg/registry/resourcequotausage/rest_test.go b/pkg/registry/resourcequotausage/rest_test.go deleted file mode 100644 index 1298a9c0de..0000000000 --- a/pkg/registry/resourcequotausage/rest_test.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright 2014 Google Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package resourcequotausage diff --git a/pkg/resourcequota/resource_quota_controller.go b/pkg/resourcequota/resource_quota_controller.go index 4b5773ce05..30e8c3b9f1 100644 --- a/pkg/resourcequota/resource_quota_controller.go +++ b/pkg/resourcequota/resource_quota_controller.go @@ -114,11 +114,13 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err dirty := quota.Status.Hard == nil || quota.Status.Used == nil // Create a usage object that is based on the quota resource version - usage := api.ResourceQuotaUsage{ + usage := api.ResourceQuota{ ObjectMeta: api.ObjectMeta{ Name: quota.Name, Namespace: quota.Namespace, - ResourceVersion: quota.ResourceVersion}, + ResourceVersion: quota.ResourceVersion, + Labels: quota.Labels, + Annotations: quota.Annotations}, Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{}, Used: api.ResourceList{}, @@ -206,7 +208,8 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err // update the usage only if it changed if dirty { - return rm.kubeClient.ResourceQuotaUsages(usage.Namespace).Create(&usage) + _, err = rm.kubeClient.ResourceQuotas(usage.Namespace).Status(&usage) + return err } return nil } diff --git a/pkg/resourcequota/resource_quota_controller_test.go b/pkg/resourcequota/resource_quota_controller_test.go index 4e1f177c97..a0f4c77a72 100644 --- a/pkg/resourcequota/resource_quota_controller_test.go +++ b/pkg/resourcequota/resource_quota_controller_test.go @@ -141,7 +141,7 @@ func TestSyncResourceQuota(t *testing.T) { }, }, } - expectedUsage := api.ResourceQuotaUsage{ + expectedUsage := api.ResourceQuota{ Status: api.ResourceQuotaStatus{ Hard: api.ResourceList{ api.ResourceCPU: resource.MustParse("3"), @@ -166,7 +166,7 @@ func TestSyncResourceQuota(t *testing.T) { t.Errorf("Unexpected error %v", err) } - usage := kubeClient.ResourceQuotaUsage + usage := kubeClient.ResourceQuotaStatus // ensure hard and used limits are what we expected for k, v := range expectedUsage.Status.Hard { diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go index b8ab2764dc..ec5a133751 100644 --- a/plugin/pkg/admission/resourcequota/admission.go +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -113,14 +113,16 @@ func (q *quota) Admit(a admission.Attributes) (err error) { if dirty { // construct a usage record - usage := api.ResourceQuotaUsage{ + usage := api.ResourceQuota{ ObjectMeta: api.ObjectMeta{ Name: quota.Name, Namespace: quota.Namespace, - ResourceVersion: quota.ResourceVersion}, + ResourceVersion: quota.ResourceVersion, + Labels: quota.Labels, + Annotations: quota.Annotations}, } usage.Status = *status - err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage) + _, err = q.client.ResourceQuotas(usage.Namespace).Status(&usage) if err != nil { return apierrors.NewForbidden(a.GetResource(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetResource())) }