Example #1
0
// 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())
			}
		}
	}
}