diff --git a/examples/limitrange/limitrange.json b/examples/limitrange/limitrange.json new file mode 100644 index 0000000000..daef2adaa7 --- /dev/null +++ b/examples/limitrange/limitrange.json @@ -0,0 +1,31 @@ +{ + "id": "limits", + "kind": "LimitRange", + "apiVersion": "v1beta1", + "spec": { + "limits": [ + { + "kind": "pods", + "max": { + "memory": "1073741824", + "cpu": "2", + }, + "min": { + "memory": "1048576", + "cpu": "0.25" + } + }, + { + "kind": "containers", + "max": { + "memory": "1073741824", + "cpu": "2", + }, + "min": { + "memory": "1048576", + "cpu": "0.25" + } + }, + ], + } +} diff --git a/plugin/pkg/admission/limitranger/admission.go b/plugin/pkg/admission/limitranger/admission.go new file mode 100644 index 0000000000..41b2e60711 --- /dev/null +++ b/plugin/pkg/admission/limitranger/admission.go @@ -0,0 +1,190 @@ +/* +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 + 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] + // enforce max + for k, v := range limit.Max { + observed := int64(0) + enforced := int64(0) + var err error + switch k { + case api.ResourceMemory: + enforced = v.Value() + switch limit.Kind { + case "pods": + observed = podMem + err = fmt.Errorf("Maximum memory usage per pod is %s", v.String()) + case "containers": + observed = maxContainerMem + err = fmt.Errorf("Maximum memory usage per container is %s", v.String()) + } + case api.ResourceCPU: + enforced = v.MilliValue() + switch limit.Kind { + case "pods": + observed = podCPU + err = fmt.Errorf("Maximum CPU usage per pod is %s, but requested %s", v.String(), resource.NewMilliQuantity(observed, resource.DecimalSI)) + case "containers": + observed = maxContainerCPU + err = fmt.Errorf("Maximum CPU usage per container is %s", v.String()) + } + } + if observed > enforced { + return apierrors.NewForbidden(kind, pod.Name, err) + } + } + for k, v := range limit.Min { + observed := int64(0) + enforced := int64(0) + var err error + switch k { + case api.ResourceMemory: + enforced = v.Value() + switch limit.Kind { + case "pods": + observed = podMem + err = fmt.Errorf("Minimum memory usage per pod is %s", v.String()) + case "containers": + observed = maxContainerMem + err = fmt.Errorf("Minimum memory usage per container is %s", v.String()) + } + case api.ResourceCPU: + enforced = v.MilliValue() + switch limit.Kind { + case "pods": + observed = podCPU + err = fmt.Errorf("Minimum CPU usage per pod is %s", v.String()) + case "containers": + observed = maxContainerCPU + err = fmt.Errorf("Minimum CPU usage per container is %s", v.String()) + } + } + 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 0000000000..9aed5a8591 --- /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{ + { + Kind: "pods", + 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"), + }, + }, + { + Kind: "containers", + 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 0000000000..57d7c20bd8 --- /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