Skip to content

Commit

Permalink
Add quota support for PVC with VolumeAttributesClass
Browse files Browse the repository at this point in the history
  • Loading branch information
carlory committed Apr 18, 2024
1 parent 88350db commit e6a8f9c
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 1 deletion.
7 changes: 7 additions & 0 deletions pkg/apis/core/helper/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ var standardResourceQuotaScopes = sets.New(
core.ResourceQuotaScopeBestEffort,
core.ResourceQuotaScopeNotBestEffort,
core.ResourceQuotaScopePriorityClass,
core.ResourceQuotaScopeVolumeAttributesClass,
)

// IsStandardResourceQuotaScope returns true if the scope is a standard value
Expand All @@ -139,6 +140,10 @@ var podComputeQuotaResources = sets.New(
core.ResourceRequestsMemory,
)

var pvcObjectCountQuotaResources = sets.New(
core.ResourcePersistentVolumeClaims,
)

// IsResourceQuotaScopeValidForResource returns true if the resource applies to the specified scope
func IsResourceQuotaScopeValidForResource(scope core.ResourceQuotaScope, resource core.ResourceName) bool {
switch scope {
Expand All @@ -147,6 +152,8 @@ func IsResourceQuotaScopeValidForResource(scope core.ResourceQuotaScope, resourc
return podObjectCountQuotaResources.Has(resource) || podComputeQuotaResources.Has(resource)
case core.ResourceQuotaScopeBestEffort:
return podObjectCountQuotaResources.Has(resource)
case core.ResourceQuotaScopeVolumeAttributesClass:
return pvcObjectCountQuotaResources.Has(resource)
default:
return true
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5728,6 +5728,9 @@ const (
ResourceQuotaScopePriorityClass ResourceQuotaScope = "PriorityClass"
// Match all pod objects that have cross-namespace pod (anti)affinity mentioned
ResourceQuotaScopeCrossNamespacePodAffinity ResourceQuotaScope = "CrossNamespacePodAffinity"

// Match all pvc objects that have volume attributes class mentioned.
ResourceQuotaScopeVolumeAttributesClass ResourceQuotaScope = "VolumeAttributesClass"
)

// ResourceQuotaSpec defines the desired hard limits to enforce for Quota
Expand Down
95 changes: 94 additions & 1 deletion pkg/quota/v1/evaluator/core/persistent_volume_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"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"
utilfeature "k8s.io/apiserver/pkg/util/feature"
storagehelpers "k8s.io/component-helpers/storage/volume"
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"
k8sfeatures "k8s.io/kubernetes/pkg/features"
"k8s.io/utils/ptr"
)

// the name used for object count quota
Expand Down Expand Up @@ -102,17 +106,50 @@ func (p *pvcEvaluator) Handles(a admission.Attributes) bool {

// Matches returns true if the evaluator matches the specified quota with the provided input item
func (p *pvcEvaluator) Matches(resourceQuota *corev1.ResourceQuota, item runtime.Object) (bool, error) {
if utilfeature.DefaultFeatureGate.Enabled(k8sfeatures.VolumeAttributesClass) {
return generic.Matches(resourceQuota, item, p.MatchingResources, pvcMatchesScopeFunc)
}
return generic.Matches(resourceQuota, item, p.MatchingResources, generic.MatchesNoScopeFunc)
}

// MatchingScopes takes the input specified list of scopes and input object. Returns the set of scopes resource matches.
func (p *pvcEvaluator) MatchingScopes(item runtime.Object, scopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) {
func (p *pvcEvaluator) MatchingScopes(item runtime.Object, scopeSelectors []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) {
if utilfeature.DefaultFeatureGate.Enabled(k8sfeatures.VolumeAttributesClass) {
matchedScopes := []corev1.ScopedResourceSelectorRequirement{}
for _, selector := range scopeSelectors {
match, err := pvcMatchesScopeFunc(selector, item)
if err != nil {
return []corev1.ScopedResourceSelectorRequirement{}, fmt.Errorf("error on matching scope %v: %v", selector, err)
}
if match {
matchedScopes = append(matchedScopes, selector)
}
}
return matchedScopes, nil
}
return []corev1.ScopedResourceSelectorRequirement{}, nil
}

// UncoveredQuotaScopes takes the input matched scopes which are limited by configuration and the matched quota scopes.
// It returns the scopes which are in limited scopes but don't have a corresponding covering quota scope
func (p *pvcEvaluator) UncoveredQuotaScopes(limitedScopes []corev1.ScopedResourceSelectorRequirement, matchedQuotaScopes []corev1.ScopedResourceSelectorRequirement) ([]corev1.ScopedResourceSelectorRequirement, error) {
if utilfeature.DefaultFeatureGate.Enabled(k8sfeatures.VolumeAttributesClass) {
uncoveredScopes := []corev1.ScopedResourceSelectorRequirement{}
for _, selector := range limitedScopes {
isCovered := false
for _, matchedScopeSelector := range matchedQuotaScopes {
if matchedScopeSelector.ScopeName == selector.ScopeName {
isCovered = true
break
}
}

if !isCovered {
uncoveredScopes = append(uncoveredScopes, selector)
}
}
return uncoveredScopes, nil
}
return []corev1.ScopedResourceSelectorRequirement{}, nil
}

Expand Down Expand Up @@ -235,3 +272,59 @@ func RequiresQuotaReplenish(pvc, oldPVC *corev1.PersistentVolumeClaim) bool {
}
return false
}

// pvcMatchesScopeFunc is a function that knows how to evaluate if a pvc matches a scope
func pvcMatchesScopeFunc(selector corev1.ScopedResourceSelectorRequirement, object runtime.Object) (bool, error) {
pvc, err := toExternalPersistentVolumeClaimOrError(object)
if err != nil {
return false, err
}

switch selector.ScopeName {
case corev1.ResourceQuotaScopeVolumeAttributesClass:
if selector.Operator == corev1.ScopeSelectorOpExists {
// This is just checking for existence of a volumeAttributesClass on the pvc,
// no need to take the overhead of selector parsing/evaluation.
if ptr.Deref(pvc.Spec.VolumeAttributesClassName, "") != "" {
return true, nil
}
if ptr.Deref(pvc.Status.CurrentVolumeAttributesClassName, "") != "" {
return true, nil
}
modifyStatus := pvc.Status.ModifyVolumeStatus
if modifyStatus != nil && modifyStatus.TargetVolumeAttributesClassName != "" {
return true, nil
}
return false, nil
}
return pvcMatchesSelector(pvc, selector)
}
return false, nil
}

func pvcMatchesSelector(pvc *corev1.PersistentVolumeClaim, selector corev1.ScopedResourceSelectorRequirement) (bool, error) {
labelSelector, err := helper.ScopedResourceSelectorRequirementsAsSelector(selector)
if err != nil {
return false, fmt.Errorf("failed to parse and convert selector: %v", err)
}

vacNames := sets.New[string]()
if ptr.Deref(pvc.Spec.VolumeAttributesClassName, "") != "" {
vacNames.Insert(*pvc.Spec.VolumeAttributesClassName)
}
if ptr.Deref(pvc.Status.CurrentVolumeAttributesClassName, "") != "" {
vacNames.Insert(*pvc.Status.CurrentVolumeAttributesClassName)
}
modifyStatus := pvc.Status.ModifyVolumeStatus
if modifyStatus != nil && modifyStatus.TargetVolumeAttributesClassName != "" {
vacNames.Insert(modifyStatus.TargetVolumeAttributesClassName)
}

for vacName := range vacNames {
m := labels.Set{string(corev1.ResourceQuotaScopeVolumeAttributesClass): vacName}
if labelSelector.Matches(m) {
return true, nil
}
}
return false, nil
}
3 changes: 3 additions & 0 deletions staging/src/k8s.io/api/core/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6841,6 +6841,9 @@ const (
ResourceQuotaScopePriorityClass ResourceQuotaScope = "PriorityClass"
// Match all pod objects that have cross-namespace pod (anti)affinity mentioned.
ResourceQuotaScopeCrossNamespacePodAffinity ResourceQuotaScope = "CrossNamespacePodAffinity"

// Match all pvc objects that have volume attributes class mentioned.
ResourceQuotaScopeVolumeAttributesClass ResourceQuotaScope = "VolumeAttributesClass"
)

// ResourceQuotaSpec defines the desired hard limits to enforce for Quota.
Expand Down
15 changes: 15 additions & 0 deletions test/e2e/feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,21 @@ var (
// TODO: document the feature (owning SIG, when to use this feature for a test)
ValidatingAdmissionPolicy = framework.WithFeature(framework.ValidFeatures.Add("ValidatingAdmissionPolicy"))

// owning-sig: sig-storage
// kep: https://kep.k8s.io/3751
// test-infra jobs:
// - pull-kubernetes-e2e-storage-kind-alpha-features (need manual trigger)
// - ci-kubernetes-e2e-storage-kind-alpha-features
//
// When this label is added to a test, it means that the cluster must be created
// with the feature-gate "VolumeAttributesClass=true" and the storage.k8s.io/v1alpha1
// API version must be enabled.
//
// Once the feature and API version are stable, this label should be removed and
// these tests will be run by default on any cluster. The test-infra job also should
// be updated to not focus on this feature anymore.
VolumeAttributesClass = framework.WithFeature(framework.ValidFeatures.Add("VolumeAttributesClass"))

// TODO: document the feature (owning SIG, when to use this feature for a test)
Volumes = framework.WithFeature(framework.ValidFeatures.Add("Volumes"))

Expand Down

0 comments on commit e6a8f9c

Please sign in to comment.