Add CrossNamespacePodAffinity quota scope and PodAffinityTerm.NamespaceSelector APIs, and CrossNamespacePodAffinity quota scope implementation.
This commit is contained in:
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user