diff --git a/cmd/kube-apiserver/plugins.go b/cmd/kube-apiserver/plugins.go index 366b211ecf6..de652600880 100644 --- a/cmd/kube-apiserver/plugins.go +++ b/cmd/kube-apiserver/plugins.go @@ -28,5 +28,6 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/admit" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/deny" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/limitranger" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcedefaults" ) diff --git a/examples/limitrange/invalid-pod.json b/examples/limitrange/invalid-pod.json new file mode 100644 index 00000000000..28a4d83e042 --- /dev/null +++ b/examples/limitrange/invalid-pod.json @@ -0,0 +1,18 @@ +{ + "id": "invalid-pod", + "kind": "Pod", + "apiVersion":"v1beta2", + "labels": { + "name": "invalid-pod" + }, + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "invalid-pod", + "containers": [{ + "name": "kubernetes-serve-hostname", + "image": "kubernetes/serve_hostname", + }] + } + }, +} \ No newline at end of file diff --git a/examples/limitrange/limit-range.json b/examples/limitrange/limit-range.json new file mode 100644 index 00000000000..f3ade396f45 --- /dev/null +++ b/examples/limitrange/limit-range.json @@ -0,0 +1,31 @@ +{ + "id": "limits", + "kind": "LimitRange", + "apiVersion": "v1beta1", + "spec": { + "limits": [ + { + "type": "Pod", + "max": { + "memory": "1073741824", + "cpu": "2", + }, + "min": { + "memory": "1048576", + "cpu": "0.25" + } + }, + { + "type": "Container", + "max": { + "memory": "1073741824", + "cpu": "2", + }, + "min": { + "memory": "1048576", + "cpu": "0.25" + } + }, + ], + } +} diff --git a/examples/limitrange/valid-pod.json b/examples/limitrange/valid-pod.json new file mode 100644 index 00000000000..3f606840815 --- /dev/null +++ b/examples/limitrange/valid-pod.json @@ -0,0 +1,20 @@ +{ + "id": "valid-pod", + "kind": "Pod", + "apiVersion":"v1beta2", + "labels": { + "name": "valid-pod" + }, + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "invalid-pod", + "containers": [{ + "name": "kubernetes-serve-hostname", + "image": "kubernetes/serve_hostname", + "cpu": 1000, + "memory": 1048576, + }] + } + }, +} \ No newline at end of file diff --git a/pkg/api/register.go b/pkg/api/register.go index 5c138425b18..15fbb1f2809 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -47,6 +47,8 @@ func init() { &BoundPod{}, &BoundPods{}, &List{}, + &LimitRange{}, + &LimitRangeList{}, ) // Legacy names are supported Scheme.AddKnownTypeWithName("", "Minion", &Node{}) @@ -77,3 +79,5 @@ func (*ContainerManifestList) IsAnAPIObject() {} func (*BoundPod) IsAnAPIObject() {} func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} +func (*LimitRange) IsAnAPIObject() {} +func (*LimitRangeList) IsAnAPIObject() {} diff --git a/pkg/api/types.go b/pkg/api/types.go index a800631a8cd..7a85d4209fb 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1138,3 +1138,47 @@ type List struct { Items []runtime.Object `json:"items"` } + +// A type of object that is limited +type LimitType string + +const ( + // Limit that applies to all pods in a namespace + LimitTypePod LimitType = "Pod" + // Limit that applies to all containers in a namespace + LimitTypeContainer LimitType = "Container" +) + +// LimitRangeItem defines a min/max usage limit for any resource that matches on kind +type LimitRangeItem struct { + // Type of resource that this limit applies to + Type LimitType `json:"type,omitempty"` + // Max usage constraints on this kind by resource name + Max ResourceList `json:"max,omitempty"` + // Min usage constraints on this kind by resource name + Min ResourceList `json:"min,omitempty"` +} + +// LimitRangeSpec defines a min/max usage limit for resources that match on kind +type LimitRangeSpec struct { + // Limits is the list of LimitRangeItem objects that are enforced + Limits []LimitRangeItem `json:"limits"` +} + +// LimitRange sets resource usage limits for each kind of resource in a Namespace +type LimitRange struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the limits enforced + Spec LimitRangeSpec `json:"spec,omitempty"` +} + +// LimitRangeList is a list of LimitRange items. +type LimitRangeList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + // Items is a list of LimitRange objects + Items []LimitRange `json:"items"` +} diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 52600138907..3e581da3d9a 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -568,7 +568,72 @@ func init() { out.Status.HostIP = in.HostIP return s.Convert(&in.NodeResources.Capacity, &out.Spec.Capacity, 0) }, - + func(in *newer.LimitRange, out *LimitRange, 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 + } + return nil + }, + func(in *LimitRange, out *newer.LimitRange, 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 + } + return nil + }, + func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { + *out = LimitRangeSpec{} + out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) + for i := range in.Limits { + if err := s.Convert(&in.Limits[i], &out.Limits[i], 0); err != nil { + return err + } + } + return nil + }, + func(in *LimitRangeSpec, out *newer.LimitRangeSpec, s conversion.Scope) error { + *out = newer.LimitRangeSpec{} + out.Limits = make([]newer.LimitRangeItem, len(in.Limits), len(in.Limits)) + for i := range in.Limits { + if err := s.Convert(&in.Limits[i], &out.Limits[i], 0); err != nil { + return err + } + } + return nil + }, + func(in *newer.LimitRangeItem, out *LimitRangeItem, s conversion.Scope) error { + *out = LimitRangeItem{} + out.Type = LimitType(in.Type) + if err := s.Convert(&in.Max, &out.Max, 0); err != nil { + return err + } + if err := s.Convert(&in.Min, &out.Min, 0); err != nil { + return err + } + return nil + }, + func(in *LimitRangeItem, out *newer.LimitRangeItem, s conversion.Scope) error { + *out = newer.LimitRangeItem{} + out.Type = newer.LimitType(in.Type) + if err := s.Convert(&in.Max, &out.Max, 0); err != nil { + return err + } + if err := s.Convert(&in.Min, &out.Min, 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 9d5e19bc8e8..992f2ed733f 100644 --- a/pkg/api/v1beta1/register.go +++ b/pkg/api/v1beta1/register.go @@ -48,6 +48,8 @@ func init() { &BoundPod{}, &BoundPods{}, &List{}, + &LimitRange{}, + &LimitRangeList{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta1", "Node", &Minion{}) @@ -78,3 +80,5 @@ func (*ContainerManifestList) IsAnAPIObject() {} func (*BoundPod) IsAnAPIObject() {} func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} +func (*LimitRange) IsAnAPIObject() {} +func (*LimitRangeList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index ecdbf672db8..e45d135a191 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -904,3 +904,45 @@ type List struct { TypeMeta `json:",inline"` Items []runtime.RawExtension `json:"items" description:"list of objects"` } + +// A type of object that is limited +type LimitType string + +const ( + // Limit that applies to all pods in a namespace + LimitTypePod LimitType = "Pod" + // Limit that applies to all containers in a namespace + LimitTypeContainer LimitType = "Container" +) + +// LimitRangeItem defines a min/max usage limit for any resource that matches on kind +type LimitRangeItem struct { + // Type of resource that this limit applies to + Type LimitType `json:"type,omitempty"` + // Max usage constraints on this kind by resource name + Max ResourceList `json:"max,omitempty"` + // Min usage constraints on this kind by resource name + Min ResourceList `json:"min,omitempty"` +} + +// LimitRangeSpec defines a min/max usage limit for resources that match on kind +type LimitRangeSpec struct { + // Limits is the list of LimitRangeItem objects that are enforced + Limits []LimitRangeItem `json:"limits"` +} + +// LimitRange sets resource usage limits for each kind of resource in a Namespace +type LimitRange struct { + TypeMeta `json:",inline"` + + // Spec defines the limits enforced + Spec LimitRangeSpec `json:"spec,omitempty"` +} + +// LimitRangeList is a list of LimitRange items. +type LimitRangeList struct { + TypeMeta `json:",inline"` + + // Items is a list of LimitRange objects + Items []LimitRange `json:"items"` +} diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 6862bae8709..d373212c516 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -485,7 +485,72 @@ func init() { out.Status.HostIP = in.HostIP return s.Convert(&in.NodeResources.Capacity, &out.Spec.Capacity, 0) }, - + func(in *newer.LimitRange, out *LimitRange, 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 + } + return nil + }, + func(in *LimitRange, out *newer.LimitRange, 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 + } + return nil + }, + func(in *newer.LimitRangeSpec, out *LimitRangeSpec, s conversion.Scope) error { + *out = LimitRangeSpec{} + out.Limits = make([]LimitRangeItem, len(in.Limits), len(in.Limits)) + for i := range in.Limits { + if err := s.Convert(&in.Limits[i], &out.Limits[i], 0); err != nil { + return err + } + } + return nil + }, + func(in *LimitRangeSpec, out *newer.LimitRangeSpec, s conversion.Scope) error { + *out = newer.LimitRangeSpec{} + out.Limits = make([]newer.LimitRangeItem, len(in.Limits), len(in.Limits)) + for i := range in.Limits { + if err := s.Convert(&in.Limits[i], &out.Limits[i], 0); err != nil { + return err + } + } + return nil + }, + func(in *newer.LimitRangeItem, out *LimitRangeItem, s conversion.Scope) error { + *out = LimitRangeItem{} + out.Type = LimitType(in.Type) + if err := s.Convert(&in.Max, &out.Max, 0); err != nil { + return err + } + if err := s.Convert(&in.Min, &out.Min, 0); err != nil { + return err + } + return nil + }, + func(in *LimitRangeItem, out *newer.LimitRangeItem, s conversion.Scope) error { + *out = newer.LimitRangeItem{} + out.Type = newer.LimitType(in.Type) + if err := s.Convert(&in.Max, &out.Max, 0); err != nil { + return err + } + if err := s.Convert(&in.Min, &out.Min, 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 7682d9ad246..2990dbbf739 100644 --- a/pkg/api/v1beta2/register.go +++ b/pkg/api/v1beta2/register.go @@ -48,6 +48,8 @@ func init() { &BoundPod{}, &BoundPods{}, &List{}, + &LimitRange{}, + &LimitRangeList{}, ) // Future names are supported api.Scheme.AddKnownTypeWithName("v1beta2", "Node", &Minion{}) @@ -78,3 +80,5 @@ func (*ContainerManifestList) IsAnAPIObject() {} func (*BoundPod) IsAnAPIObject() {} func (*BoundPods) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} +func (*LimitRange) IsAnAPIObject() {} +func (*LimitRangeList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index a386857bb54..a19f11cbd2f 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -906,3 +906,45 @@ type List struct { TypeMeta `json:",inline"` Items []runtime.RawExtension `json:"items" description:"list of objects"` } + +// A type of object that is limited +type LimitType string + +const ( + // Limit that applies to all pods in a namespace + LimitTypePod LimitType = "Pod" + // Limit that applies to all containers in a namespace + LimitTypeContainer LimitType = "Container" +) + +// LimitRangeItem defines a min/max usage limit for any resource that matches on kind +type LimitRangeItem struct { + // Type of resource that this limit applies to + Type LimitType `json:"type,omitempty"` + // Max usage constraints on this kind by resource name + Max ResourceList `json:"max,omitempty"` + // Min usage constraints on this kind by resource name + Min ResourceList `json:"min,omitempty"` +} + +// LimitRangeSpec defines a min/max usage limit for resources that match on kind +type LimitRangeSpec struct { + // Limits is the list of LimitRangeItem objects that are enforced + Limits []LimitRangeItem `json:"limits"` +} + +// LimitRange sets resource usage limits for each kind of resource in a Namespace +type LimitRange struct { + TypeMeta `json:",inline"` + + // Spec defines the limits enforced + Spec LimitRangeSpec `json:"spec,omitempty"` +} + +// LimitRangeList is a list of LimitRange items. +type LimitRangeList struct { + TypeMeta `json:",inline"` + + // Items is a list of LimitRange objects + Items []LimitRange `json:"items"` +} diff --git a/pkg/api/v1beta3/register.go b/pkg/api/v1beta3/register.go index 6594cb67e89..75f2c04e81c 100644 --- a/pkg/api/v1beta3/register.go +++ b/pkg/api/v1beta3/register.go @@ -48,6 +48,8 @@ func init() { &Event{}, &EventList{}, &List{}, + &LimitRange{}, + &LimitRangeList{}, ) // Legacy names are supported api.Scheme.AddKnownTypeWithName("v1beta3", "Minion", &Node{}) @@ -78,3 +80,5 @@ func (*OperationList) IsAnAPIObject() {} func (*Event) IsAnAPIObject() {} func (*EventList) IsAnAPIObject() {} func (*List) IsAnAPIObject() {} +func (*LimitRange) IsAnAPIObject() {} +func (*LimitRangeList) IsAnAPIObject() {} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 2947e2b1642..314521b6618 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -1066,3 +1066,47 @@ type List struct { Items []runtime.RawExtension `json:"items" description:"list of objects"` } + +// A type of object that is limited +type LimitType string + +const ( + // Limit that applies to all pods in a namespace + LimitTypePod LimitType = "Pod" + // Limit that applies to all containers in a namespace + LimitTypeContainer LimitType = "Container" +) + +// LimitRangeItem defines a min/max usage limit for any resource that matches on kind +type LimitRangeItem struct { + // Type of resource that this limit applies to + Type LimitType `json:"type,omitempty"` + // Max usage constraints on this kind by resource name + Max ResourceList `json:"max,omitempty"` + // Min usage constraints on this kind by resource name + Min ResourceList `json:"min,omitempty"` +} + +// LimitRangeSpec defines a min/max usage limit for resources that match on kind +type LimitRangeSpec struct { + // Limits is the list of LimitRangeItem objects that are enforced + Limits []LimitRangeItem `json:"limits"` +} + +// LimitRange sets resource usage limits for each kind of resource in a Namespace +type LimitRange struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the limits enforced + Spec LimitRangeSpec `json:"spec,omitempty"` +} + +// LimitRangeList is a list of LimitRange items. +type LimitRangeList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + // Items is a list of LimitRange objects + Items []LimitRange `json:"items"` +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 8cdaaedfa78..22888fd315f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -640,3 +640,29 @@ func ValidateResourceName(str string) errs.ValidationErrorList { return errs.ValidationErrorList{} } + +// ValidateLimitRange tests if required fields in the LimitRange are set. +func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(limitRange.Name) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("name", limitRange.Name)) + } else if !util.IsDNSSubdomain(limitRange.Name) { + allErrs = append(allErrs, errs.NewFieldInvalid("name", limitRange.Name, "")) + } + if len(limitRange.Namespace) == 0 { + allErrs = append(allErrs, errs.NewFieldRequired("namespace", limitRange.Namespace)) + } else if !util.IsDNSSubdomain(limitRange.Namespace) { + allErrs = append(allErrs, errs.NewFieldInvalid("namespace", limitRange.Namespace, "")) + } + // ensure resource names are properly qualified per docs/resources.md + for i := range limitRange.Spec.Limits { + limit := limitRange.Spec.Limits[i] + for k := range limit.Max { + allErrs = append(allErrs, ValidateResourceName(string(k))...) + } + for k := range limit.Min { + 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 52fa51997e1..4555eeab6ca 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1552,3 +1552,92 @@ func TestValidateResourceNames(t *testing.T) { } } } + +func TestValidateLimitRange(t *testing.T) { + successCases := []api.LimitRange{ + { + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + }, + } + for _, successCase := range successCases { + if errs := ValidateLimitRange(&successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := map[string]api.LimitRange{ + "zero-length Name": { + ObjectMeta: api.ObjectMeta{ + Name: "", + Namespace: "foo", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + }, + "zero-length-namespace": { + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + }, + } + for k, v := range errorCases { + errs := ValidateLimitRange(&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 8a92fc6ce3f..67d7258cc4b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -37,6 +37,7 @@ type Interface interface { VersionInterface NodesInterface EventNamespacer + LimitRangesNamespacer } func (c *Client) ReplicationControllers(namespace string) ReplicationControllerInterface { @@ -63,6 +64,10 @@ func (c *Client) Services(namespace string) ServiceInterface { return newServices(c, namespace) } +func (c *Client) LimitRanges(namespace string) LimitRangeInterface { + return newLimitRanges(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 7cb4c5d059b..dfa0f8d9f1d 100644 --- a/pkg/client/fake.go +++ b/pkg/client/fake.go @@ -34,15 +34,20 @@ 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 - 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 + Err error + Watch watch.Interface +} + +func (c *Fake) LimitRanges(namespace string) LimitRangeInterface { + return &FakeLimitRanges{Fake: c, Namespace: namespace} } func (c *Fake) ReplicationControllers(namespace string) ReplicationControllerInterface { diff --git a/pkg/client/fake_limit_ranges.go b/pkg/client/fake_limit_ranges.go new file mode 100644 index 00000000000..9d1c7c237c5 --- /dev/null +++ b/pkg/client/fake_limit_ranges.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" +) + +// FakeLimitRanges implements PodsInterface. 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 FakeLimitRanges struct { + Fake *Fake + Namespace string +} + +func (c *FakeLimitRanges) List(selector labels.Selector) (*api.LimitRangeList, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "list-limitRanges"}) + return api.Scheme.CopyOrDie(&c.Fake.LimitRangesList).(*api.LimitRangeList), nil +} + +func (c *FakeLimitRanges) Get(name string) (*api.LimitRange, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "get-limitRange", Value: name}) + return &api.LimitRange{ObjectMeta: api.ObjectMeta{Name: name, Namespace: c.Namespace}}, nil +} + +func (c *FakeLimitRanges) Delete(name string) error { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-limitRange", Value: name}) + return nil +} + +func (c *FakeLimitRanges) Create(limitRange *api.LimitRange) (*api.LimitRange, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "create-limitRange"}) + return &api.LimitRange{}, nil +} + +func (c *FakeLimitRanges) Update(limitRange *api.LimitRange) (*api.LimitRange, error) { + c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "update-limitRange", Value: limitRange.Name}) + return &api.LimitRange{}, nil +} diff --git a/pkg/client/limit_ranges.go b/pkg/client/limit_ranges.go new file mode 100644 index 00000000000..fa03c61178e --- /dev/null +++ b/pkg/client/limit_ranges.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" +) + +// LimitRangesNamespacer has methods to work with LimitRange resources in a namespace +type LimitRangesNamespacer interface { + LimitRanges(namespace string) LimitRangeInterface +} + +// LimitRangeInterface has methods to work with LimitRange resources. +type LimitRangeInterface interface { + List(selector labels.Selector) (*api.LimitRangeList, error) + Get(name string) (*api.LimitRange, error) + Delete(name string) error + Create(limitRange *api.LimitRange) (*api.LimitRange, error) + Update(limitRange *api.LimitRange) (*api.LimitRange, error) +} + +// limitRanges implements LimitRangesNamespacer interface +type limitRanges struct { + r *Client + ns string +} + +// newLimitRanges returns a limitRanges +func newLimitRanges(c *Client, namespace string) *limitRanges { + return &limitRanges{ + r: c, + ns: namespace, + } +} + +// List takes a selector, and returns the list of limitRanges that match that selector. +func (c *limitRanges) List(selector labels.Selector) (result *api.LimitRangeList, err error) { + result = &api.LimitRangeList{} + err = c.r.Get().Namespace(c.ns).Resource("limitRanges").SelectorParam("labels", selector).Do().Into(result) + return +} + +// Get takes the name of the limitRange, and returns the corresponding Pod object, and an error if it occurs +func (c *limitRanges) Get(name string) (result *api.LimitRange, err error) { + if len(name) == 0 { + return nil, errors.New("name is required parameter to Get") + } + + result = &api.LimitRange{} + err = c.r.Get().Namespace(c.ns).Resource("limitRanges").Name(name).Do().Into(result) + return +} + +// Delete takes the name of the limitRange, and returns an error if one occurs +func (c *limitRanges) Delete(name string) error { + return c.r.Delete().Namespace(c.ns).Resource("limitRanges").Name(name).Do().Error() +} + +// Create takes the representation of a limitRange. Returns the server's representation of the limitRange, and an error, if it occurs. +func (c *limitRanges) Create(limitRange *api.LimitRange) (result *api.LimitRange, err error) { + result = &api.LimitRange{} + err = c.r.Post().Namespace(c.ns).Resource("limitRanges").Body(limitRange).Do().Into(result) + return +} + +// Update takes the representation of a limitRange to update. Returns the server's representation of the limitRange, and an error, if it occurs. +func (c *limitRanges) Update(limitRange *api.LimitRange) (result *api.LimitRange, err error) { + result = &api.LimitRange{} + if len(limitRange.ResourceVersion) == 0 { + err = fmt.Errorf("invalid update object, missing resource version: %v", limitRange) + return + } + err = c.r.Put().Namespace(c.ns).Resource("limitRanges").Name(limitRange.Name).Body(limitRange).Do().Into(result) + return +} diff --git a/pkg/client/limit_ranges_test.go b/pkg/client/limit_ranges_test.go new file mode 100644 index 00000000000..126baa7ac5f --- /dev/null +++ b/pkg/client/limit_ranges_test.go @@ -0,0 +1,194 @@ +/* +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" + //"github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestLimitRangeCreate(t *testing.T) { + ns := api.NamespaceDefault + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "POST", + Path: buildResourcePath(ns, "/limitRanges"), + Query: buildQueryValues(ns, nil), + Body: limitRange, + }, + Response: Response{StatusCode: 200, Body: limitRange}, + } + + response, err := c.Setup().LimitRanges(ns).Create(limitRange) + c.Validate(t, response, err) +} + +func TestLimitRangeGet(t *testing.T) { + ns := api.NamespaceDefault + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: buildResourcePath(ns, "/limitRanges/abc"), + Query: buildQueryValues(ns, nil), + Body: nil, + }, + Response: Response{StatusCode: 200, Body: limitRange}, + } + + response, err := c.Setup().LimitRanges(ns).Get("abc") + c.Validate(t, response, err) +} + +func TestLimitRangeList(t *testing.T) { + ns := api.NamespaceDefault + + limitRangeList := &api.LimitRangeList{ + Items: []api.LimitRange{ + { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + }, + }, + } + c := &testClient{ + Request: testRequest{ + Method: "GET", + Path: buildResourcePath(ns, "/limitRanges"), + Query: buildQueryValues(ns, nil), + Body: nil, + }, + Response: Response{StatusCode: 200, Body: limitRangeList}, + } + response, err := c.Setup().LimitRanges(ns).List(labels.Everything()) + c.Validate(t, response, err) +} + +func TestLimitRangeUpdate(t *testing.T) { + ns := api.NamespaceDefault + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + ResourceVersion: "1", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/limitRanges/abc"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: limitRange}, + } + response, err := c.Setup().LimitRanges(ns).Update(limitRange) + c.Validate(t, response, err) +} + +func TestInvalidLimitRangeUpdate(t *testing.T) { + ns := api.NamespaceDefault + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + } + c := &testClient{ + Request: testRequest{Method: "PUT", Path: buildResourcePath(ns, "/limitRanges/abc"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200, Body: limitRange}, + } + _, err := c.Setup().LimitRanges(ns).Update(limitRange) + if err == nil { + t.Errorf("Expected an error due to missing ResourceVersion") + } +} + +func TestLimitRangeDelete(t *testing.T) { + ns := api.NamespaceDefault + c := &testClient{ + Request: testRequest{Method: "DELETE", Path: buildResourcePath(ns, "/limitRanges/foo"), Query: buildQueryValues(ns, nil)}, + Response: Response{StatusCode: 200}, + } + err := c.Setup().LimitRanges(ns).Delete("foo") + c.Validate(t, nil, err) +} diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 3ebb5ca9ccb..9c742a41b8c 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -47,10 +47,65 @@ func DescriberFor(kind string, c *client.Client) (Describer, bool) { return &ServiceDescriber{c}, true case "Minion", "Node": return &MinionDescriber{c}, true + case "LimitRange": + return &LimitRangeDescriber{c}, true } return nil, false } +// LimitRangeDescriber generates information about a limit range +type LimitRangeDescriber struct { + client.Interface +} + +func (d *LimitRangeDescriber) Describe(namespace, name string) (string, error) { + lr := d.LimitRanges(namespace) + + limitRange, err := lr.Get(name) + if err != nil { + return "", err + } + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "Name:\t%s\n", limitRange.Name) + fmt.Fprintf(out, "Type\tResource\tMin\tMax\n") + fmt.Fprintf(out, "----\t--------\t---\t---\n") + for i := range limitRange.Spec.Limits { + item := limitRange.Spec.Limits[i] + maxResources := item.Max + minResources := item.Min + + set := map[api.ResourceName]bool{} + for k := range maxResources { + set[k] = true + } + for k := range minResources { + set[k] = true + } + + for k := range set { + // if no value is set, we output - + maxValue := "-" + minValue := "-" + + maxQuantity, maxQuantityFound := maxResources[k] + if maxQuantityFound { + maxValue = maxQuantity.String() + } + + minQuantity, minQuantityFound := minResources[k] + if minQuantityFound { + minValue = minQuantity.String() + } + + msg := "%v\t%v\t%v\t%v\n" + fmt.Fprintf(out, msg, item.Type, k, minValue, maxValue) + } + } + 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 07530b4ef25..72b5c0d8f35 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -143,11 +143,12 @@ func (e ShortcutExpander) VersionAndKindForResource(resource string) (defaultVer // indeed a shortcut. Otherwise, will return resource unmodified. func expandResourceShortcut(resource string) string { shortForms := map[string]string{ - "po": "pods", - "rc": "replicationcontrollers", - "se": "services", - "mi": "minions", - "ev": "events", + "po": "pods", + "rc": "replicationcontrollers", + "se": "services", + "mi": "minions", + "ev": "events", + "limits": "limitRanges", } if expanded, ok := shortForms[resource]; ok { return expanded diff --git a/pkg/kubectl/resource_printer.go b/pkg/kubectl/resource_printer.go index 79321044a55..73d0464e63e 100644 --- a/pkg/kubectl/resource_printer.go +++ b/pkg/kubectl/resource_printer.go @@ -221,6 +221,7 @@ var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT"} var minionColumns = []string{"NAME", "LABELS", "STATUS"} var statusColumns = []string{"STATUS"} var eventColumns = []string{"TIME", "NAME", "KIND", "SUBOBJECT", "REASON", "SOURCE", "MESSAGE"} +var limitRangeColumns = []string{"NAME"} // addDefaultHandlers adds print handlers for default Kubernetes types. func (h *HumanReadablePrinter) addDefaultHandlers() { @@ -235,6 +236,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { h.Handler(statusColumns, printStatus) h.Handler(eventColumns, printEvent) h.Handler(eventColumns, printEventList) + h.Handler(limitRangeColumns, printLimitRange) + h.Handler(limitRangeColumns, printLimitRangeList) } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { @@ -409,6 +412,24 @@ func printEventList(list *api.EventList, w io.Writer) error { return nil } +func printLimitRange(limitRange *api.LimitRange, w io.Writer) error { + _, err := fmt.Fprintf( + w, "%s\n", + limitRange.Name, + ) + return err +} + +// Prints the LimitRangeList in a human-friendly format. +func printLimitRangeList(list *api.LimitRangeList, w io.Writer) error { + for i := range list.Items { + if err := printLimitRange(&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/master/master.go b/pkg/master/master.go index be5ad949b41..765c43249a6 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -47,6 +47,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/etcd" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/event" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic" + "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/service" @@ -109,6 +110,7 @@ type Master struct { minionRegistry minion.Registry bindingRegistry binding.Registry eventRegistry generic.Registry + limitRangeRegistry generic.Registry storage map[string]apiserver.RESTStorage client *client.Client portalNet *net.IPNet @@ -248,6 +250,7 @@ func New(c *Config) *Master { bindingRegistry: etcd.NewRegistry(c.EtcdHelper, boundPodFactory), eventRegistry: event.NewEtcdRegistry(c.EtcdHelper, uint64(c.EventTTL.Seconds())), minionRegistry: minionRegistry, + limitRangeRegistry: limitrange.NewEtcdRegistry(c.EtcdHelper), client: c.Client, portalNet: c.PortalNet, rootWebService: new(restful.WebService), @@ -361,6 +364,8 @@ func (m *Master) init(c *Config) { // TODO: should appear only in scheduler API group. "bindings": binding.NewREST(m.bindingRegistry), + + "limitRanges": limitrange.NewREST(m.limitRangeRegistry), } apiVersions := []string{"v1beta1", "v1beta2"} diff --git a/pkg/registry/limitrange/doc.go b/pkg/registry/limitrange/doc.go new file mode 100644 index 00000000000..7619321aca3 --- /dev/null +++ b/pkg/registry/limitrange/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 limitrange provides Registry interface and it's REST +// implementation for storing LimitRange api objects. +package limitrange diff --git a/pkg/registry/limitrange/registry.go b/pkg/registry/limitrange/registry.go new file mode 100644 index 00000000000..e08d2b0d8b6 --- /dev/null +++ b/pkg/registry/limitrange/registry.go @@ -0,0 +1,48 @@ +/* +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 limitrange + +import ( + "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/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" +) + +// registry implements custom changes to generic.Etcd. +type registry struct { + *etcdgeneric.Etcd +} + +// NewEtcdRegistry returns a registry which will store LimitRange in the given helper +func NewEtcdRegistry(h tools.EtcdHelper) generic.Registry { + return registry{ + Etcd: &etcdgeneric.Etcd{ + NewFunc: func() runtime.Object { return &api.LimitRange{} }, + NewListFunc: func() runtime.Object { return &api.LimitRangeList{} }, + EndpointName: "limitranges", + KeyRootFunc: func(ctx api.Context) string { + return etcdgeneric.NamespaceKeyRootFunc(ctx, "/registry/limitranges") + }, + KeyFunc: func(ctx api.Context, id string) (string, error) { + return etcdgeneric.NamespaceKeyFunc(ctx, "/registry/limitranges", id) + }, + Helper: h, + }, + } +} diff --git a/pkg/registry/limitrange/registry_test.go b/pkg/registry/limitrange/registry_test.go new file mode 100644 index 00000000000..4c081f8d024 --- /dev/null +++ b/pkg/registry/limitrange/registry_test.go @@ -0,0 +1,121 @@ +/* +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 limitrange + +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 TestLimitRangeCreate(t *testing.T) { + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("0"), + api.ResourceMemory: resource.MustParse("100"), + }, + }, + }, + }, + } + + nodeWithLimitRange := tools.EtcdResponseWithError{ + R: &etcd.Response{ + Node: &etcd.Node{ + Value: runtime.EncodeOrDie(testapi.Codec(), limitRange), + ModifiedIndex: 1, + CreatedIndex: 1, + }, + }, + E: nil, + } + + emptyNode := tools.EtcdResponseWithError{ + R: &etcd.Response{}, + E: tools.EtcdErrorNotFound, + } + + ctx := api.NewDefaultContext() + key := "foo" + path, err := etcdgeneric.NamespaceKeyFunc(ctx, "/registry/limitranges", 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: nodeWithLimitRange, + toCreate: limitRange, + errOK: func(err error) bool { return err == nil }, + }, + "preExisting": { + existing: nodeWithLimitRange, + expect: nodeWithLimitRange, + toCreate: limitRange, + 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", name, err) + } + + 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/limitrange/rest.go b/pkg/registry/limitrange/rest.go new file mode 100644 index 00000000000..f5eaaa1a71d --- /dev/null +++ b/pkg/registry/limitrange/rest.go @@ -0,0 +1,159 @@ +/* +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 limitrange + +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 LimitRange 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 LimitRange object +func (rs *REST) Create(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + limitRange, ok := obj.(*api.LimitRange) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + + if !api.ValidNamespace(ctx, &limitRange.ObjectMeta) { + return nil, errors.NewConflict("limitRange", limitRange.Namespace, fmt.Errorf("LimitRange.Namespace does not match the provided context")) + } + + if len(limitRange.Name) == 0 { + limitRange.Name = string(util.NewUUID()) + } + + if errs := validation.ValidateLimitRange(limitRange); len(errs) > 0 { + return nil, errors.NewInvalid("limitRange", limitRange.Name, errs) + } + api.FillObjectMetaSystemFields(ctx, &limitRange.ObjectMeta) + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Create(ctx, limitRange.Name, limitRange) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, limitRange.Name) + }), nil +} + +// Update updates a LimitRange object. +func (rs *REST) Update(ctx api.Context, obj runtime.Object) (<-chan apiserver.RESTResult, error) { + limitRange, ok := obj.(*api.LimitRange) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + + if !api.ValidNamespace(ctx, &limitRange.ObjectMeta) { + return nil, errors.NewConflict("limitRange", limitRange.Namespace, fmt.Errorf("LimitRange.Namespace does not match the provided context")) + } + + oldObj, err := rs.registry.Get(ctx, limitRange.Name) + if err != nil { + return nil, err + } + + editLimitRange := oldObj.(*api.LimitRange) + + // set the editable fields on the existing object + editLimitRange.Labels = limitRange.Labels + editLimitRange.ResourceVersion = limitRange.ResourceVersion + editLimitRange.Annotations = limitRange.Annotations + editLimitRange.Spec = limitRange.Spec + + if errs := validation.ValidateLimitRange(editLimitRange); len(errs) > 0 { + return nil, errors.NewInvalid("limitRange", editLimitRange.Name, errs) + } + + return apiserver.MakeAsync(func() (runtime.Object, error) { + err := rs.registry.Update(ctx, editLimitRange.Name, editLimitRange) + if err != nil { + return nil, err + } + return rs.registry.Get(ctx, editLimitRange.Name) + }), nil +} + +// Delete deletes the LimitRange 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.LimitRange) + 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 LimitRange 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 + } + limitRange, ok := obj.(*api.LimitRange) + if !ok { + return nil, fmt.Errorf("invalid object type") + } + return limitRange, 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.LimitRange +func (*REST) New() runtime.Object { + return &api.LimitRange{} +} + +func (*REST) NewList() runtime.Object { + return &api.LimitRangeList{} +} diff --git a/pkg/registry/limitrange/rest_test.go b/pkg/registry/limitrange/rest_test.go new file mode 100644 index 00000000000..f003a69184a --- /dev/null +++ b/pkg/registry/limitrange/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 limitrange diff --git a/plugin/pkg/admission/limitranger/admission.go b/plugin/pkg/admission/limitranger/admission.go new file mode 100644 index 00000000000..5af414cd5a8 --- /dev/null +++ b/plugin/pkg/admission/limitranger/admission.go @@ -0,0 +1,175 @@ +/* +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 limitranger + +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/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func init() { + admission.RegisterPlugin("LimitRanger", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewLimitRanger(client, PodLimitFunc), nil + }) +} + +// limitRanger enforces usage limits on a per resource basis in the namespace +type limitRanger struct { + client client.Interface + limitFunc LimitFunc +} + +// Admit admits resources into cluster that do not violate any defined LimitRange in the namespace +func (l *limitRanger) Admit(a admission.Attributes) (err error) { + // ignore deletes + if a.GetOperation() == "DELETE" { + return nil + } + + // look for a limit range in current namespace that requires enforcement + // TODO: Move to cache when issue is resolved: https://github.com/GoogleCloudPlatform/kubernetes/issues/2294 + items, err := l.client.LimitRanges(a.GetNamespace()).List(labels.Everything()) + if err != nil { + return err + } + + // ensure it meets each prescribed min/max + for i := range items.Items { + limitRange := &items.Items[i] + err = l.limitFunc(limitRange, a.GetKind(), a.GetObject()) + if err != nil { + return err + } + } + return nil +} + +// NewLimitRanger returns an object that enforces limits based on the supplied limit function +func NewLimitRanger(client client.Interface, limitFunc LimitFunc) admission.Interface { + return &limitRanger{client: client, limitFunc: limitFunc} +} + +func Min(a int64, b int64) int64 { + if a < b { + return a + } + return b +} + +func Max(a int64, b int64) int64 { + if a > b { + return a + } + return b +} + +// PodLimitFunc enforces that a pod spec does not exceed any limits specified on the supplied limit range +func PodLimitFunc(limitRange *api.LimitRange, kind string, obj runtime.Object) error { + if kind != "pods" { + return nil + } + + pod := obj.(*api.Pod) + + podCPU := int64(0) + podMem := int64(0) + + minContainerCPU := int64(0) + minContainerMem := int64(0) + maxContainerCPU := int64(0) + maxContainerMem := int64(0) + + for i := range pod.Spec.Containers { + container := pod.Spec.Containers[i] + containerCPU := container.CPU.MilliValue() + containerMem := container.Memory.Value() + + if i == 0 { + minContainerCPU = containerCPU + minContainerMem = containerMem + maxContainerCPU = containerCPU + maxContainerMem = containerMem + } + + podCPU = podCPU + container.CPU.MilliValue() + podMem = podMem + container.Memory.Value() + + minContainerCPU = Min(containerCPU, minContainerCPU) + minContainerMem = Min(containerMem, minContainerMem) + maxContainerCPU = Max(containerCPU, maxContainerCPU) + maxContainerMem = Max(containerMem, maxContainerMem) + } + + for i := range limitRange.Spec.Limits { + limit := limitRange.Spec.Limits[i] + for _, minOrMax := range []string{"Min", "Max"} { + var rl api.ResourceList + switch minOrMax { + case "Min": + rl = limit.Min + case "Max": + rl = limit.Max + } + for k, v := range rl { + observed := int64(0) + enforced := int64(0) + var err error + switch k { + case api.ResourceMemory: + enforced = v.Value() + switch limit.Type { + case api.LimitTypePod: + observed = podMem + err = fmt.Errorf("%simum memory usage per pod is %s", minOrMax, v.String()) + case api.LimitTypeContainer: + observed = maxContainerMem + err = fmt.Errorf("%simum memory usage per container is %s", minOrMax, v.String()) + } + case api.ResourceCPU: + enforced = v.MilliValue() + switch limit.Type { + case api.LimitTypePod: + observed = podCPU + err = fmt.Errorf("%simum CPU usage per pod is %s, but requested %s", minOrMax, v.String(), resource.NewMilliQuantity(observed, resource.DecimalSI)) + case api.LimitTypeContainer: + observed = maxContainerCPU + err = fmt.Errorf("%simum CPU usage per container is %s", minOrMax, v.String()) + } + } + switch minOrMax { + case "Min": + if observed < enforced { + return apierrors.NewForbidden(kind, pod.Name, err) + } + case "Max": + if observed > enforced { + return apierrors.NewForbidden(kind, pod.Name, err) + } + } + } + } + } + return nil +} diff --git a/plugin/pkg/admission/limitranger/admission_test.go b/plugin/pkg/admission/limitranger/admission_test.go new file mode 100644 index 00000000000..570cdcacc52 --- /dev/null +++ b/plugin/pkg/admission/limitranger/admission_test.go @@ -0,0 +1,238 @@ +/* +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 limitranger + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" +) + +func TestPodLimitFunc(t *testing.T) { + limitRange := &api.LimitRange{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + }, + Spec: api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("200m"), + api.ResourceMemory: resource.MustParse("4Gi"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("50m"), + api.ResourceMemory: resource.MustParse("2Mi"), + }, + }, + { + Type: api.LimitTypeContainer, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + api.ResourceMemory: resource.MustParse("2Gi"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("25m"), + api.ResourceMemory: resource.MustParse("1Mi"), + }, + }, + }, + }, + } + + successCases := []api.Pod{ + { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "foo:V1", + CPU: resource.MustParse("100m"), + Memory: resource.MustParse("2Gi"), + }, + { + Image: "boo:V1", + CPU: resource.MustParse("100m"), + Memory: resource.MustParse("2Gi"), + }, + }, + }, + }, + { + ObjectMeta: api.ObjectMeta{Name: "bar"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("100m"), + Memory: resource.MustParse("2Gi"), + }, + }, + }, + }, + } + + errorCases := map[string]api.Pod{ + "min-container-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("25m"), + Memory: resource.MustParse("2Gi"), + }, + }, + }, + }, + "max-container-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("110m"), + Memory: resource.MustParse("1Gi"), + }, + }, + }, + }, + "min-container-mem": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("30m"), + Memory: resource.MustParse("0"), + }, + }, + }, + }, + "max-container-mem": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("30m"), + Memory: resource.MustParse("3Gi"), + }, + }, + }, + }, + "min-pod-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("40m"), + Memory: resource.MustParse("2Gi"), + }, + }, + }, + }, + "max-pod-cpu": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("1Mi"), + }, + { + Image: "boo:V2", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("1Mi"), + }, + { + Image: "boo:V3", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("1Mi"), + }, + { + Image: "boo:V4", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("1Mi"), + }, + }, + }, + }, + "max-pod-memory": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("2Gi"), + }, + { + Image: "boo:V2", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("2Gi"), + }, + { + Image: "boo:V3", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("2Gi"), + }, + }, + }, + }, + "min-pod-memory": { + ObjectMeta: api.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Image: "boo:V1", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("0"), + }, + { + Image: "boo:V2", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("0"), + }, + { + Image: "boo:V3", + CPU: resource.MustParse("60m"), + Memory: resource.MustParse("0"), + }, + }, + }, + }, + } + + for i := range successCases { + err := PodLimitFunc(limitRange, "pods", &successCases[i]) + if err != nil { + t.Errorf("Unexpected error for valid pod: %v, %v", successCases[i].Name, err) + } + } + + for k, v := range errorCases { + err := PodLimitFunc(limitRange, "pods", &v) + if err == nil { + t.Errorf("Expected error for %s", k) + } + } +} diff --git a/plugin/pkg/admission/limitranger/interfaces.go b/plugin/pkg/admission/limitranger/interfaces.go new file mode 100644 index 00000000000..57d7c20bd8e --- /dev/null +++ b/plugin/pkg/admission/limitranger/interfaces.go @@ -0,0 +1,25 @@ +/* +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 limitranger + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// LimitFunc is a pluggable function to enforce limits on the object +type LimitFunc func(limitRange *api.LimitRange, kind string, obj runtime.Object) error