/* Copyright 2014 The Kubernetes Authors 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 ( "strconv" "testing" "k8s.io/kubernetes/pkg/admission" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/client/cache" "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) func getResourceList(cpu, memory string) api.ResourceList { res := api.ResourceList{} if cpu != "" { res[api.ResourceCPU] = resource.MustParse(cpu) } if memory != "" { res[api.ResourceMemory] = resource.MustParse(memory) } return res } func getResourceRequirements(requests, limits api.ResourceList) api.ResourceRequirements { res := api.ResourceRequirements{} res.Requests = requests res.Limits = limits return res } // createLimitRange creates a limit range with the specified data func createLimitRange(limitType api.LimitType, min, max, defaultLimit, defaultRequest, maxLimitRequestRatio api.ResourceList) api.LimitRange { return api.LimitRange{ ObjectMeta: api.ObjectMeta{ Name: "abc", Namespace: "test", }, Spec: api.LimitRangeSpec{ Limits: []api.LimitRangeItem{ { Type: limitType, Min: min, Max: max, Default: defaultLimit, DefaultRequest: defaultRequest, MaxLimitRequestRatio: maxLimitRequestRatio, }, }, }, } } func validLimitRange() api.LimitRange { return api.LimitRange{ ObjectMeta: api.ObjectMeta{ Name: "abc", Namespace: "test", }, Spec: api.LimitRangeSpec{ Limits: []api.LimitRangeItem{ { Type: api.LimitTypePod, Max: getResourceList("200m", "4Gi"), Min: getResourceList("50m", "2Mi"), }, { Type: api.LimitTypeContainer, Max: getResourceList("100m", "2Gi"), Min: getResourceList("25m", "1Mi"), Default: getResourceList("75m", "10Mi"), DefaultRequest: getResourceList("50m", "5Mi"), }, }, }, } } func validLimitRangeNoDefaults() api.LimitRange { return api.LimitRange{ ObjectMeta: api.ObjectMeta{ Name: "abc", Namespace: "test", }, Spec: api.LimitRangeSpec{ Limits: []api.LimitRangeItem{ { Type: api.LimitTypePod, Max: getResourceList("200m", "4Gi"), Min: getResourceList("50m", "2Mi"), }, { Type: api.LimitTypeContainer, Max: getResourceList("100m", "2Gi"), Min: getResourceList("25m", "1Mi"), }, }, }, } } func validPod(name string, numContainers int, resources api.ResourceRequirements) api.Pod { pod := api.Pod{ ObjectMeta: api.ObjectMeta{Name: name, Namespace: "test"}, Spec: api.PodSpec{}, } pod.Spec.Containers = make([]api.Container, 0, numContainers) for i := 0; i < numContainers; i++ { pod.Spec.Containers = append(pod.Spec.Containers, api.Container{ Image: "foo:V" + strconv.Itoa(i), Resources: resources, }) } return pod } func TestDefaultContainerResourceRequirements(t *testing.T) { limitRange := validLimitRange() expected := api.ResourceRequirements{ Requests: getResourceList("50m", "5Mi"), Limits: getResourceList("75m", "10Mi"), } actual := defaultContainerResourceRequirements(&limitRange) if !api.Semantic.DeepEqual(expected, actual) { t.Errorf("actual.Limits != expected.Limits; %v != %v", actual.Limits, expected.Limits) t.Errorf("actual.Requests != expected.Requests; %v != %v", actual.Requests, expected.Requests) t.Errorf("expected != actual; %v != %v", expected, actual) } } func TestMergePodResourceRequirements(t *testing.T) { limitRange := validLimitRange() // pod with no resources enumerated should get each resource from default request expected := getResourceRequirements(getResourceList("", ""), getResourceList("", "")) pod := validPod("empty-resources", 1, expected) defaultRequirements := defaultContainerResourceRequirements(&limitRange) mergePodResourceRequirements(&pod, &defaultRequirements) for i := range pod.Spec.Containers { actual := pod.Spec.Containers[i].Resources if !api.Semantic.DeepEqual(expected, actual) { t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual) } } // pod with some resources enumerated should only merge empty input := getResourceRequirements(getResourceList("", "512Mi"), getResourceList("", "")) pod = validPod("limit-memory", 1, input) expected = api.ResourceRequirements{ Requests: api.ResourceList{ api.ResourceCPU: defaultRequirements.Requests[api.ResourceCPU], api.ResourceMemory: resource.MustParse("512Mi"), }, Limits: defaultRequirements.Limits, } mergePodResourceRequirements(&pod, &defaultRequirements) for i := range pod.Spec.Containers { actual := pod.Spec.Containers[i].Resources if !api.Semantic.DeepEqual(expected, actual) { t.Errorf("pod %v, expected != actual; %v != %v", pod.Name, expected, actual) } } } func TestPodLimitFunc(t *testing.T) { type testCase struct { pod api.Pod limitRange api.LimitRange } successCases := []testCase{ { pod: validPod("ctr-min-cpu-request", 1, getResourceRequirements(getResourceList("100m", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-cpu-request-limit", 1, getResourceRequirements(getResourceList("100m", ""), getResourceList("200m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-memory-request", 1, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-memory-request-limit", 1, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", "100Mi"))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-request-limit", 1, getResourceRequirements(getResourceList("500m", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-mem-request-limit", 1, getResourceRequirements(getResourceList("", "250Mi"), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-ratio", 1, getResourceRequirements(getResourceList("500m", ""), getResourceList("750m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, getResourceList("1.5", "")), }, { pod: validPod("ctr-max-mem-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-cpu-request", 2, getResourceRequirements(getResourceList("75m", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-cpu-request-limit", 2, getResourceRequirements(getResourceList("75m", ""), getResourceList("200m", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-memory-request", 2, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-memory-request-limit", 2, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", "100Mi"))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-cpu-request-limit", 2, getResourceRequirements(getResourceList("500m", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-cpu-limit", 2, getResourceRequirements(getResourceList("", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-request-limit", 2, getResourceRequirements(getResourceList("", "250Mi"), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-limit", 2, getResourceRequirements(getResourceList("", ""), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-ratio", 3, getResourceRequirements(getResourceList("", "300Mi"), getResourceList("", "450Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "2Gi"), api.ResourceList{}, api.ResourceList{}, getResourceList("", "1.5")), }, } for i := range successCases { test := successCases[i] err := PodLimitFunc(&test.limitRange, &test.pod) if err != nil { t.Errorf("Unexpected error for pod: %s, %v", test.pod.Name, err) } } errorCases := []testCase{ { pod: validPod("ctr-min-cpu-request", 1, getResourceRequirements(getResourceList("40m", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-cpu-request-limit", 1, getResourceRequirements(getResourceList("40m", ""), getResourceList("200m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-cpu-no-request-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("50m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-memory-request", 1, getResourceRequirements(getResourceList("", "40Mi"), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-memory-request-limit", 1, getResourceRequirements(getResourceList("", "40Mi"), getResourceList("", "100Mi"))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-min-memory-no-request-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, getResourceList("", "50Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-request-limit", 1, getResourceRequirements(getResourceList("500m", ""), getResourceList("2500m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("2500m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-no-request-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-cpu-ratio", 1, getResourceRequirements(getResourceList("1250m", ""), getResourceList("2500m", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, getResourceList("1", "")), }, { pod: validPod("ctr-max-mem-request-limit", 1, getResourceRequirements(getResourceList("", "250Mi"), getResourceList("", "2Gi"))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-mem-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", "2Gi"))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("ctr-max-mem-no-request-limit", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypeContainer, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-cpu-request", 1, getResourceRequirements(getResourceList("75m", ""), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-cpu-request-limit", 1, getResourceRequirements(getResourceList("75m", ""), getResourceList("200m", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("100m", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-memory-request", 1, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", ""))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-min-memory-request-limit", 1, getResourceRequirements(getResourceList("", "60Mi"), getResourceList("", "100Mi"))), limitRange: createLimitRange(api.LimitTypePod, getResourceList("", "100Mi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-cpu-request-limit", 3, getResourceRequirements(getResourceList("500m", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-cpu-limit", 3, getResourceRequirements(getResourceList("", ""), getResourceList("1", ""))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("2", ""), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-request-limit", 3, getResourceRequirements(getResourceList("", "250Mi"), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-limit", 3, getResourceRequirements(getResourceList("", ""), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "1Gi"), api.ResourceList{}, api.ResourceList{}, api.ResourceList{}), }, { pod: validPod("pod-max-mem-ratio", 3, getResourceRequirements(getResourceList("", "250Mi"), getResourceList("", "500Mi"))), limitRange: createLimitRange(api.LimitTypePod, api.ResourceList{}, getResourceList("", "2Gi"), api.ResourceList{}, api.ResourceList{}, getResourceList("", "1.5")), }, } for i := range errorCases { test := errorCases[i] err := PodLimitFunc(&test.limitRange, &test.pod) if err == nil { t.Errorf("Expected error for pod: %s", test.pod.Name) } } } func TestPodLimitFuncApplyDefault(t *testing.T) { limitRange := validLimitRange() testPod := validPod("foo", 1, getResourceRequirements(api.ResourceList{}, api.ResourceList{})) err := PodLimitFunc(&limitRange, &testPod) if err != nil { t.Errorf("Unexpected error for valid pod: %v, %v", testPod.Name, err) } for i := range testPod.Spec.Containers { container := testPod.Spec.Containers[i] limitMemory := container.Resources.Limits.Memory().String() limitCpu := container.Resources.Limits.Cpu().String() requestMemory := container.Resources.Requests.Memory().String() requestCpu := container.Resources.Requests.Cpu().String() if limitMemory != "10Mi" { t.Errorf("Unexpected memory value %s", limitMemory) } if limitCpu != "75m" { t.Errorf("Unexpected cpu value %s", limitCpu) } if requestMemory != "5Mi" { t.Errorf("Unexpected memory value %s", requestMemory) } if requestCpu != "50m" { t.Errorf("Unexpected cpu value %s", requestCpu) } } } func TestLimitRangerIgnoresSubresource(t *testing.T) { client := testclient.NewSimpleFake() indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc}) handler := &limitRanger{ Handler: admission.NewHandler(admission.Create, admission.Update), client: client, limitFunc: Limit, indexer: indexer, } limitRange := validLimitRangeNoDefaults() testPod := validPod("testPod", 1, api.ResourceRequirements{}) indexer.Add(&limitRange) err := handler.Admit(admission.NewAttributesRecord(&testPod, "Pod", limitRange.Namespace, "testPod", "pods", "", admission.Update, nil)) if err == nil { t.Errorf("Expected an error since the pod did not specify resource limits in its update call") } err = handler.Admit(admission.NewAttributesRecord(&testPod, "Pod", limitRange.Namespace, "testPod", "pods", "status", admission.Update, nil)) if err != nil { t.Errorf("Should have ignored calls to any subresource of pod %v", err) } }