Add CrossNamespacePodAffinity quota scope and PodAffinityTerm.NamespaceSelector APIs, and CrossNamespacePodAffinity quota scope implementation.

This commit is contained in:
Abdullah Gharaibeh
2021-01-26 14:28:35 -05:00
parent 4f9317596c
commit 3c5f018f8e
85 changed files with 11392 additions and 3610 deletions

View File

@@ -26,16 +26,17 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/clock"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
quota "k8s.io/apiserver/pkg/quota/v1"
"k8s.io/apiserver/pkg/quota/v1/generic"
"k8s.io/apiserver/pkg/util/feature"
api "k8s.io/kubernetes/pkg/apis/core"
k8s_api_v1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/apis/core/v1/helper"
"k8s.io/kubernetes/pkg/apis/core/v1/helper/qos"
"k8s.io/kubernetes/pkg/features"
)
// the name used for object count quota
@@ -308,6 +309,8 @@ func podMatchesScopeFunc(selector corev1.ScopedResourceSelectorRequirement, obje
return !isBestEffort(pod), nil
case corev1.ResourceQuotaScopePriorityClass:
return podMatchesSelector(pod, selector)
case corev1.ResourceQuotaScopeCrossNamespacePodAffinity:
return usesCrossNamespacePodAffinity(pod), nil
}
return false, nil
}
@@ -381,6 +384,59 @@ func podMatchesSelector(pod *corev1.Pod, selector corev1.ScopedResourceSelectorR
return false, nil
}
func crossNamespacePodAffinityTerm(term *corev1.PodAffinityTerm) bool {
return len(term.Namespaces) != 0 || term.NamespaceSelector != nil
}
func crossNamespacePodAffinityTerms(terms []corev1.PodAffinityTerm) bool {
for _, t := range terms {
if crossNamespacePodAffinityTerm(&t) {
return true
}
}
return false
}
func crossNamespaceWeightedPodAffinityTerms(terms []corev1.WeightedPodAffinityTerm) bool {
for _, t := range terms {
if crossNamespacePodAffinityTerm(&t.PodAffinityTerm) {
return true
}
}
return false
}
func usesCrossNamespacePodAffinity(pod *corev1.Pod) bool {
if !feature.DefaultFeatureGate.Enabled(features.PodAffinityNamespaceSelector) {
return false
}
if pod == nil || pod.Spec.Affinity == nil {
return false
}
affinity := pod.Spec.Affinity.PodAffinity
if affinity != nil {
if crossNamespacePodAffinityTerms(affinity.RequiredDuringSchedulingIgnoredDuringExecution) {
return true
}
if crossNamespaceWeightedPodAffinityTerms(affinity.PreferredDuringSchedulingIgnoredDuringExecution) {
return true
}
}
antiAffinity := pod.Spec.Affinity.PodAntiAffinity
if antiAffinity != nil {
if crossNamespacePodAffinityTerms(antiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) {
return true
}
if crossNamespaceWeightedPodAffinityTerms(antiAffinity.PreferredDuringSchedulingIgnoredDuringExecution) {
return true
}
}
return false
}
// QuotaV1Pod returns true if the pod is eligible to track against a quota
// if it's not in a terminal state according to its phase.
func QuotaV1Pod(pod *corev1.Pod, clock clock.Clock) bool {

View File

@@ -20,6 +20,8 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -27,7 +29,10 @@ import (
"k8s.io/apimachinery/pkg/util/clock"
quota "k8s.io/apiserver/pkg/quota/v1"
"k8s.io/apiserver/pkg/quota/v1/generic"
"k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/util/node"
)
@@ -446,3 +451,238 @@ func TestPodEvaluatorUsage(t *testing.T) {
})
}
}
func TestPodEvaluatorMatchingScopes(t *testing.T) {
fakeClock := clock.NewFakeClock(time.Now())
evaluator := NewPodEvaluator(nil, fakeClock)
activeDeadlineSeconds := int64(30)
testCases := map[string]struct {
pod *api.Pod
selectors []corev1.ScopedResourceSelectorRequirement
wantSelectors []corev1.ScopedResourceSelectorRequirement
disableNamespaceSelector bool
}{
"EmptyPod": {
pod: &api.Pod{},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeNotTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
},
},
"PriorityClass": {
pod: &api.Pod{
Spec: api.PodSpec{
PriorityClassName: "class1",
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeNotTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopePriorityClass, Operator: corev1.ScopeSelectorOpIn, Values: []string{"class1"}},
},
},
"NotBestEffort": {
pod: &api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{{
Resources: api.ResourceRequirements{
Requests: api.ResourceList{
api.ResourceCPU: resource.MustParse("1"),
api.ResourceMemory: resource.MustParse("50M"),
api.ResourceName("example.com/dongle"): resource.MustParse("1"),
},
Limits: api.ResourceList{
api.ResourceCPU: resource.MustParse("2"),
api.ResourceMemory: resource.MustParse("100M"),
api.ResourceName("example.com/dongle"): resource.MustParse("1"),
},
},
}},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeNotTerminating},
{ScopeName: corev1.ResourceQuotaScopeNotBestEffort},
},
},
"Terminating": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
},
},
"OnlyTerminating": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
},
},
selectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
},
},
"CrossNamespaceRequiredAffinity": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAffinity: &api.PodAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{LabelSelector: &metav1.LabelSelector{}, Namespaces: []string{"ns1"}, NamespaceSelector: &metav1.LabelSelector{}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"CrossNamespaceRequiredAffinityWithSlice": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAffinity: &api.PodAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{LabelSelector: &metav1.LabelSelector{}, Namespaces: []string{"ns1"}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"CrossNamespacePreferredAffinity": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAffinity: &api.PodAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
{PodAffinityTerm: api.PodAffinityTerm{LabelSelector: &metav1.LabelSelector{}, Namespaces: []string{"ns2"}, NamespaceSelector: &metav1.LabelSelector{}}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"CrossNamespacePreferredAffinityWithSelector": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAffinity: &api.PodAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
{PodAffinityTerm: api.PodAffinityTerm{LabelSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{}}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"CrossNamespacePreferredAntiAffinity": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
{PodAffinityTerm: api.PodAffinityTerm{LabelSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{}}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"CrossNamespaceRequiredAntiAffinity": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{LabelSelector: &metav1.LabelSelector{}, Namespaces: []string{"ns3"}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
},
},
"NamespaceSelectorFeatureDisabled": {
pod: &api.Pod{
Spec: api.PodSpec{
ActiveDeadlineSeconds: &activeDeadlineSeconds,
Affinity: &api.Affinity{
PodAntiAffinity: &api.PodAntiAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
{LabelSelector: &metav1.LabelSelector{}, Namespaces: []string{"ns3"}},
},
},
},
},
},
wantSelectors: []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
},
disableNamespaceSelector: true,
},
}
for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.PodAffinityNamespaceSelector, !testCase.disableNamespaceSelector)()
if testCase.selectors == nil {
testCase.selectors = []corev1.ScopedResourceSelectorRequirement{
{ScopeName: corev1.ResourceQuotaScopeTerminating},
{ScopeName: corev1.ResourceQuotaScopeNotTerminating},
{ScopeName: corev1.ResourceQuotaScopeBestEffort},
{ScopeName: corev1.ResourceQuotaScopeNotBestEffort},
{ScopeName: corev1.ResourceQuotaScopePriorityClass, Operator: corev1.ScopeSelectorOpIn, Values: []string{"class1"}},
{ScopeName: corev1.ResourceQuotaScopeCrossNamespacePodAffinity},
}
}
gotSelectors, err := evaluator.MatchingScopes(testCase.pod, testCase.selectors)
if err != nil {
t.Error(err)
}
if diff := cmp.Diff(testCase.wantSelectors, gotSelectors); diff != "" {
t.Errorf("%v: unexpected diff (-want, +got):\n%s", testName, diff)
}
})
}
}