// IncrementUsage updates the supplied ResourceQuotaStatus object based on the incoming operation // Return true if the usage must be recorded prior to admitting the new resource // Return an error if the operation should not pass admission control func IncrementUsage(a admission.Attributes, status *api.ResourceQuotaStatus, client client.Interface) (bool, error) { var errs []error dirty := true set := map[api.ResourceName]bool{} for k := range status.Hard { set[k] = true } obj := a.GetObject() // handle max counts for each kind of resource (pods, services, replicationControllers, etc.) if a.GetOperation() == admission.Create { resourceName := resourceToResourceName[a.GetResource()] hard, hardFound := status.Hard[resourceName] if hardFound { used, usedFound := status.Used[resourceName] if !usedFound { return false, fmt.Errorf("quota usage stats are not yet known, unable to admit resource until an accurate count is completed.") } if used.Value() >= hard.Value() { errs = append(errs, fmt.Errorf("limited to %s %s", hard.String(), resourceName)) dirty = false } else { status.Used[resourceName] = *resource.NewQuantity(used.Value()+int64(1), resource.DecimalSI) } } } if a.GetResource() == "pods" { for _, resourceName := range []api.ResourceName{api.ResourceMemory, api.ResourceCPU} { // ignore tracking the resource if it's not in the quota document if !set[resourceName] { continue } hard, hardFound := status.Hard[resourceName] if !hardFound { continue } // if we do not yet know how much of the current resource is used, we cannot accept any request used, usedFound := status.Used[resourceName] if !usedFound { return false, fmt.Errorf("unable to admit pod until quota usage stats are calculated.") } // the amount of resource being requested, or an error if it does not make a request that is tracked pod := obj.(*api.Pod) delta, err := resourcequotacontroller.PodRequests(pod, resourceName) if err != nil { return false, fmt.Errorf("must make a non-zero request for %s since it is tracked by quota.", resourceName) } // if this operation is an update, we need to find the delta usage from the previous state if a.GetOperation() == admission.Update { oldPod, err := client.Pods(a.GetNamespace()).Get(pod.Name) if err != nil { return false, err } // if the previous version of the resource made a resource request, we need to subtract the old request // from the current to get the actual resource request delta. if the previous version of the pod // made no request on the resource, then we get an err value. we ignore the err value, and delta // will just be equal to the total resource request on the pod since there is nothing to subtract. oldRequest, err := resourcequotacontroller.PodRequests(oldPod, resourceName) if err == nil { err = delta.Sub(*oldRequest) if err != nil { return false, err } } } newUsage := used.Copy() newUsage.Add(*delta) // make the most precise comparison possible newUsageValue := newUsage.Value() hardUsageValue := hard.Value() if newUsageValue <= resource.MaxMilliValue && hardUsageValue <= resource.MaxMilliValue { newUsageValue = newUsage.MilliValue() hardUsageValue = hard.MilliValue() } if newUsageValue > hardUsageValue { errs = append(errs, fmt.Errorf("unable to admit pod without exceeding quota for resource %s: limited to %s but require %s to succeed.", resourceName, hard.String(), newUsage.String())) dirty = false } else { status.Used[resourceName] = *newUsage } } } return dirty, utilerrors.NewAggregate(errs) }
func TestIncrementUsagePodResources(t *testing.T) { type testCase struct { testName string existing *api.Pod input *api.Pod resourceName api.ResourceName hard resource.Quantity expectedUsage resource.Quantity expectedError bool } testCases := []testCase{ { testName: "memory-allowed", existing: validPod("a", 1, getResourceRequirements(getResourceList("", "100Mi"), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("", "100Mi"), getResourceList("", ""))), resourceName: api.ResourceMemory, hard: resource.MustParse("500Mi"), expectedUsage: resource.MustParse("200Mi"), expectedError: false, }, { testName: "memory-not-allowed", existing: validPod("a", 1, getResourceRequirements(getResourceList("", "100Mi"), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("", "450Mi"), getResourceList("", ""))), resourceName: api.ResourceMemory, hard: resource.MustParse("500Mi"), expectedError: true, }, { testName: "memory-not-allowed-with-different-format", existing: validPod("a", 1, getResourceRequirements(getResourceList("", "100M"), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("", "450Mi"), getResourceList("", ""))), resourceName: api.ResourceMemory, hard: resource.MustParse("500Mi"), expectedError: true, }, { testName: "memory-no-request", existing: validPod("a", 1, getResourceRequirements(getResourceList("", "100Mi"), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), resourceName: api.ResourceMemory, hard: resource.MustParse("500Mi"), expectedError: true, }, { testName: "cpu-allowed", existing: validPod("a", 1, getResourceRequirements(getResourceList("1", ""), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("1", ""), getResourceList("", ""))), resourceName: api.ResourceCPU, hard: resource.MustParse("2"), expectedUsage: resource.MustParse("2"), expectedError: false, }, { testName: "cpu-not-allowed", existing: validPod("a", 1, getResourceRequirements(getResourceList("1", ""), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("600m", ""), getResourceList("", ""))), resourceName: api.ResourceCPU, hard: resource.MustParse("1500m"), expectedError: true, }, { testName: "cpu-no-request", existing: validPod("a", 1, getResourceRequirements(getResourceList("1", ""), getResourceList("", ""))), input: validPod("b", 1, getResourceRequirements(getResourceList("", ""), getResourceList("", ""))), resourceName: api.ResourceCPU, hard: resource.MustParse("1500m"), expectedError: true, }, } for _, item := range testCases { podList := &api.PodList{Items: []api.Pod{*item.existing}} client := fake.NewSimpleClientset(podList) status := &api.ResourceQuotaStatus{ Hard: api.ResourceList{}, Used: api.ResourceList{}, } used, err := resourcequotacontroller.PodRequests(item.existing, item.resourceName) if err != nil { t.Errorf("Test %s, unexpected error %v", item.testName, err) } status.Hard[item.resourceName] = item.hard status.Used[item.resourceName] = *used dirty, err := IncrementUsage(admission.NewAttributesRecord(item.input, api.Kind("Pod"), item.input.Namespace, item.input.Name, api.Resource("pods"), "", admission.Create, nil), status, client) if err == nil && item.expectedError { t.Errorf("Test %s, expected error", item.testName) } if err != nil && !item.expectedError { t.Errorf("Test %s, unexpected error", err) } if !item.expectedError { if !dirty { t.Errorf("Test %s, expected the quota to be dirty", item.testName) } quantity := status.Used[item.resourceName] if quantity.String() != item.expectedUsage.String() { t.Errorf("Test %s, expected usage %s, actual usage %s", item.testName, item.expectedUsage.String(), quantity.String()) } } } }