From 829fa69527ea37e20dc592586ed2482e6711f7fd Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Fri, 23 Jan 2015 12:38:30 -0500 Subject: [PATCH 1/5] Introduce a ResourceQuota object --- pkg/api/helpers.go | 8 +- pkg/api/register.go | 6 + pkg/api/types.go | 58 ++++++ pkg/api/v1beta1/conversion.go | 88 +++++++++ pkg/api/v1beta1/register.go | 6 + pkg/api/v1beta1/types.go | 55 ++++++ pkg/api/v1beta2/conversion.go | 88 +++++++++ pkg/api/v1beta2/register.go | 6 + pkg/api/v1beta2/types.go | 55 ++++++ pkg/api/v1beta3/register.go | 6 + pkg/api/v1beta3/types.go | 58 ++++++ pkg/api/validation/validation.go | 25 +++ pkg/api/validation/validation_test.go | 76 ++++++++ pkg/client/client.go | 10 ++ pkg/client/fake.go | 29 +-- pkg/client/fake_resource_quota_usages.go | 33 ++++ pkg/client/fake_resource_quotas.go | 54 ++++++ pkg/client/resource_quota_usages.go | 57 ++++++ pkg/client/resource_quota_usages_test.go | 93 ++++++++++ pkg/client/resource_quotas.go | 94 ++++++++++ pkg/client/resource_quotas_test.go | 177 +++++++++++++++++++ pkg/kubectl/describe.go | 37 ++++ pkg/kubectl/kubectl.go | 1 + pkg/kubectl/resource_printer.go | 21 +++ pkg/kubectl/sorted_resource_name_list.go | 35 ++++ pkg/master/master.go | 30 ++-- pkg/registry/resourcequota/doc.go | 19 ++ pkg/registry/resourcequota/registry.go | 75 ++++++++ pkg/registry/resourcequota/registry_test.go | 116 ++++++++++++ pkg/registry/resourcequota/rest.go | 162 +++++++++++++++++ pkg/registry/resourcequota/rest_test.go | 17 ++ pkg/registry/resourcequotausage/doc.go | 19 ++ pkg/registry/resourcequotausage/registry.go | 28 +++ pkg/registry/resourcequotausage/rest.go | 56 ++++++ pkg/registry/resourcequotausage/rest_test.go | 17 ++ 35 files changed, 1692 insertions(+), 23 deletions(-) create mode 100644 pkg/client/fake_resource_quota_usages.go create mode 100644 pkg/client/fake_resource_quotas.go create mode 100644 pkg/client/resource_quota_usages.go create mode 100644 pkg/client/resource_quota_usages_test.go create mode 100644 pkg/client/resource_quotas.go create mode 100644 pkg/client/resource_quotas_test.go create mode 100644 pkg/kubectl/sorted_resource_name_list.go create mode 100644 pkg/registry/resourcequota/doc.go create mode 100644 pkg/registry/resourcequota/registry.go create mode 100644 pkg/registry/resourcequota/registry_test.go create mode 100644 pkg/registry/resourcequota/rest.go create mode 100644 pkg/registry/resourcequota/rest_test.go create mode 100644 pkg/registry/resourcequotausage/doc.go create mode 100644 pkg/registry/resourcequotausage/registry.go create mode 100644 pkg/registry/resourcequotausage/rest.go create mode 100644 pkg/registry/resourcequotausage/rest_test.go diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index 78db3b8d379..e05538e86a3 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -62,7 +62,13 @@ var Semantic = conversion.EqualitiesOrDie( }, ) -var standardResources = util.NewStringSet(string(ResourceMemory), string(ResourceCPU)) +var standardResources = util.NewStringSet( + string(ResourceMemory), + string(ResourceCPU), + string(ResourcePods), + string(ResourceQuotas), + string(ResourceServices), + string(ResourceReplicationControllers)) func IsStandardResourceName(str string) bool { return standardResources.Has(str) diff --git a/pkg/api/register.go b/pkg/api/register.go index 15fbb1f2809..b8638e442c2 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -49,6 +49,9 @@ func init() { &List{}, &LimitRange{}, &LimitRangeList{}, + &ResourceQuota{}, + &ResourceQuotaList{}, + &ResourceQuotaUsage{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -81,3 +84,6 @@ func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} +func (*ResourceQuota) IsAnAPIObject() {} +func (*ResourceQuotaList) IsAnAPIObject() {} +func (*ResourceQuotaUsage) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index 2804bff1ad5..4b81bdca829 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1133,6 +1133,7 @@ type List struct { Items []runtime.Object `json:"items"` } +<<<<<<< HEAD // A type of object that is limited type LimitType string @@ -1176,3 +1177,60 @@ type LimitRangeList struct { // Items is a list of LimitRange objects Items []LimitRange `json:"items"` } + +// The following identify resource constants for Kubernetes object types +const ( + // Pods, number + ResourcePods ResourceName = "pods" + // Services, number + ResourceServices ResourceName = "services" + // ReplicationControllers, number + ResourceReplicationControllers ResourceName = "replicationcontrollers" + // ResourceQuotas, number + ResourceQuotas ResourceName = "resourcequotas" +) + +// ResourceQuotaSpec defines the desired hard limits to enforce for Quota +type ResourceQuotaSpec struct { + // Hard is the set of desired hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` +} + +// ResourceQuotaStatus defines the enforced hard limits and observed use +type ResourceQuotaStatus struct { + // Hard is the set of enforced hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` + // Used is the current observed total usage of the resource in the namespace + Used ResourceList `json:"used,omitempty"` +} + +// ResourceQuota sets aggregate quota restrictions enforced per namespace +type ResourceQuota struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired quota + Spec ResourceQuotaSpec `json:"spec,omitempty"` + + // Status defines the actual enforced quota and its current usage + 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"` + ListMeta `json:"metadata,omitempty"` + + // Items is a list of ResourceQuota objects + Items []ResourceQuota `json:"items"` +} diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 3e581da3d9a..be0ffa5da30 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -634,6 +634,94 @@ func init() { } return nil }, + func(in *newer.ResourceQuota, out *ResourceQuota, 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.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuota, out *newer.ResourceQuota, 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.Spec, &out.Spec, 0); err != nil { + return err + } + 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 { + return err + } + return nil + }, + func(in *newer.ResourceQuotaSpec, out *ResourceQuotaSpec, s conversion.Scope) error { + *out = ResourceQuotaSpec{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuotaSpec, out *newer.ResourceQuotaSpec, s conversion.Scope) error { + *out = newer.ResourceQuotaSpec{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + return nil + }, + func(in *newer.ResourceQuotaStatus, out *ResourceQuotaStatus, s conversion.Scope) error { + *out = ResourceQuotaStatus{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + if err := s.Convert(&in.Used, &out.Used, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuotaStatus, out *newer.ResourceQuotaStatus, s conversion.Scope) error { + *out = newer.ResourceQuotaStatus{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + if err := s.Convert(&in.Used, &out.Used, 0); err != nil { + return err + } + return nil + }, // Object ID <-> Name // TODO: amend the conversion package to allow overriding specific fields. func(in *ObjectReference, out *newer.ObjectReference, s conversion.Scope) error { diff --git a/pkg/api/v1beta1/register.go b/pkg/api/v1beta1/register.go index 992f2ed733f..17c917a4860 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -50,6 +50,9 @@ func init() { &List{}, &LimitRange{}, &LimitRangeList{}, + &ResourceQuota{}, + &ResourceQuotaList{}, + &ResourceQuotaUsage{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -82,3 +85,6 @@ func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} +func (*ResourceQuota) IsAnAPIObject() {} +func (*ResourceQuotaList) IsAnAPIObject() {} +func (*ResourceQuotaUsage) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 549e86aac86..cfa4b7522dc 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -893,6 +893,7 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } +<<<<<<< HEAD // A type of object that is limited type LimitType string @@ -934,3 +935,57 @@ type LimitRangeList struct { // Items is a list of LimitRange objects Items []LimitRange `json:"items"` } + +// The following identify resource constants for Kubernetes object types +const ( + // Pods, number + ResourcePods ResourceName = "pods" + // Services, number + ResourceServices ResourceName = "services" + // ReplicationControllers, number + ResourceReplicationControllers ResourceName = "replicationcontrollers" + // ResourceQuotas, number + ResourceQuotas ResourceName = "resourcequotas" +) + +// ResourceQuotaSpec defines the desired hard limits to enforce for Quota +type ResourceQuotaSpec struct { + // Hard is the set of desired hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` +} + +// ResourceQuotaStatus defines the enforced hard limits and observed use +type ResourceQuotaStatus struct { + // Hard is the set of enforced hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` + // Used is the current observed total usage of the resource in the namespace + Used ResourceList `json:"used,omitempty"` +} + +// ResourceQuota sets aggregate quota restrictions enforced per namespace +type ResourceQuota struct { + TypeMeta `json:",inline"` + + // Spec defines the desired quota + Spec ResourceQuotaSpec `json:"spec,omitempty"` + + // Status defines the actual enforced quota and its current usage + 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"` + + // 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"` + + // Items is a list of ResourceQuota objects + Items []ResourceQuota `json:"items"` +} diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index d373212c516..95c96145d1e 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -551,6 +551,94 @@ func init() { } return nil }, + func(in *newer.ResourceQuota, out *ResourceQuota, 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.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuota, out *newer.ResourceQuota, 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.Spec, &out.Spec, 0); err != nil { + return err + } + 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 { + return err + } + return nil + }, + func(in *newer.ResourceQuotaSpec, out *ResourceQuotaSpec, s conversion.Scope) error { + *out = ResourceQuotaSpec{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuotaSpec, out *newer.ResourceQuotaSpec, s conversion.Scope) error { + *out = newer.ResourceQuotaSpec{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + return nil + }, + func(in *newer.ResourceQuotaStatus, out *ResourceQuotaStatus, s conversion.Scope) error { + *out = ResourceQuotaStatus{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + if err := s.Convert(&in.Used, &out.Used, 0); err != nil { + return err + } + return nil + }, + func(in *ResourceQuotaStatus, out *newer.ResourceQuotaStatus, s conversion.Scope) error { + *out = newer.ResourceQuotaStatus{} + if err := s.Convert(&in.Hard, &out.Hard, 0); err != nil { + return err + } + if err := s.Convert(&in.Used, &out.Used, 0); err != nil { + return err + } + return nil + }, // Object ID <-> Name // TODO: amend the conversion package to allow overriding specific fields. func(in *ObjectReference, out *newer.ObjectReference, s conversion.Scope) error { diff --git a/pkg/api/v1beta2/register.go b/pkg/api/v1beta2/register.go index 2990dbbf739..ad447476c23 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -50,6 +50,9 @@ func init() { &List{}, &LimitRange{}, &LimitRangeList{}, + &ResourceQuota{}, + &ResourceQuotaList{}, + &ResourceQuotaUsage{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -82,3 +85,6 @@ func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} +func (*ResourceQuota) IsAnAPIObject() {} +func (*ResourceQuotaList) IsAnAPIObject() {} +func (*ResourceQuotaUsage) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 0a193d35e48..bc8cfd9d172 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -895,6 +895,7 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } +<<<<<<< HEAD // A type of object that is limited type LimitType string @@ -936,3 +937,57 @@ type LimitRangeList struct { // Items is a list of LimitRange objects Items []LimitRange `json:"items"` } + +// The following identify resource constants for Kubernetes object types +const ( + // Pods, number + ResourcePods ResourceName = "pods" + // Services, number + ResourceServices ResourceName = "services" + // ReplicationControllers, number + ResourceReplicationControllers ResourceName = "replicationcontrollers" + // ResourceQuotas, number + ResourceQuotas ResourceName = "resourcequotas" +) + +// ResourceQuotaSpec defines the desired hard limits to enforce for Quota +type ResourceQuotaSpec struct { + // Hard is the set of desired hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` +} + +// ResourceQuotaStatus defines the enforced hard limits and observed use +type ResourceQuotaStatus struct { + // Hard is the set of enforced hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` + // Used is the current observed total usage of the resource in the namespace + Used ResourceList `json:"used,omitempty"` +} + +// ResourceQuota sets aggregate quota restrictions enforced per namespace +type ResourceQuota struct { + TypeMeta `json:",inline"` + + // Spec defines the desired quota + Spec ResourceQuotaSpec `json:"spec,omitempty"` + + // Status defines the actual enforced quota and its current usage + 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"` + + // 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"` + + // Items is a list of ResourceQuota objects + Items []ResourceQuota `json:"items"` +} diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index 75f2c04e81c..a1956cdc67b 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -50,6 +50,9 @@ func init() { &List{}, &LimitRange{}, &LimitRangeList{}, + &ResourceQuota{}, + &ResourceQuotaList{}, + &ResourceQuotaUsage{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -82,3 +85,6 @@ func (*EventList) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} func (*LimitRange) IsAnAPIObject() {} func (*LimitRangeList) IsAnAPIObject() {} +func (*ResourceQuota) IsAnAPIObject() {} +func (*ResourceQuotaList) IsAnAPIObject() {} +func (*ResourceQuotaUsage) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 1c91305a8a8..c44090e1e1b 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1055,6 +1055,7 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } +<<<<<<< HEAD // A type of object that is limited type LimitType string @@ -1098,3 +1099,60 @@ type LimitRangeList struct { // Items is a list of LimitRange objects Items []LimitRange `json:"items"` } + +// The following identify resource constants for Kubernetes object types +const ( + // Pods, number + ResourcePods ResourceName = "pods" + // Services, number + ResourceServices ResourceName = "services" + // ReplicationControllers, number + ResourceReplicationControllers ResourceName = "replicationcontrollers" + // ResourceQuotas, number + ResourceQuotas ResourceName = "resourcequotas" +) + +// ResourceQuotaSpec defines the desired hard limits to enforce for Quota +type ResourceQuotaSpec struct { + // Hard is the set of desired hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` +} + +// ResourceQuotaStatus defines the enforced hard limits and observed use +type ResourceQuotaStatus struct { + // Hard is the set of enforced hard limits for each named resource + Hard ResourceList `json:"hard,omitempty"` + // Used is the current observed total usage of the resource in the namespace + Used ResourceList `json:"used,omitempty"` +} + +// ResourceQuota sets aggregate quota restrictions enforced per namespace +type ResourceQuota struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired quota + Spec ResourceQuotaSpec `json:"spec,omitempty"` + + // Status defines the actual enforced quota and its current usage + 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"` + ListMeta `json:"metadata,omitempty"` + + // Items is a list of ResourceQuota objects + Items []ResourceQuota `json:"items"` +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 22888fd315f..08c7b2eabe7 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -666,3 +666,28 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { } return allErrs } + +// ValidateResourceQuota tests if required fields in the ResourceQuota are set. +func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(resourceQuota.Name) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("name", resourceQuota.Name)) + } else if !util.IsDNSSubdomain(resourceQuota.Name) { + allErrs = append(allErrs, errs.NewFieldInvalid("name", resourceQuota.Name, "")) + } + if len(resourceQuota.Namespace) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("namespace", resourceQuota.Namespace)) + } else if !util.IsDNSSubdomain(resourceQuota.Namespace) { + allErrs = append(allErrs, errs.NewFieldInvalid("namespace", resourceQuota.Namespace, "")) + } + for k := range resourceQuota.Spec.Hard { + allErrs = append(allErrs, ValidateResourceName(string(k))...) + } + for k := range resourceQuota.Status.Hard { + allErrs = append(allErrs, ValidateResourceName(string(k))...) + } + for k := range resourceQuota.Status.Used { + allErrs = append(allErrs, ValidateResourceName(string(k))...) + } + return allErrs +} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 4555eeab6ca..08c270d28f3 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1577,6 +1577,7 @@ func TestValidateLimitRange(t *testing.T) { }, }, } + for _, successCase := range successCases { if errs := ValidateLimitRange(&successCase); len(errs) != 0 { t.Errorf("expected success: %v", errs) @@ -1641,3 +1642,78 @@ func TestValidateLimitRange(t *testing.T) { } } } + +func TestValidateResourceQuota(t *testing.T) { + successCases := []api.ResourceQuota{ + { + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + 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"), + }, + }, + }, + } + + for _, successCase := range successCases { + if errs := ValidateResourceQuota(&successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := map[string]api.ResourceQuota{ + "zero-length Name": { + ObjectMeta: api.ObjectMeta{ + Name: "", + Namespace: "foo", + }, + 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"), + }, + }, + }, + "zero-length-namespace": { + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "", + }, + 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"), + }, + }, + }, + } + for k, v := range errorCases { + errs := ValidateResourceQuota(&v) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + for i := range errs { + field := errs[i].(*errors.ValidationError).Field + if field != "name" && + field != "namespace" { + t.Errorf("%s: missing prefix for: %v", k, errs[i]) + } + } + } +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 67d7258cc4b..6fddce33a20 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -38,6 +38,8 @@ type Interface interface { NodesInterface EventNamespacer LimitRangesNamespacer + ResourceQuotasNamespacer + ResourceQuotaUsagesNamespacer } func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface { @@ -68,6 +70,14 @@ func (c *Client) LimitRanges(namespace string) LimitRangeInterface { return newLimitRanges(c, namespace) } +func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface { + return newResourceQuotas(c, namespace) +} + +func (c *Client) ResourceQuotaUsages(namespace string) ResourceQuotaUsageInterface { + return newResourceQuotaUsages(c, namespace) +} + // VersionInterface has a method to retrieve the server version. type VersionInterface interface { ServerVersion() (*version.Info, error) diff --git a/pkg/client/fake.go b/pkg/client/fake.go index dfa0f8d9f1d..0c4c209fa44 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -34,22 +34,31 @@ 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 - Ctrl api.ReplicationController - ServiceList api.ServiceList - EndpointsList api.EndpointsList - MinionsList api.NodeList - EventsList api.EventList - LimitRangesList api.LimitRangeList - Err error - Watch watch.Interface + Actions []FakeAction + PodsList api.PodList + Ctrl api.ReplicationController + ServiceList api.ServiceList + EndpointsList api.EndpointsList + MinionsList api.NodeList + EventsList api.EventList + LimitRangesList api.LimitRangeList + ResourceQuotasList api.ResourceQuotaList + Err error + Watch watch.Interface } func (c *Fake) LimitRanges(namespace string) LimitRangeInterface { return &FakeLimitRanges{Fake: c, Namespace: namespace} } +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 new file mode 100644 index 00000000000..50b733b55ff --- /dev/null +++ b/pkg/client/fake_resource_quota_usages.go @@ -0,0 +1,33 @@ +/* +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"}) + return nil +} diff --git a/pkg/client/fake_resource_quotas.go b/pkg/client/fake_resource_quotas.go new file mode 100644 index 00000000000..f7b870ed127 --- /dev/null +++ b/pkg/client/fake_resource_quotas.go @@ -0,0 +1,54 @@ +/* +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" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +// FakeResourceQuotas implements ResourceQuotaInterface. 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 FakeResourceQuotas struct { + Fake *Fake + Namespace string +} + +func (c *FakeResourceQuotas) List(selector labels.Selector) (*api.ResourceQuotaList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-resourceQuotas"}) + return api.Scheme.CopyOrDie(&c.Fake.ResourceQuotasList).(*api.ResourceQuotaList), nil +} + +func (c *FakeResourceQuotas) Get(name string) (*api.ResourceQuota, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-resourceQuota", Value: name}) + return &api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: name, Namespace: c.Namespace}}, nil +} + +func (c *FakeResourceQuotas) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-resourceQuota", Value: name}) + return nil +} + +func (c *FakeResourceQuotas) Create(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-resourceQuota"}) + return &api.ResourceQuota{}, nil +} + +func (c *FakeResourceQuotas) Update(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-resourceQuota", Value: resourceQuota.Name}) + return &api.ResourceQuota{}, nil +} diff --git a/pkg/client/resource_quota_usages.go b/pkg/client/resource_quota_usages.go new file mode 100644 index 00000000000..1ba2c5e5e82 --- /dev/null +++ b/pkg/client/resource_quota_usages.go @@ -0,0 +1,57 @@ +/* +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 new file mode 100644 index 00000000000..eb2c65f1bcb --- /dev/null +++ b/pkg/client/resource_quota_usages_test.go @@ -0,0 +1,93 @@ +/* +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 new file mode 100644 index 00000000000..d0d51ea800a --- /dev/null +++ b/pkg/client/resource_quotas.go @@ -0,0 +1,94 @@ +/* +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 ( + "errors" + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +// ResourceQuotasNamespacer has methods to work with ResourceQuota resources in a namespace +type ResourceQuotasNamespacer interface { + ResourceQuotas(namespace string) ResourceQuotaInterface +} + +// ResourceQuotaInterface has methods to work with ResourceQuota resources. +type ResourceQuotaInterface interface { + List(selector labels.Selector) (*api.ResourceQuotaList, error) + Get(name string) (*api.ResourceQuota, error) + Delete(name string) error + Create(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) + Update(resourceQuota *api.ResourceQuota) (*api.ResourceQuota, error) +} + +// resourceQuotas implements ResourceQuotasNamespacer interface +type resourceQuotas struct { + r *Client + ns string +} + +// newResourceQuotas returns a resourceQuotas +func newResourceQuotas(c *Client, namespace string) *resourceQuotas { + return &resourceQuotas{ + r: c, + ns: namespace, + } +} + +// List takes a selector, and returns the list of resourceQuotas that match that selector. +func (c *resourceQuotas) List(selector labels.Selector) (result *api.ResourceQuotaList, err error) { + result = &api.ResourceQuotaList{} + err = c.r.Get().Namespace(c.ns).Resource("resourceQuotas").SelectorParam("labels", selector).Do().Into(result) + return +} + +// Get takes the name of the resourceQuota, and returns the corresponding ResourceQuota object, and an error if it occurs +func (c *resourceQuotas) Get(name string) (result *api.ResourceQuota, err error) { + if len(name) == 0 { + return nil, errors.New("name is required parameter to Get") + } + + result = &api.ResourceQuota{} + err = c.r.Get().Namespace(c.ns).Resource("resourceQuotas").Name(name).Do().Into(result) + return +} + +// Delete takes the name of the resourceQuota, and returns an error if one occurs +func (c *resourceQuotas) Delete(name string) error { + return c.r.Delete().Namespace(c.ns).Resource("resourceQuotas").Name(name).Do().Error() +} + +// Create takes the representation of a resourceQuota. Returns the server's representation of the resourceQuota, and an error, if it occurs. +func (c *resourceQuotas) Create(resourceQuota *api.ResourceQuota) (result *api.ResourceQuota, err error) { + result = &api.ResourceQuota{} + err = c.r.Post().Namespace(c.ns).Resource("resourceQuotas").Body(resourceQuota).Do().Into(result) + return +} + +// Update takes the representation of a resourceQuota to update. 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 { + err = fmt.Errorf("invalid update object, missing resource version: %v", resourceQuota) + return + } + err = c.r.Put().Namespace(c.ns).Resource("resourceQuotas").Name(resourceQuota.Name).Body(resourceQuota).Do().Into(result) + return +} diff --git a/pkg/client/resource_quotas_test.go b/pkg/client/resource_quotas_test.go new file mode 100644 index 00000000000..22eee5dadab --- /dev/null +++ b/pkg/client/resource_quotas_test.go @@ -0,0 +1,177 @@ +/* +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" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" +) + +func TestResourceQuotaCreate(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + 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"), + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "POST", + Path: buildResourcePath(ns, "/resourceQuotas"), + Query: buildQueryValues(ns, nil), + Body: resourceQuota, + }, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + + response, err := c.Setup().ResourceQuotas(ns).Create(resourceQuota) + c.Validate(t, response, err) +} + +func TestResourceQuotaGet(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + 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"), + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: buildResourcePath(ns, "/resourceQuotas/abc"), + Query: buildQueryValues(ns, nil), + Body: nil, + }, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + + response, err := c.Setup().ResourceQuotas(ns).Get("abc") + c.Validate(t, response, err) +} + +func TestResourceQuotaList(t *testing.T) { + ns := api.NamespaceDefault + + resourceQuotaList := &api.ResourceQuotaList{ + Items: []api.ResourceQuota{ + { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: buildResourcePath(ns, "/resourceQuotas"), + Query: buildQueryValues(ns, nil), + Body: nil, + }, + Response: Response{StatusCode: 200, Body: resourceQuotaList}, + } + response, err := c.Setup().ResourceQuotas(ns).List(labels.Everything()) + c.Validate(t, response, err) +} + +func TestResourceQuotaUpdate(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + ResourceVersion: "1", + }, + 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"), + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/resourceQuotas/abc"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + response, err := c.Setup().ResourceQuotas(ns).Update(resourceQuota) + c.Validate(t, response, err) +} + +func TestInvalidResourceQuotaUpdate(t *testing.T) { + ns := api.NamespaceDefault + resourceQuota := &api.ResourceQuota{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + 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"), + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/resourceQuotas/abc"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: resourceQuota}, + } + _, err := c.Setup().ResourceQuotas(ns).Update(resourceQuota) + if err == nil { + t.Errorf("Expected an error due to missing ResourceVersion") + } +} + +func TestResourceQuotaDelete(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{ + Request: testRequest{Method: "DELETE", Path: buildResourcePath(ns, "/resourceQuotas/foo"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200}, + } + err := c.Setup().ResourceQuotas(ns).Delete("foo") + c.Validate(t, nil, err) +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index c587282b85e..72baad1efda 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -49,6 +49,8 @@ func DescriberFor(kind string, c *client.Client) (Describer, bool) { return &MinionDescriber{c}, true case "LimitRange": return &LimitRangeDescriber{c}, true + case "ResourceQuota": + return &ResourceQuotaDescriber{c}, true } return nil, false } @@ -106,6 +108,41 @@ func (d *LimitRangeDescriber) Describe(namespace, name string) (string, error) { }) } +// ResourceQuotaDescriber generates information about a resource quota +type ResourceQuotaDescriber struct { + client.Interface +} + +func (d *ResourceQuotaDescriber) Describe(namespace, name string) (string, error) { + rq := d.ResourceQuotas(namespace) + + resourceQuota, err := rq.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", resourceQuota.Name) + fmt.Fprintf(out, "Resource\tUsed\tHard\n") + fmt.Fprintf(out, "--------\t----\t----\n") + + resources := []api.ResourceName{} + for resource := range resourceQuota.Status.Hard { + resources = append(resources, resource) + } + sort.Sort(SortableResourceNames(resources)) + + msg := "%v\t%v\t%v\n" + for i := range resources { + resource := resources[i] + hardQuantity := resourceQuota.Status.Hard[resource] + usedQuantity := resourceQuota.Status.Used[resource] + fmt.Fprintf(out, msg, resource, usedQuantity.String(), hardQuantity.String()) + } + return nil + }) +} + // PodDescriber generates information about a pod and the replication controllers that // create it. type PodDescriber struct { diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 72b5c0d8f35..517ff7ddf35 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -149,6 +149,7 @@ func expandResourceShortcut(resource string) string { "mi": "minions", "ev": "events", "limits": "limitRanges", + "quota": "resourceQuotas", } if expanded, ok := shortForms[resource]; ok { return expanded diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 73d0464e63e..5b0775e9b35 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -222,6 +222,7 @@ var minionColumns = []string{"NAME", "LABELS", "STATUS"} var statusColumns = []string{"STATUS"} var eventColumns = []string{"TIME", "NAME", "KIND", "SUBOBJECT", "REASON", "SOURCE", "MESSAGE"} var limitRangeColumns = []string{"NAME"} +var resourceQuotaColumns = []string{"NAME"} // addDefaultHandlers adds print handlers for default Kubernetes types. func (h *HumanReadablePrinter) addDefaultHandlers() { @@ -238,6 +239,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(eventColumns, printEventList) h.Handler(limitRangeColumns, printLimitRange) h.Handler(limitRangeColumns, printLimitRangeList) + h.Handler(resourceQuotaColumns, printResourceQuota) + h.Handler(resourceQuotaColumns, printResourceQuotaList) } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { @@ -430,6 +433,24 @@ func printLimitRangeList(list *api.LimitRangeList, w io.Writer) error { return nil } +func printResourceQuota(resourceQuota *api.ResourceQuota, w io.Writer) error { + _, err := fmt.Fprintf( + w, "%s\n", + resourceQuota.Name, + ) + return err +} + +// Prints the ResourceQuotaList in a human-friendly format. +func printResourceQuotaList(list *api.ResourceQuotaList, w io.Writer) error { + for i := range list.Items { + if err := printResourceQuota(&list.Items[i], w); err != nil { + return err + } + } + return nil +} + // PrintObj prints the obj in a human-friendly format according to the type of the obj. func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { w := tabwriter.NewWriter(output, 20, 5, 3, ' ', 0) diff --git a/pkg/kubectl/sorted_resource_name_list.go b/pkg/kubectl/sorted_resource_name_list.go new file mode 100644 index 00000000000..6e6a4fb602b --- /dev/null +++ b/pkg/kubectl/sorted_resource_name_list.go @@ -0,0 +1,35 @@ +/* +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 kubectl + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +type SortableResourceNames []api.ResourceName + +func (list SortableResourceNames) Len() int { + return len(list) +} + +func (list SortableResourceNames) Swap(i, j int) { + list[i], list[j] = list[j], list[i] +} + +func (list SortableResourceNames) Less(i, j int) bool { + return list[i] < list[j] +} diff --git a/pkg/master/master.go b/pkg/master/master.go index 765c43249a6..cd2169c6e02 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -50,6 +50,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/limitrange" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota" + "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequotausage" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" @@ -103,17 +105,18 @@ type Config struct { // Master contains state for a Kubernetes cluster master/api server. type Master struct { // "Inputs", Copied from Config - podRegistry pod.Registry - controllerRegistry controller.Registry - serviceRegistry service.Registry - endpointRegistry endpoint.Registry - minionRegistry minion.Registry - bindingRegistry binding.Registry - eventRegistry generic.Registry - limitRangeRegistry generic.Registry - storage map[string]apiserver.RESTStorage - client *client.Client - portalNet *net.IPNet + podRegistry pod.Registry + controllerRegistry controller.Registry + serviceRegistry service.Registry + endpointRegistry endpoint.Registry + minionRegistry minion.Registry + bindingRegistry binding.Registry + eventRegistry generic.Registry + limitRangeRegistry generic.Registry + resourceQuotaRegistry resourcequota.Registry + storage map[string]apiserver.RESTStorage + client *client.Client + portalNet *net.IPNet mux apiserver.Mux muxHelper *apiserver.MuxHelper @@ -251,6 +254,7 @@ func New(c *Config) *Master { eventRegistry: event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())), minionRegistry: minionRegistry, limitRangeRegistry: limitrange.NewEtcdRegistry(c.EtcdHelper), + resourceQuotaRegistry: resourcequota.NewEtcdRegistry(c.EtcdHelper), client: c.Client, portalNet: c.PortalNet, rootWebService: new(restful.WebService), @@ -365,7 +369,9 @@ func (m *Master) init(c *Config) { // TODO: should appear only in scheduler API group. "bindings": binding.NewREST(m.bindingRegistry), - "limitRanges": limitrange.NewREST(m.limitRangeRegistry), + "limitRanges": limitrange.NewREST(m.limitRangeRegistry), + "resourceQuotas": resourcequota.NewREST(m.resourceQuotaRegistry), + "resourceQuotaUsages": resourcequotausage.NewREST(m.resourceQuotaRegistry), } apiVersions := []string{"v1beta1", "v1beta2"} diff --git a/pkg/registry/resourcequota/doc.go b/pkg/registry/resourcequota/doc.go new file mode 100644 index 00000000000..4d6a1de1959 --- /dev/null +++ b/pkg/registry/resourcequota/doc.go @@ -0,0 +1,19 @@ +/* +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 provides Registry interface and it's REST +// implementation for storing ResourceQuota api objects. +package resourcequota diff --git a/pkg/registry/resourcequota/registry.go b/pkg/registry/resourcequota/registry.go new file mode 100644 index 00000000000..2a918e96d5f --- /dev/null +++ b/pkg/registry/resourcequota/registry.go @@ -0,0 +1,75 @@ +/* +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 ( + "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/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// Registry implements operations to modify ResourceQuota objects +type Registry interface { + generic.Registry + resourcequotausage.Registry +} + +// registry implements custom changes to generic.Etcd. +type registry struct { + *etcdgeneric.Etcd +} + +// 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) + if err != nil { + return 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.Update(ctx, resourceQuota.Name, resourceQuota) +} + +// 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, + }, + } +} diff --git a/pkg/registry/resourcequota/registry_test.go b/pkg/registry/resourcequota/registry_test.go new file mode 100644 index 00000000000..573ebf2e9b1 --- /dev/null +++ b/pkg/registry/resourcequota/registry_test.go @@ -0,0 +1,116 @@ +/* +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.Create(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 new file mode 100644 index 00000000000..3f22be18826 --- /dev/null +++ b/pkg/registry/resourcequota/rest.go @@ -0,0 +1,162 @@ +/* +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 ( + "fmt" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" + "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "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 +} + +// 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, + } +} + +// Create a ResourceQuota object +func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, 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) + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Create(ctx, resourceQuota.Name, resourceQuota) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, resourceQuota.Name) + }), nil +} + +// Update updates a ResourceQuota object. +func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, 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")) + } + + oldObj, err := rs.registry.Get(ctx, resourceQuota.Name) + if err != nil { + return nil, 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, errors.NewInvalid("resourceQuota", editResourceQuota.Name, errs) + } + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Update(ctx, editResourceQuota.Name, editResourceQuota) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, editResourceQuota.Name) + }), nil +} + +// Delete deletes the ResourceQuota with the specified name +func (rs *REST) Delete(ctx api.Context, name string) (<-chan apiserver.RESTResult, error) { + obj, err := rs.registry.Get(ctx, name) + if err != nil { + return nil, err + } + _, ok := obj.(*api.ResourceQuota) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + return &api.Status{Status: api.StatusSuccess}, rs.registry.Delete(ctx, name) + }), nil +} + +// 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, objFields labels.Set, err error) { + return labels.Set{}, labels.Set{}, nil +} + +func (rs *REST) List(ctx api.Context, label, field labels.Selector) (runtime.Object, error) { + return rs.registry.List(ctx, &generic.SelectionPredicate{label, field, rs.getAttrs}) +} + +func (rs *REST) Watch(ctx api.Context, label, field labels.Selector, resourceVersion string) (watch.Interface, error) { + return rs.registry.Watch(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 new file mode 100644 index 00000000000..45930005154 --- /dev/null +++ b/pkg/registry/resourcequota/rest_test.go @@ -0,0 +1,17 @@ +/* +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 diff --git a/pkg/registry/resourcequotausage/doc.go b/pkg/registry/resourcequotausage/doc.go new file mode 100644 index 00000000000..9892fc3cba1 --- /dev/null +++ b/pkg/registry/resourcequotausage/doc.go @@ -0,0 +1,19 @@ +/* +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 new file mode 100644 index 00000000000..02100672723 --- /dev/null +++ b/pkg/registry/resourcequotausage/registry.go @@ -0,0 +1,28 @@ +/* +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 new file mode 100644 index 00000000000..cfd63cf4c7a --- /dev/null +++ b/pkg/registry/resourcequotausage/rest.go @@ -0,0 +1,56 @@ +/* +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/apiserver" + "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) (<-chan apiserver.RESTResult, error) { + resourceQuotaUsage, ok := obj.(*api.ResourceQuotaUsage) + if !ok { + return nil, fmt.Errorf("incorrect type: %#v", obj) + } + return apiserver.MakeAsync(func() (runtime.Object, error) { + if err := b.registry.ApplyStatus(ctx, resourceQuotaUsage); err != nil { + return nil, err + } + return &api.Status{Status: api.StatusSuccess}, nil + }), nil +} diff --git a/pkg/registry/resourcequotausage/rest_test.go b/pkg/registry/resourcequotausage/rest_test.go new file mode 100644 index 00000000000..1298a9c0de1 --- /dev/null +++ b/pkg/registry/resourcequotausage/rest_test.go @@ -0,0 +1,17 @@ +/* +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 From 67b359ebf98b571ae4d76acd5e5b529f939e8975 Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Sun, 25 Jan 2015 01:11:10 -0500 Subject: [PATCH 2/5] Add a resource quota controller to track usage state --- .../controller-manager.go | 9 +- examples/resourcequota/resource-quota.json | 15 ++ pkg/resourcequota/doc.go | 18 ++ .../resource_quota_controller.go | 180 ++++++++++++++++++ 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 examples/resourcequota/resource-quota.json create mode 100644 pkg/resourcequota/doc.go create mode 100644 pkg/resourcequota/resource_quota_controller.go diff --git a/cmd/kube-controller-manager/controller-manager.go b/cmd/kube-controller-manager/controller-manager.go index fd7d52599fb..02d98ad0683 100644 --- a/cmd/kube-controller-manager/controller-manager.go +++ b/cmd/kube-controller-manager/controller-manager.go @@ -34,6 +34,7 @@ import ( replicationControllerPkg "github.com/GoogleCloudPlatform/kubernetes/pkg/controller" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/healthz" "github.com/GoogleCloudPlatform/kubernetes/pkg/master/ports" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" "github.com/GoogleCloudPlatform/kubernetes/pkg/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version/verflag" @@ -55,8 +56,9 @@ var ( machineList util.StringList // TODO: Discover these by pinging the host machines, and rip out these flags. // TODO: in the meantime, use resource.QuantityFlag() instead of these - nodeMilliCPU = flag.Int64("node_milli_cpu", 1000, "The amount of MilliCPU provisioned on each node") - nodeMemory = resource.QuantityFlag("node_memory", "3Gi", "The amount of memory (in bytes) provisioned on each node") + nodeMilliCPU = flag.Int64("node_milli_cpu", 1000, "The amount of MilliCPU provisioned on each node") + nodeMemory = resource.QuantityFlag("node_memory", "3Gi", "The amount of memory (in bytes) provisioned on each node") + resourceQuotaSyncPeriod = flag.Duration("resource_quota_sync_period", 10*time.Second, "The period for syncing quota usage status in the system") ) func init() { @@ -112,5 +114,8 @@ func main() { nodeController := nodeControllerPkg.NewNodeController(cloud, *minionRegexp, machineList, nodeResources, kubeClient) nodeController.Run(*nodeSyncPeriod) + resourceQuotaManager := resourcequota.NewResourceQuotaManager(kubeClient) + resourceQuotaManager.Run(*resourceQuotaSyncPeriod) + select {} } diff --git a/examples/resourcequota/resource-quota.json b/examples/resourcequota/resource-quota.json new file mode 100644 index 00000000000..7d0c40aefb9 --- /dev/null +++ b/examples/resourcequota/resource-quota.json @@ -0,0 +1,15 @@ +{ + "id": "quota", + "kind": "ResourceQuota", + "apiVersion": "v1beta1", + "spec": { + "hard": { + "memory": "1073741824", + "cpu": "20", + "pods": "10", + "services": "5", + "replicationcontrollers":"20", + "resourcequotas":"1", + }, + } +} diff --git a/pkg/resourcequota/doc.go b/pkg/resourcequota/doc.go new file mode 100644 index 00000000000..2e31b049332 --- /dev/null +++ b/pkg/resourcequota/doc.go @@ -0,0 +1,18 @@ +/* +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. +*/ + +// resourcequota contains a controller that makes resource quota usage observations +package resourcequota diff --git a/pkg/resourcequota/resource_quota_controller.go b/pkg/resourcequota/resource_quota_controller.go new file mode 100644 index 00000000000..1dc5b9b151d --- /dev/null +++ b/pkg/resourcequota/resource_quota_controller.go @@ -0,0 +1,180 @@ +/* +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 ( + "sync" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" +) + +// ResourceQuotaManager is responsible for tracking quota usage status in the system +type ResourceQuotaManager struct { + kubeClient client.Interface + syncTime <-chan time.Time + + // To allow injection of syncUsage for testing. + syncHandler func(quota api.ResourceQuota) error +} + +// NewResourceQuotaManager creates a new ResourceQuotaManager +func NewResourceQuotaManager(kubeClient client.Interface) *ResourceQuotaManager { + + rm := &ResourceQuotaManager{ + kubeClient: kubeClient, + } + + // set the synchronization handler + rm.syncHandler = rm.syncResourceQuota + return rm +} + +// Run begins watching and syncing. +func (rm *ResourceQuotaManager) Run(period time.Duration) { + rm.syncTime = time.Tick(period) + go util.Forever(func() { rm.synchronize() }, period) +} + +func (rm *ResourceQuotaManager) synchronize() { + var resourceQuotas []api.ResourceQuota + list, err := rm.kubeClient.ResourceQuotas(api.NamespaceAll).List(labels.Everything()) + if err != nil { + glog.Errorf("Synchronization error: %v (%#v)", err, err) + } + resourceQuotas = list.Items + wg := sync.WaitGroup{} + wg.Add(len(resourceQuotas)) + for ix := range resourceQuotas { + go func(ix int) { + defer wg.Done() + glog.V(4).Infof("periodic sync of %v/%v", resourceQuotas[ix].Namespace, resourceQuotas[ix].Name) + err := rm.syncHandler(resourceQuotas[ix]) + if err != nil { + glog.Errorf("Error synchronizing: %v", err) + } + }(ix) + } + wg.Wait() +} + +// syncResourceQuota runs a complete sync of current status +func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err error) { + + // dirty tracks if the usage status differs from the previous sync, + // if so, we send a new usage with latest status + // if this is our first sync, it will be dirty by default, since we need track usage + dirty := quota.Status.Hard == nil || quota.Status.Used == nil + + // Create a usage object that is based on the quota resource version + usage := api.ResourceQuotaUsage{ + ObjectMeta: api.ObjectMeta{ + Name: quota.Name, + Namespace: quota.Namespace, + ResourceVersion: quota.ResourceVersion}, + Status: api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + }, + } + // populate the usage with the current observed hard/used limits + usage.Status.Hard = quota.Spec.Hard + usage.Status.Used = quota.Status.Used + + set := map[api.ResourceName]bool{} + for k := range usage.Status.Hard { + set[k] = true + } + + pods := &api.PodList{} + if set[api.ResourcePods] || set[api.ResourceMemory] || set[api.ResourceCPU] { + pods, err = rm.kubeClient.Pods(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + } + + // iterate over each resource, and update observation + for k := range usage.Status.Hard { + + // look if there is a used value, if none, we are definitely dirty + prevQuantity, found := usage.Status.Used[k] + if !found { + dirty = true + } + + var value *resource.Quantity + + switch k { + case api.ResourcePods: + value = resource.NewQuantity(int64(len(pods.Items)), resource.DecimalSI) + case api.ResourceMemory: + val := int64(0) + for i := range pods.Items { + for j := range pods.Items[i].Spec.Containers { + val = val + pods.Items[i].Spec.Containers[j].Memory.Value() + } + } + value = resource.NewQuantity(int64(val), resource.DecimalSI) + case api.ResourceCPU: + val := int64(0) + for i := range pods.Items { + for j := range pods.Items[i].Spec.Containers { + val = val + pods.Items[i].Spec.Containers[j].CPU.MilliValue() + } + } + value = resource.NewMilliQuantity(int64(val), resource.DecimalSI) + case api.ResourceServices: + items, err := rm.kubeClient.Services(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + case api.ResourceReplicationControllers: + items, err := rm.kubeClient.ReplicationControllers(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + case api.ResourceQuotas: + items, err := rm.kubeClient.ResourceQuotas(usage.Namespace).List(labels.Everything()) + if err != nil { + return err + } + value = resource.NewQuantity(int64(len(items.Items)), resource.DecimalSI) + } + + // ignore fields we do not understand (assume another controller is tracking it) + if value != nil { + // see if the value has changed + dirty = dirty || (value.Value() != prevQuantity.Value()) + // just update the value + usage.Status.Used[k] = *value + } + } + + // update the usage only if it changed + if dirty { + return rm.kubeClient.ResourceQuotaUsages(usage.Namespace).Create(&usage) + } + return nil +} From 4887d71c51e75c43604cd6f3689b1457d12db99a Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Sun, 25 Jan 2015 23:34:30 -0500 Subject: [PATCH 3/5] Implement resource quota admission plugin --- cmd/kube-apiserver/plugins.go | 1 + .../resource_quota_controller.go | 26 ++- .../pkg/admission/resourcequota/admission.go | 173 ++++++++++++++++++ .../admission/resourcequota/admission_test.go | 17 ++ plugin/pkg/admission/resourcequota/doc.go | 19 ++ 5 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 plugin/pkg/admission/resourcequota/admission.go create mode 100644 plugin/pkg/admission/resourcequota/admission_test.go create mode 100644 plugin/pkg/admission/resourcequota/doc.go diff --git a/cmd/kube-apiserver/plugins.go b/cmd/kube-apiserver/plugins.go index de652600880..9eadcf7c95b 100644 --- a/cmd/kube-apiserver/plugins.go +++ b/cmd/kube-apiserver/plugins.go @@ -30,4 +30,5 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" ) diff --git a/pkg/resourcequota/resource_quota_controller.go b/pkg/resourcequota/resource_quota_controller.go index 1dc5b9b151d..74075945d80 100644 --- a/pkg/resourcequota/resource_quota_controller.go +++ b/pkg/resourcequota/resource_quota_controller.go @@ -130,17 +130,13 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err case api.ResourceMemory: val := int64(0) for i := range pods.Items { - for j := range pods.Items[i].Spec.Containers { - val = val + pods.Items[i].Spec.Containers[j].Memory.Value() - } + val = val + PodMemory(&pods.Items[i]).Value() } value = resource.NewQuantity(int64(val), resource.DecimalSI) case api.ResourceCPU: val := int64(0) for i := range pods.Items { - for j := range pods.Items[i].Spec.Containers { - val = val + pods.Items[i].Spec.Containers[j].CPU.MilliValue() - } + val = val + PodCPU(&pods.Items[i]).MilliValue() } value = resource.NewMilliQuantity(int64(val), resource.DecimalSI) case api.ResourceServices: @@ -178,3 +174,21 @@ func (rm *ResourceQuotaManager) syncResourceQuota(quota api.ResourceQuota) (err } return nil } + +// PodCPU computes total cpu usage of a pod +func PodCPU(pod *api.Pod) *resource.Quantity { + val := int64(0) + for j := range pod.Spec.Containers { + val = val + pod.Spec.Containers[j].CPU.MilliValue() + } + return resource.NewMilliQuantity(int64(val), resource.DecimalSI) +} + +// PodMemory computes the memory usage of a pod +func PodMemory(pod *api.Pod) *resource.Quantity { + val := int64(0) + for j := range pod.Spec.Containers { + val = val + pod.Spec.Containers[j].Memory.Value() + } + return resource.NewQuantity(int64(val), resource.DecimalSI) +} diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go new file mode 100644 index 00000000000..dfd1c88cd91 --- /dev/null +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -0,0 +1,173 @@ +/* +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 ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" +) + +func init() { + admission.RegisterPlugin("ResourceQuota", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return "a{client: client}, nil + }) +} + +type quota struct { + client client.Interface +} + +var kindToResourceName = map[string]api.ResourceName{ + "pods": api.ResourcePods, + "services": api.ResourceServices, + "replicationControllers": api.ResourceReplicationControllers, + "resourceQuotas": api.ResourceQuotas, +} + +func (q *quota) Admit(a admission.Attributes) (err error) { + if a.GetOperation() == "DELETE" { + return nil + } + + obj := a.GetObject() + kind := a.GetKind() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + + list, err := q.client.ResourceQuotas(a.GetNamespace()).List(labels.Everything()) + if err != nil { + return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), kind)) + } + + if len(list.Items) == 0 { + return nil + } + + for i := range list.Items { + quota := list.Items[i] + dirty, err := IncrementUsage(a, "a.Status, q.client) + if err != nil { + return err + } + + if dirty { + // construct a usage record + usage := api.ResourceQuotaUsage{ + ObjectMeta: api.ObjectMeta{ + Name: quota.Name, + Namespace: quota.Namespace, + ResourceVersion: quota.ResourceVersion}, + } + usage.Status = quota.Status + err = q.client.ResourceQuotaUsages(usage.Namespace).Create(&usage) + if err != nil { + return apierrors.NewForbidden(a.GetKind(), name, fmt.Errorf("Unable to %s %s at this time because there was an error enforcing quota", a.GetOperation(), a.GetKind())) + } + } + } + return nil +} + +// IncrementUsage updates the supplied ResourceQuotaStatus object based on the incoming operation +// Return true if the usage must be recorded prior to admitting the new resource +// Return an error if the operation should not pass admission control +func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) { + obj := a.GetObject() + kind := a.GetKind() + name := "Unknown" + if obj != nil { + name, _ = meta.NewAccessor().Name(obj) + } + dirty := false + set := map[api.ResourceName]bool{} + for k := range status.Hard { + set[k] = true + } + // handle max counts for each kind of resource (pods, services, replicationControllers, etc.) + if a.GetOperation() == "CREATE" { + resourceName := kindToResourceName[a.GetKind()] + hard, hardFound := status.Hard[resourceName] + if hardFound { + used, usedFound := status.Used[resourceName] + if !usedFound { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.Value() >= hard.Value() { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s %s", hard.String(), kind)) + } else { + status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI) + dirty = true + } + } + } + // handle memory/cpu constraints, and any diff of usage based on memory/cpu on updates + if a.GetKind() == "pods" && (set[api.ResourceMemory] || set[api.ResourceCPU]) { + pod := obj.(*api.Pod) + deltaCPU := resourcequota.PodCPU(pod) + deltaMemory := resourcequota.PodMemory(pod) + // if this is an update, we need to find the delta cpu/memory usage from previous state + if a.GetOperation() == "UPDATE" { + oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name) + if err != nil { + return false, apierrors.NewForbidden(kind, name, err) + } + oldCPU := resourcequota.PodCPU(oldPod) + oldMemory := resourcequota.PodMemory(oldPod) + deltaCPU = resource.NewMilliQuantity(deltaCPU.MilliValue()-oldCPU.MilliValue(), resource.DecimalSI) + deltaMemory = resource.NewQuantity(deltaMemory.Value()-oldMemory.Value(), resource.DecimalSI) + } + + hardMem, hardMemFound := status.Hard[api.ResourceMemory] + if hardMemFound { + used, usedFound := status.Used[api.ResourceMemory] + if !usedFound { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.Value()+deltaMemory.Value() > hardMem.Value() { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s memory", hardMem.String())) + } else { + status.Used[api.ResourceMemory] = *resource.NewQuantity(used.Value()+deltaMemory.Value(), resource.DecimalSI) + dirty = true + } + } + hardCPU, hardCPUFound := status.Hard[api.ResourceCPU] + if hardCPUFound { + used, usedFound := status.Used[api.ResourceCPU] + if !usedFound { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Quota usage stats are not yet known, unable to admit resource until an accurate count is completed.")) + } + if used.MilliValue()+deltaCPU.MilliValue() > hardCPU.MilliValue() { + return false, apierrors.NewForbidden(kind, name, fmt.Errorf("Limited to %s CPU", hardCPU.String())) + } else { + status.Used[api.ResourceCPU] = *resource.NewMilliQuantity(used.MilliValue()+deltaCPU.MilliValue(), resource.DecimalSI) + dirty = true + } + } + } + return dirty, nil +} diff --git a/plugin/pkg/admission/resourcequota/admission_test.go b/plugin/pkg/admission/resourcequota/admission_test.go new file mode 100644 index 00000000000..45930005154 --- /dev/null +++ b/plugin/pkg/admission/resourcequota/admission_test.go @@ -0,0 +1,17 @@ +/* +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 diff --git a/plugin/pkg/admission/resourcequota/doc.go b/plugin/pkg/admission/resourcequota/doc.go new file mode 100644 index 00000000000..6e99dc3ab0b --- /dev/null +++ b/plugin/pkg/admission/resourcequota/doc.go @@ -0,0 +1,19 @@ +/* +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. +*/ + +// resourcequota enforces all incoming requests against any applied quota +// in the namespace context of the request +package resourcequota From 9674f08504c434afe74e9e74f728a5ddeb4e49e7 Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Mon, 26 Jan 2015 13:43:55 -0500 Subject: [PATCH 4/5] Added unit tests for incrementing usage --- pkg/client/fake.go | 1 + pkg/client/fake_replication_controllers.go | 2 +- .../pkg/admission/resourcequota/admission.go | 6 +- .../admission/resourcequota/admission_test.go | 347 ++++++++++++++++++ 4 files changed, 354 insertions(+), 2 deletions(-) diff --git a/pkg/client/fake.go b/pkg/client/fake.go index 0c4c209fa44..42bd2e5f36d 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -36,6 +36,7 @@ type FakeAction struct { type Fake struct { Actions []FakeAction PodsList api.PodList + CtrlList api.ReplicationControllerList Ctrl api.ReplicationController ServiceList api.ServiceList EndpointsList api.EndpointsList diff --git a/pkg/client/fake_replication_controllers.go b/pkg/client/fake_replication_controllers.go index 589cac62b79..17a1f02f9df 100644 --- a/pkg/client/fake_replication_controllers.go +++ b/pkg/client/fake_replication_controllers.go @@ -31,7 +31,7 @@ type FakeReplicationControllers struct { func (c *FakeReplicationControllers) List(selector labels.Selector) (*api.ReplicationControllerList, error) { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-controllers"}) - return &api.ReplicationControllerList{}, nil + return api.Scheme.CopyOrDie(&c.Fake.CtrlList).(*api.ReplicationControllerList), nil } func (c *FakeReplicationControllers) Get(name string) (*api.ReplicationController, error) { diff --git a/plugin/pkg/admission/resourcequota/admission.go b/plugin/pkg/admission/resourcequota/admission.go index dfd1c88cd91..3093f95f5fe 100644 --- a/plugin/pkg/admission/resourcequota/admission.go +++ b/plugin/pkg/admission/resourcequota/admission.go @@ -32,7 +32,7 @@ import ( func init() { admission.RegisterPlugin("ResourceQuota", func(client client.Interface, config io.Reader) (admission.Interface, error) { - return "a{client: client}, nil + return NewResourceQuota(client), nil }) } @@ -40,6 +40,10 @@ type quota struct { client client.Interface } +func NewResourceQuota(client client.Interface) admission.Interface { + return "a{client: client} +} + var kindToResourceName = map[string]api.ResourceName{ "pods": api.ResourcePods, "services": api.ResourceServices, diff --git a/plugin/pkg/admission/resourcequota/admission_test.go b/plugin/pkg/admission/resourcequota/admission_test.go index 45930005154..77b06ce4377 100644 --- a/plugin/pkg/admission/resourcequota/admission_test.go +++ b/plugin/pkg/admission/resourcequota/admission_test.go @@ -15,3 +15,350 @@ limitations under the License. */ package resourcequota + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func TestAdmissionIgnoresDelete(t *testing.T) { + namespace := "default" + handler := NewResourceQuota(&client.Fake{}) + err := handler.Admit(admission.NewAttributesRecord(nil, namespace, "pods", "DELETE")) + if err != nil { + t.Errorf("ResourceQuota should admit all deletes", err) + } +} + +func TestIncrementUsagePods(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourcePods + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.Pod{}, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestIncrementUsageMemory(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceMemory + status.Hard[r] = resource.MustParse("2Gi") + status.Used[r] = resource.MustParse("1Gi") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }} + dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + expectedVal := resource.MustParse("2Gi") + quantity := status.Used[r] + if quantity.Value() != expectedVal.Value() { + t.Errorf("Expected %v was %v", expectedVal.Value(), quantity.Value()) + } +} + +func TestExceedUsageMemory(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceMemory + status.Hard[r] = resource.MustParse("2Gi") + status.Used[r] = resource.MustParse("1Gi") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("3Gi"), CPU: resource.MustParse("100m")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected memory usage exceeded error") + } +} + +func TestIncrementUsageCPU(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceCPU + status.Hard[r] = resource.MustParse("200m") + status.Used[r] = resource.MustParse("100m") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }} + dirty, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + expectedVal := resource.MustParse("200m") + quantity := status.Used[r] + if quantity.Value() != expectedVal.Value() { + t.Errorf("Expected %v was %v", expectedVal.Value(), quantity.Value()) + } +} + +func TestExceedUsageCPU(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceCPU + status.Hard[r] = resource.MustParse("200m") + status.Used[r] = resource.MustParse("100m") + + newPod := &api.Pod{ + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("500m")}}, + }} + _, err := IncrementUsage(admission.NewAttributesRecord(newPod, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected CPU usage exceeded error") + } +} + +func TestExceedUsagePods(t *testing.T) { + namespace := "default" + client := &client.Fake{ + PodsList: api.PodList{ + Items: []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + Spec: api.PodSpec{ + Volumes: []api.Volume{{Name: "vol"}}, + Containers: []api.Container{{Name: "ctr", Image: "image", Memory: resource.MustParse("1Gi"), CPU: resource.MustParse("100m")}}, + }, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourcePods + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.Pod{}, namespace, "pods", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error because this would exceed your quota") + } +} + +func TestIncrementUsageServices(t *testing.T) { + namespace := "default" + client := &client.Fake{ + ServiceList: api.ServiceList{ + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceServices + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.Service{}, namespace, "services", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestExceedUsageServices(t *testing.T) { + namespace := "default" + client := &client.Fake{ + ServiceList: api.ServiceList{ + Items: []api.Service{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceServices + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.Service{}, namespace, "services", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error because this would exceed usage") + } +} + +func TestIncrementUsageReplicationControllers(t *testing.T) { + namespace := "default" + client := &client.Fake{ + CtrlList: api.ReplicationControllerList{ + Items: []api.ReplicationController{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceReplicationControllers + status.Hard[r] = resource.MustParse("2") + status.Used[r] = resource.MustParse("1") + dirty, err := IncrementUsage(admission.NewAttributesRecord(&api.ReplicationController{}, namespace, "replicationControllers", "CREATE"), status, client) + if err != nil { + t.Errorf("Unexpected error", err) + } + if !dirty { + t.Errorf("Expected the status to get incremented, therefore should have been dirty") + } + quantity := status.Used[r] + if quantity.Value() != int64(2) { + t.Errorf("Expected new item count to be 2, but was %s", quantity.String()) + } +} + +func TestExceedUsageReplicationControllers(t *testing.T) { + namespace := "default" + client := &client.Fake{ + CtrlList: api.ReplicationControllerList{ + Items: []api.ReplicationController{ + { + ObjectMeta: api.ObjectMeta{Name: "123", Namespace: namespace}, + }, + }, + }, + } + status := &api.ResourceQuotaStatus{ + Hard: api.ResourceList{}, + Used: api.ResourceList{}, + } + r := api.ResourceReplicationControllers + status.Hard[r] = resource.MustParse("1") + status.Used[r] = resource.MustParse("1") + _, err := IncrementUsage(admission.NewAttributesRecord(&api.ReplicationController{}, namespace, "replicationControllers", "CREATE"), status, client) + if err == nil { + t.Errorf("Expected error for exceeding hard limits") + } +} From 0ff20c84dd5bd078b308f92a5e27c03e58bb2212 Mon Sep 17 00:00:00 2001 From: derekwaynecarr Date: Wed, 28 Jan 2015 15:09:42 -0500 Subject: [PATCH 5/5] Rebase errors --- pkg/api/types.go | 1 - pkg/api/v1beta1/types.go | 1 - pkg/api/v1beta2/types.go | 1 - pkg/api/v1beta3/types.go | 1 - 4 files changed, 4 deletions(-) diff --git a/pkg/api/types.go b/pkg/api/types.go index 4b81bdca829..ca44d616ed1 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1133,7 +1133,6 @@ type List struct { Items []runtime.Object `json:"items"` } -<<<<<<< HEAD // A type of object that is limited type LimitType string diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index cfa4b7522dc..650d82b6c63 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -893,7 +893,6 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } -<<<<<<< HEAD // A type of object that is limited type LimitType string diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index bc8cfd9d172..624bba5b66d 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -895,7 +895,6 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } -<<<<<<< HEAD // A type of object that is limited type LimitType string diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index c44090e1e1b..f7359ac10a3 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1055,7 +1055,6 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } -<<<<<<< HEAD // A type of object that is limited type LimitType string