mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 07:20:13 +00:00
Merge pull request #36765 from derekwaynecarr/quota-precious-resources
Automatic merge from submit-queue (batch tested with PRs 41421, 41440, 36765, 41722) ResourceQuota ability to support default limited resources Add support for the ability to configure the quota system to identify specific resources that are limited by default. A limited resource means its consumption is denied absent a covering quota. This is in contrast to the current behavior where consumption is unlimited absent a covering quota. Intended use case is to allow operators to restrict consumption of high-cost resources by default. Example configuration: **admission-control-config-file.yaml** ``` apiVersion: apiserver.k8s.io/v1alpha1 kind: AdmissionConfiguration plugins: - name: "ResourceQuota" configuration: apiVersion: resourcequota.admission.k8s.io/v1alpha1 kind: Configuration limitedResources: - resource: pods matchContains: - pods - requests.cpu - resource: persistentvolumeclaims matchContains: - .storageclass.storage.k8s.io/requests.storage ``` In the above configuration, if a namespace lacked a quota for any of the following: * cpu * any pvc associated with particular storage class The attempt to consume the resource is denied with a message stating the user has insufficient quota for the matching resources. ``` $ kubectl create -f pvc-gold.yaml Error from server: error when creating "pvc-gold.yaml": insufficient quota to consume: gold.storageclass.storage.k8s.io/requests.storage $ kubectl create quota quota --hard=gold.storageclass.storage.k8s.io/requests.storage=10Gi $ kubectl create -f pvc-gold.yaml ... created ```
This commit is contained in:
commit
506950ada0
@ -262,6 +262,8 @@ plugin/pkg/admission/gc
|
||||
plugin/pkg/admission/imagepolicy
|
||||
plugin/pkg/admission/namespace/autoprovision
|
||||
plugin/pkg/admission/namespace/exists
|
||||
plugin/pkg/admission/resourcequota/apis/resourcequota/install
|
||||
plugin/pkg/admission/resourcequota/apis/resourcequota/validation
|
||||
plugin/pkg/admission/securitycontext/scdeny
|
||||
plugin/pkg/auth
|
||||
plugin/pkg/auth/authorizer
|
||||
|
@ -76,6 +76,7 @@ AUTH_ARGS=${AUTH_ARGS:-""}
|
||||
KUBE_CACHE_MUTATION_DETECTOR="${KUBE_CACHE_MUTATION_DETECTOR:-true}"
|
||||
export KUBE_CACHE_MUTATION_DETECTOR
|
||||
|
||||
ADMISSION_CONTROL_CONFIG_FILE=${ADMISSION_CONTROL_CONFIG_FILE:-""}
|
||||
|
||||
# START_MODE can be 'all', 'kubeletonly', or 'nokubelet'
|
||||
START_MODE=${START_MODE:-"all"}
|
||||
@ -434,6 +435,7 @@ function start_apiserver {
|
||||
--service-account-key-file="${SERVICE_ACCOUNT_KEY}" \
|
||||
--service-account-lookup="${SERVICE_ACCOUNT_LOOKUP}" \
|
||||
--admission-control="${ADMISSION_CONTROL}" \
|
||||
--admission-control-config-file="${ADMISSION_CONTROL_CONFIG_FILE}" \
|
||||
--bind-address="${API_BIND_ADDR}" \
|
||||
--secure-port="${API_SECURE_PORT}" \
|
||||
--tls-cert-file="${CERT_DIR}/serving-kube-apiserver.crt" \
|
||||
|
@ -12,6 +12,7 @@ go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"config.go",
|
||||
"controller.go",
|
||||
"doc.go",
|
||||
"resource_access.go",
|
||||
@ -24,11 +25,19 @@ go_library(
|
||||
"//pkg/quota:go_default_library",
|
||||
"//pkg/quota/install:go_default_library",
|
||||
"//pkg/util/workqueue/prometheus:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/install:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/validation:go_default_library",
|
||||
"//vendor:github.com/golang/glog",
|
||||
"//vendor:github.com/hashicorp/golang-lru",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/meta",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apimachinery/announced",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apimachinery/registered",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/serializer",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/sets",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/wait",
|
||||
@ -51,6 +60,7 @@ go_test(
|
||||
"//pkg/quota:go_default_library",
|
||||
"//pkg/quota/generic:go_default_library",
|
||||
"//pkg/quota/install:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
|
||||
"//vendor:github.com/hashicorp/golang-lru",
|
||||
"//vendor:k8s.io/apimachinery/pkg/api/resource",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
@ -71,6 +81,9 @@ filegroup(
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
@ -27,22 +27,35 @@ import (
|
||||
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
||||
"k8s.io/kubernetes/pkg/quota"
|
||||
"k8s.io/kubernetes/pkg/quota/install"
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/validation"
|
||||
)
|
||||
|
||||
func init() {
|
||||
admission.RegisterPlugin("ResourceQuota",
|
||||
func(config io.Reader) (admission.Interface, error) {
|
||||
// load the configuration provided (if any)
|
||||
configuration, err := LoadConfiguration(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// validate the configuration (if any)
|
||||
if configuration != nil {
|
||||
if errs := validation.ValidateConfiguration(configuration); len(errs) != 0 {
|
||||
return nil, errs.ToAggregate()
|
||||
}
|
||||
}
|
||||
// NOTE: we do not provide informers to the registry because admission level decisions
|
||||
// does not require us to open watches for all items tracked by quota.
|
||||
registry := install.NewRegistry(nil, nil)
|
||||
return NewResourceQuota(registry, 5, make(chan struct{}))
|
||||
return NewResourceQuota(registry, configuration, 5, make(chan struct{}))
|
||||
})
|
||||
}
|
||||
|
||||
// quotaAdmission implements an admission controller that can enforce quota constraints
|
||||
type quotaAdmission struct {
|
||||
*admission.Handler
|
||||
|
||||
config *resourcequotaapi.Configuration
|
||||
stopCh <-chan struct{}
|
||||
registry quota.Registry
|
||||
numEvaluators int
|
||||
@ -59,12 +72,13 @@ type liveLookupEntry struct {
|
||||
// NewResourceQuota configures an admission controller that can enforce quota constraints
|
||||
// using the provided registry. The registry must have the capability to handle group/kinds that
|
||||
// are persisted by the server this admission controller is intercepting
|
||||
func NewResourceQuota(registry quota.Registry, numEvaluators int, stopCh <-chan struct{}) (admission.Interface, error) {
|
||||
func NewResourceQuota(registry quota.Registry, config *resourcequotaapi.Configuration, numEvaluators int, stopCh <-chan struct{}) (admission.Interface, error) {
|
||||
return "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
stopCh: stopCh,
|
||||
registry: registry,
|
||||
numEvaluators: numEvaluators,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -77,7 +91,7 @@ func (a *quotaAdmission) SetInternalClientSet(client internalclientset.Interface
|
||||
}
|
||||
go quotaAccessor.Run(a.stopCh)
|
||||
|
||||
a.evaluator = NewQuotaEvaluator(quotaAccessor, a.registry, nil, a.numEvaluators, a.stopCh)
|
||||
a.evaluator = NewQuotaEvaluator(quotaAccessor, a.registry, nil, a.config, a.numEvaluators, a.stopCh)
|
||||
}
|
||||
|
||||
// Validate ensures an authorizer is set.
|
||||
@ -89,11 +103,10 @@ func (a *quotaAdmission) Validate() error {
|
||||
}
|
||||
|
||||
// Admit makes admission decisions while enforcing quota
|
||||
func (q *quotaAdmission) Admit(a admission.Attributes) (err error) {
|
||||
func (a *quotaAdmission) Admit(attr admission.Attributes) (err error) {
|
||||
// ignore all operations that correspond to sub-resource actions
|
||||
if a.GetSubresource() != "" {
|
||||
if attr.GetSubresource() != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return q.evaluator.Evaluate(a)
|
||||
return a.evaluator.Evaluate(attr)
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/quota"
|
||||
"k8s.io/kubernetes/pkg/quota/generic"
|
||||
"k8s.io/kubernetes/pkg/quota/install"
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
)
|
||||
|
||||
func getResourceList(cpu, memory string) api.ResourceList {
|
||||
@ -130,7 +131,8 @@ func TestAdmissionIgnoresDelete(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -164,7 +166,8 @@ func TestAdmissionIgnoresSubresources(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -207,7 +210,8 @@ func TestAdmitBelowQuotaLimit(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -289,7 +293,8 @@ func TestAdmitHandlesOldObjects(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -385,7 +390,8 @@ func TestAdmitHandlesCreatingUpdates(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -478,7 +484,8 @@ func TestAdmitExceedQuotaLimit(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -521,7 +528,9 @@ func TestAdmitEnforceQuotaConstraints(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -574,7 +583,8 @@ func TestAdmitPodInNamespaceWithoutQuota(t *testing.T) {
|
||||
quotaAccessor.indexer = indexer
|
||||
quotaAccessor.liveLookupCache = liveLookupCache
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -639,7 +649,8 @@ func TestAdmitBelowTerminatingQuotaLimit(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -743,7 +754,8 @@ func TestAdmitBelowBestEffortQuotaLimit(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -834,7 +846,8 @@ func TestAdmitBestEffortQuotaLimitIgnoresBurstable(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -929,7 +942,8 @@ func TestAdmissionSetsMissingNamespace(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
evaluator.(*quotaEvaluator).registry = registry
|
||||
|
||||
handler := "aAdmission{
|
||||
@ -974,7 +988,8 @@ func TestAdmitRejectsNegativeUsage(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -1019,7 +1034,8 @@ func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) {
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, 5, stopCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
@ -1034,3 +1050,219 @@ func TestAdmitWhenUnrelatedResourceExceedsQuota(t *testing.T) {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmitLimitedResourceNoQuota verifies if a limited resource is configured with no quota, it cannot be consumed.
|
||||
func TestAdmitLimitedResourceNoQuota(t *testing.T) {
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"cpu"},
|
||||
},
|
||||
},
|
||||
}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
evaluator: evaluator,
|
||||
}
|
||||
newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
|
||||
err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error for consuming a limited resource without quota.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources shows it ignores non matching resources in config.
|
||||
func TestAdmitLimitedResourceNoQuotaIgnoresNonMatchingResources(t *testing.T) {
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "services",
|
||||
MatchContains: []string{"services"},
|
||||
},
|
||||
},
|
||||
}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
evaluator: evaluator,
|
||||
}
|
||||
newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
|
||||
err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmitLimitedResourceWithQuota verifies if a limited resource is configured with quota, it can be consumed.
|
||||
func TestAdmitLimitedResourceWithQuota(t *testing.T) {
|
||||
resourceQuota := &api.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
|
||||
Status: api.ResourceQuotaStatus{
|
||||
Hard: api.ResourceList{
|
||||
api.ResourceRequestsCPU: resource.MustParse("10"),
|
||||
},
|
||||
Used: api.ResourceList{
|
||||
api.ResourceRequestsCPU: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(resourceQuota)
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
|
||||
},
|
||||
},
|
||||
}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
evaluator: evaluator,
|
||||
}
|
||||
indexer.Add(resourceQuota)
|
||||
newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
|
||||
err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmitLimitedResourceWithMultipleQuota verifies if a limited resource is configured with quota, it can be consumed if one matches.
|
||||
func TestAdmitLimitedResourceWithMultipleQuota(t *testing.T) {
|
||||
resourceQuota1 := &api.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "quota1", Namespace: "test", ResourceVersion: "124"},
|
||||
Status: api.ResourceQuotaStatus{
|
||||
Hard: api.ResourceList{
|
||||
api.ResourceRequestsCPU: resource.MustParse("10"),
|
||||
},
|
||||
Used: api.ResourceList{
|
||||
api.ResourceRequestsCPU: resource.MustParse("1"),
|
||||
},
|
||||
},
|
||||
}
|
||||
resourceQuota2 := &api.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "quota2", Namespace: "test", ResourceVersion: "124"},
|
||||
Status: api.ResourceQuotaStatus{
|
||||
Hard: api.ResourceList{
|
||||
api.ResourceMemory: resource.MustParse("10Gi"),
|
||||
},
|
||||
Used: api.ResourceList{
|
||||
api.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
},
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(resourceQuota1, resourceQuota2)
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"requests.cpu"}, // match on "requests.cpu" only
|
||||
},
|
||||
},
|
||||
}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
evaluator: evaluator,
|
||||
}
|
||||
indexer.Add(resourceQuota1)
|
||||
indexer.Add(resourceQuota2)
|
||||
newPod := validPod("allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
|
||||
err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmitLimitedResourceWithQuotaThatDoesNotCover verifies if a limited resource is configured the quota must cover the resource.
|
||||
func TestAdmitLimitedResourceWithQuotaThatDoesNotCover(t *testing.T) {
|
||||
resourceQuota := &api.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "quota", Namespace: "test", ResourceVersion: "124"},
|
||||
Status: api.ResourceQuotaStatus{
|
||||
Hard: api.ResourceList{
|
||||
api.ResourceMemory: resource.MustParse("10Gi"),
|
||||
},
|
||||
Used: api.ResourceList{
|
||||
api.ResourceMemory: resource.MustParse("1Gi"),
|
||||
},
|
||||
},
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(resourceQuota)
|
||||
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc})
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
|
||||
quotaAccessor, _ := newQuotaAccessor(kubeClient)
|
||||
quotaAccessor.indexer = indexer
|
||||
go quotaAccessor.Run(stopCh)
|
||||
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
// disable consumption of cpu unless there is a covering quota.
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"cpu"}, // match on "cpu" only
|
||||
},
|
||||
},
|
||||
}
|
||||
evaluator := NewQuotaEvaluator(quotaAccessor, install.NewRegistry(nil, nil), nil, config, 5, stopCh)
|
||||
|
||||
handler := "aAdmission{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
evaluator: evaluator,
|
||||
}
|
||||
indexer.Add(resourceQuota)
|
||||
newPod := validPod("not-allowed-pod", 1, getResourceRequirements(getResourceList("3", "2Gi"), getResourceList("", "")))
|
||||
err := handler.Admit(admission.NewAttributesRecord(newPod, nil, api.Kind("Pod").WithVersion("version"), newPod.Namespace, newPod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, nil))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected an error since the quota did not cover cpu")
|
||||
}
|
||||
}
|
||||
|
43
plugin/pkg/admission/resourcequota/apis/resourcequota/BUILD
Normal file
43
plugin/pkg/admission/resourcequota/apis/resourcequota/BUILD
Normal file
@ -0,0 +1,43 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"doc.go",
|
||||
"register.go",
|
||||
"types.go",
|
||||
"zz_generated.deepcopy.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/conversion",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/install:all-srcs",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1:all-srcs",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/validation:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
7
plugin/pkg/admission/resourcequota/apis/resourcequota/OWNERS
Executable file
7
plugin/pkg/admission/resourcequota/apis/resourcequota/OWNERS
Executable file
@ -0,0 +1,7 @@
|
||||
reviewers:
|
||||
- deads2k
|
||||
- derekwaynecarr
|
||||
approvers:
|
||||
- deads2k
|
||||
- derekwaynecarr
|
||||
- smarterclayton
|
19
plugin/pkg/admission/resourcequota/apis/resourcequota/doc.go
Normal file
19
plugin/pkg/admission/resourcequota/apis/resourcequota/doc.go
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
|
||||
package resourcequota // import "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
@ -0,0 +1,34 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["install.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1:go_default_library",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apimachinery/announced",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apimachinery/registered",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package install installs the experimental API group, making it available as
|
||||
// an option to all of the API encoding/decoding machinery.
|
||||
package install
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
resourcequotav1alpha1 "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1"
|
||||
)
|
||||
|
||||
// Install registers the API group and adds types to a scheme
|
||||
func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *registered.APIRegistrationManager, scheme *runtime.Scheme) {
|
||||
if err := announced.NewGroupMetaFactory(
|
||||
&announced.GroupMetaFactoryArgs{
|
||||
GroupName: resourcequotaapi.GroupName,
|
||||
VersionPreferenceOrder: []string{resourcequotav1alpha1.SchemeGroupVersion.Version},
|
||||
ImportPrefix: "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota",
|
||||
AddInternalObjectsToScheme: resourcequotaapi.AddToScheme,
|
||||
},
|
||||
announced.VersionToSchemeFunc{
|
||||
resourcequotav1alpha1.SchemeGroupVersion.Version: resourcequotav1alpha1.AddToScheme,
|
||||
},
|
||||
).Announce(groupFactoryRegistry).RegisterAndEnable(registry, scheme); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resourcequota
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// GroupName is the group name use in this package
|
||||
const GroupName = "resourcequota.admission.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
// Kind takes an unqualified kind and returns a Group qualified GroupKind
|
||||
func Kind(kind string) schema.GroupKind {
|
||||
return SchemeGroupVersion.WithKind(kind).GroupKind()
|
||||
}
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group qualified GroupResource
|
||||
func Resource(resource string) schema.GroupResource {
|
||||
return SchemeGroupVersion.WithResource(resource).GroupResource()
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
// TODO this will get cleaned up with the scheme types are fixed
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Configuration{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *Configuration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resourcequota
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// Configuration provides configuration for the ResourceQuota admission controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta
|
||||
|
||||
// LimitedResources whose consumption is limited by default.
|
||||
// +optional
|
||||
LimitedResources []LimitedResource
|
||||
}
|
||||
|
||||
// LimitedResource matches a resource whose consumption is limited by default.
|
||||
// To consume the resource, there must exist an associated quota that limits
|
||||
// its consumption.
|
||||
type LimitedResource struct {
|
||||
|
||||
// APIGroup is the name of the APIGroup that contains the limited resource.
|
||||
// +optional
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
|
||||
// Resource is the name of the resource this rule applies to.
|
||||
// For example, if the administrator wants to limit consumption
|
||||
// of a storage resource associated with persistent volume claims,
|
||||
// the value would be "persistentvolumeclaims".
|
||||
Resource string `json:"resource"`
|
||||
|
||||
// For each intercepted request, the quota system will evaluate
|
||||
// its resource usage. It will iterate through each resource consumed
|
||||
// and if the resource contains any substring in this listing, the
|
||||
// quota system will ensure that there is a covering quota. In the
|
||||
// absence of a covering quota, the quota system will deny the request.
|
||||
// For example, if an administrator wants to globally enforce that
|
||||
// that a quota must exist to consume persistent volume claims associated
|
||||
// with any storage class, the list would include
|
||||
// ".storageclass.storage.k8s.io/requests.storage"
|
||||
MatchContains []string
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"defaults.go",
|
||||
"doc.go",
|
||||
"register.go",
|
||||
"types.go",
|
||||
"zz_generated.conversion.go",
|
||||
"zz_generated.deepcopy.go",
|
||||
"zz_generated.defaults.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
|
||||
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"//vendor:k8s.io/apimachinery/pkg/conversion",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime",
|
||||
"//vendor:k8s.io/apimachinery/pkg/runtime/schema",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import kruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
func addDefaultingFuncs(scheme *kruntime.Scheme) error {
|
||||
RegisterDefaults(scheme)
|
||||
return scheme.AddDefaultingFuncs(
|
||||
SetDefaults_Configuration,
|
||||
)
|
||||
}
|
||||
|
||||
func SetDefaults_Configuration(obj *Configuration) {}
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the API.
|
||||
// +groupName=resourcequota.admission.k8s.io
|
||||
package v1alpha1 // import "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1"
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// GroupName is the group name use in this package
|
||||
const GroupName = "resourcequota.admission.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes, addDefaultingFuncs)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Configuration{},
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *Configuration) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta }
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// Configuration provides configuration for the ResourceQuota admission controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// LimitedResources whose consumption is limited by default.
|
||||
// +optional
|
||||
LimitedResources []LimitedResource `json:"limitedResources"`
|
||||
}
|
||||
|
||||
// LimitedResource matches a resource whose consumption is limited by default.
|
||||
// To consume the resource, there must exist an associated quota that limits
|
||||
// its consumption.
|
||||
type LimitedResource struct {
|
||||
|
||||
// APIGroup is the name of the APIGroup that contains the limited resource.
|
||||
// +optional
|
||||
APIGroup string `json:"apiGroup,omitempty"`
|
||||
|
||||
// Resource is the name of the resource this rule applies to.
|
||||
// For example, if the administrator wants to limit consumption
|
||||
// of a storage resource associated with persistent volume claims,
|
||||
// the value would be "persistentvolumeclaims".
|
||||
Resource string `json:"resource"`
|
||||
|
||||
// For each intercepted request, the quota system will evaluate
|
||||
// its resource usage. It will iterate through each resource consumed
|
||||
// and if the resource contains any substring in this listing, the
|
||||
// quota system will ensure that there is a covering quota. In the
|
||||
// absence of a covering quota, the quota system will deny the request.
|
||||
// For example, if an administrator wants to globally enforce that
|
||||
// that a quota must exist to consume persistent volume claims associated
|
||||
// with any storage class, the list would include
|
||||
// ".storageclass.storage.k8s.io/requests.storage"
|
||||
MatchContains []string `json:"matchContains,omitempty"`
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was autogenerated by conversion-gen. Do not edit it manually!
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
resourcequota "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(RegisterConversions)
|
||||
}
|
||||
|
||||
// RegisterConversions adds conversion functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterConversions(scheme *runtime.Scheme) error {
|
||||
return scheme.AddGeneratedConversionFuncs(
|
||||
Convert_v1alpha1_Configuration_To_resourcequota_Configuration,
|
||||
Convert_resourcequota_Configuration_To_v1alpha1_Configuration,
|
||||
Convert_v1alpha1_LimitedResource_To_resourcequota_LimitedResource,
|
||||
Convert_resourcequota_LimitedResource_To_v1alpha1_LimitedResource,
|
||||
)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_Configuration_To_resourcequota_Configuration(in *Configuration, out *resourcequota.Configuration, s conversion.Scope) error {
|
||||
out.LimitedResources = *(*[]resourcequota.LimitedResource)(unsafe.Pointer(&in.LimitedResources))
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v1alpha1_Configuration_To_resourcequota_Configuration(in *Configuration, out *resourcequota.Configuration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_Configuration_To_resourcequota_Configuration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_resourcequota_Configuration_To_v1alpha1_Configuration(in *resourcequota.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
out.LimitedResources = *(*[]LimitedResource)(unsafe.Pointer(&in.LimitedResources))
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_resourcequota_Configuration_To_v1alpha1_Configuration(in *resourcequota.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
return autoConvert_resourcequota_Configuration_To_v1alpha1_Configuration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_LimitedResource_To_resourcequota_LimitedResource(in *LimitedResource, out *resourcequota.LimitedResource, s conversion.Scope) error {
|
||||
out.APIGroup = in.APIGroup
|
||||
out.Resource = in.Resource
|
||||
out.MatchContains = *(*[]string)(unsafe.Pointer(&in.MatchContains))
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_v1alpha1_LimitedResource_To_resourcequota_LimitedResource(in *LimitedResource, out *resourcequota.LimitedResource, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_LimitedResource_To_resourcequota_LimitedResource(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_resourcequota_LimitedResource_To_v1alpha1_LimitedResource(in *resourcequota.LimitedResource, out *LimitedResource, s conversion.Scope) error {
|
||||
out.APIGroup = in.APIGroup
|
||||
out.Resource = in.Resource
|
||||
out.MatchContains = *(*[]string)(unsafe.Pointer(&in.MatchContains))
|
||||
return nil
|
||||
}
|
||||
|
||||
func Convert_resourcequota_LimitedResource_To_v1alpha1_LimitedResource(in *resourcequota.LimitedResource, out *LimitedResource, s conversion.Scope) error {
|
||||
return autoConvert_resourcequota_LimitedResource_To_v1alpha1_LimitedResource(in, out, s)
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(RegisterDeepCopies)
|
||||
}
|
||||
|
||||
// RegisterDeepCopies adds deep-copy functions to the given scheme. Public
|
||||
// to allow building arbitrary schemes.
|
||||
func RegisterDeepCopies(scheme *runtime.Scheme) error {
|
||||
return scheme.AddGeneratedDeepCopyFuncs(
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_Configuration, InType: reflect.TypeOf(&Configuration{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_v1alpha1_LimitedResource, InType: reflect.TypeOf(&LimitedResource{})},
|
||||
)
|
||||
}
|
||||
|
||||
func DeepCopy_v1alpha1_Configuration(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*Configuration)
|
||||
out := out.(*Configuration)
|
||||
*out = *in
|
||||
if in.LimitedResources != nil {
|
||||
in, out := &in.LimitedResources, &out.LimitedResources
|
||||
*out = make([]LimitedResource, len(*in))
|
||||
for i := range *in {
|
||||
if err := DeepCopy_v1alpha1_LimitedResource(&(*in)[i], &(*out)[i], c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeepCopy_v1alpha1_LimitedResource(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*LimitedResource)
|
||||
out := out.(*LimitedResource)
|
||||
*out = *in
|
||||
if in.MatchContains != nil {
|
||||
in, out := &in.MatchContains, &out.MatchContains
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was autogenerated by defaulter-gen. Do not edit it manually!
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// RegisterDefaults adds defaulters functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
// All generated defaulters are covering - they call all nested defaulters.
|
||||
func RegisterDefaults(scheme *runtime.Scheme) error {
|
||||
scheme.AddTypeDefaultingFunc(&Configuration{}, func(obj interface{}) { SetObjectDefaults_Configuration(obj.(*Configuration)) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetObjectDefaults_Configuration(in *Configuration) {
|
||||
SetDefaults_Configuration(in)
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["validation.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library",
|
||||
"//vendor:k8s.io/apimachinery/pkg/util/validation/field",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["validation_test.go"],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = ["//plugin/pkg/admission/resourcequota/apis/resourcequota:go_default_library"],
|
||||
)
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
)
|
||||
|
||||
// ValidateConfiguration validates the configuration.
|
||||
func ValidateConfiguration(config *resourcequotaapi.Configuration) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
fldPath := field.NewPath("limitedResources")
|
||||
for i, limitedResource := range config.LimitedResources {
|
||||
idxPath := fldPath.Index(i)
|
||||
if len(limitedResource.Resource) == 0 {
|
||||
allErrs = append(allErrs, field.Required(idxPath.Child("resource"), ""))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
)
|
||||
|
||||
func TestValidateConfiguration(t *testing.T) {
|
||||
successCases := []resourcequotaapi.Configuration{
|
||||
{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"requests.cpu"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "persistentvolumeclaims",
|
||||
MatchContains: []string{"requests.storage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i := range successCases {
|
||||
configuration := successCases[i]
|
||||
if errs := ValidateConfiguration(&configuration); len(errs) != 0 {
|
||||
t.Errorf("expected success: %v", errs)
|
||||
}
|
||||
}
|
||||
errorCases := map[string]resourcequotaapi.Configuration{
|
||||
"missing apiGroupResource": {LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{MatchContains: []string{"requests.cpu"}},
|
||||
}},
|
||||
}
|
||||
for k, v := range errorCases {
|
||||
if errs := ValidateConfiguration(&v); len(errs) == 0 {
|
||||
t.Errorf("expected failure for %s", k)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
|
||||
|
||||
package resourcequota
|
||||
|
||||
import (
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(RegisterDeepCopies)
|
||||
}
|
||||
|
||||
// RegisterDeepCopies adds deep-copy functions to the given scheme. Public
|
||||
// to allow building arbitrary schemes.
|
||||
func RegisterDeepCopies(scheme *runtime.Scheme) error {
|
||||
return scheme.AddGeneratedDeepCopyFuncs(
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_resourcequota_Configuration, InType: reflect.TypeOf(&Configuration{})},
|
||||
conversion.GeneratedDeepCopyFunc{Fn: DeepCopy_resourcequota_LimitedResource, InType: reflect.TypeOf(&LimitedResource{})},
|
||||
)
|
||||
}
|
||||
|
||||
func DeepCopy_resourcequota_Configuration(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*Configuration)
|
||||
out := out.(*Configuration)
|
||||
*out = *in
|
||||
if in.LimitedResources != nil {
|
||||
in, out := &in.LimitedResources, &out.LimitedResources
|
||||
*out = make([]LimitedResource, len(*in))
|
||||
for i := range *in {
|
||||
if err := DeepCopy_resourcequota_LimitedResource(&(*in)[i], &(*out)[i], c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func DeepCopy_resourcequota_LimitedResource(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*LimitedResource)
|
||||
out := out.(*LimitedResource)
|
||||
*out = *in
|
||||
if in.MatchContains != nil {
|
||||
in, out := &in.MatchContains, &out.MatchContains
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
72
plugin/pkg/admission/resourcequota/config.go
Normal file
72
plugin/pkg/admission/resourcequota/config.go
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package resourcequota
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/install"
|
||||
resourcequotav1alpha1 "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
groupFactoryRegistry = make(announced.APIGroupFactoryRegistry)
|
||||
registry = registered.NewOrDie(os.Getenv("KUBE_API_VERSIONS"))
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(groupFactoryRegistry, registry, scheme)
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the provided configuration.
|
||||
func LoadConfiguration(config io.Reader) (*resourcequotaapi.Configuration, error) {
|
||||
// if no config is provided, return a default configuration
|
||||
if config == nil {
|
||||
externalConfig := &resourcequotav1alpha1.Configuration{}
|
||||
scheme.Default(externalConfig)
|
||||
internalConfig := &resourcequotaapi.Configuration{}
|
||||
if err := scheme.Convert(externalConfig, internalConfig, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return internalConfig, nil
|
||||
}
|
||||
// we have a config so parse it.
|
||||
data, err := ioutil.ReadAll(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decoder := codecs.UniversalDecoder()
|
||||
decodedObj, err := runtime.Decode(decoder, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceQuotaConfiguration, ok := decodedObj.(*resourcequotaapi.Configuration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type: %T", decodedObj)
|
||||
}
|
||||
return resourceQuotaConfiguration, nil
|
||||
}
|
@ -26,6 +26,7 @@ import (
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
@ -34,6 +35,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/quota"
|
||||
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
)
|
||||
|
||||
// Evaluator is used to see if quota constraints are satisfied.
|
||||
@ -65,6 +67,9 @@ type quotaEvaluator struct {
|
||||
workers int
|
||||
stopCh <-chan struct{}
|
||||
init sync.Once
|
||||
|
||||
// lets us know what resources are limited by default
|
||||
config *resourcequotaapi.Configuration
|
||||
}
|
||||
|
||||
type admissionWaiter struct {
|
||||
@ -79,6 +84,7 @@ func (defaultDeny) Error() string {
|
||||
return "DEFAULT DENY"
|
||||
}
|
||||
|
||||
// IsDefaultDeny returns true if the error is defaultDeny
|
||||
func IsDefaultDeny(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@ -99,7 +105,11 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter {
|
||||
// NewQuotaEvaluator configures an admission controller that can enforce quota constraints
|
||||
// using the provided registry. The registry must have the capability to handle group/kinds that
|
||||
// are persisted by the server this admission controller is intercepting
|
||||
func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, lockAcquisitionFunc func([]api.ResourceQuota) func(), workers int, stopCh <-chan struct{}) Evaluator {
|
||||
func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, lockAcquisitionFunc func([]api.ResourceQuota) func(), config *resourcequotaapi.Configuration, workers int, stopCh <-chan struct{}) Evaluator {
|
||||
// if we get a nil config, just create an empty default.
|
||||
if config == nil {
|
||||
config = &resourcequotaapi.Configuration{}
|
||||
}
|
||||
return "aEvaluator{
|
||||
quotaAccessor: quotaAccessor,
|
||||
lockAcquisitionFunc: lockAcquisitionFunc,
|
||||
@ -113,6 +123,7 @@ func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, loc
|
||||
|
||||
workers: workers,
|
||||
stopCh: stopCh,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +177,9 @@ func (e *quotaEvaluator) checkAttributes(ns string, admissionAttributes []*admis
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(quotas) == 0 {
|
||||
// if limited resources are disabled, we can just return safely when there are no quotas.
|
||||
limitedResourcesDisabled := len(e.config.LimitedResources) == 0
|
||||
if len(quotas) == 0 && limitedResourcesDisabled {
|
||||
for _, admissionAttribute := range admissionAttributes {
|
||||
admissionAttribute.result = nil
|
||||
}
|
||||
@ -316,6 +329,41 @@ func copyQuotas(in []api.ResourceQuota) ([]api.ResourceQuota, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// filterLimitedResourcesByGroupResource filters the input that match the specified groupResource
|
||||
func filterLimitedResourcesByGroupResource(input []resourcequotaapi.LimitedResource, groupResource schema.GroupResource) []resourcequotaapi.LimitedResource {
|
||||
result := []resourcequotaapi.LimitedResource{}
|
||||
for i := range input {
|
||||
limitedResource := input[i]
|
||||
limitedGroupResource := schema.GroupResource{Group: limitedResource.APIGroup, Resource: limitedResource.Resource}
|
||||
if limitedGroupResource == groupResource {
|
||||
result = append(result, limitedResource)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// limitedByDefault determines from the specfified usage and limitedResources the set of resources names
|
||||
// that must be present in a covering quota. It returns an error if it was unable to determine if
|
||||
// a resource was not limited by default.
|
||||
func limitedByDefault(usage api.ResourceList, limitedResources []resourcequotaapi.LimitedResource) []api.ResourceName {
|
||||
result := []api.ResourceName{}
|
||||
for _, limitedResource := range limitedResources {
|
||||
for k, v := range usage {
|
||||
// if a resource is consumed, we need to check if it matches on the limited resource list.
|
||||
if v.Sign() == 1 {
|
||||
// if we get a match, we add it to limited set
|
||||
for _, matchContain := range limitedResource.MatchContains {
|
||||
if strings.Contains(string(k), matchContain) {
|
||||
result = append(result, k)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// checkRequest verifies that the request does not exceed any quota constraint. it returns a copy of quotas not yet persisted
|
||||
// that capture what the usage would be if the request succeeded. It return an error if the is insufficient quota to satisfy the request
|
||||
func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.Attributes) ([]api.ResourceQuota, error) {
|
||||
@ -331,12 +379,30 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
|
||||
return quotas, nil
|
||||
}
|
||||
|
||||
// if we have limited resources enabled for this resource, always calculate usage
|
||||
inputObject := a.GetObject()
|
||||
|
||||
// determine the set of resource names that must exist in a covering quota
|
||||
limitedResourceNames := []api.ResourceName{}
|
||||
limitedResources := filterLimitedResourcesByGroupResource(e.config.LimitedResources, a.GetResource().GroupResource())
|
||||
if len(limitedResources) > 0 {
|
||||
deltaUsage, err := evaluator.Usage(inputObject)
|
||||
if err != nil {
|
||||
return quotas, err
|
||||
}
|
||||
limitedResourceNames = limitedByDefault(deltaUsage, limitedResources)
|
||||
}
|
||||
limitedResourceNamesSet := quota.ToSet(limitedResourceNames)
|
||||
|
||||
// find the set of quotas that are pertinent to this request
|
||||
// reject if we match the quota, but usage is not calculated yet
|
||||
// reject if the input object does not satisfy quota constraints
|
||||
// if there are no pertinent quotas, we can just return
|
||||
inputObject := a.GetObject()
|
||||
interestingQuotaIndexes := []int{}
|
||||
// track the cumulative set of resources that were required across all quotas
|
||||
// this is needed to know if we have satisfied any constraints where consumption
|
||||
// was limited by default.
|
||||
restrictedResourcesSet := sets.String{}
|
||||
for i := range quotas {
|
||||
resourceQuota := quotas[i]
|
||||
match, err := evaluator.Matches(&resourceQuota, inputObject)
|
||||
@ -348,16 +414,26 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
|
||||
}
|
||||
|
||||
hardResources := quota.ResourceNames(resourceQuota.Status.Hard)
|
||||
requiredResources := evaluator.MatchingResources(hardResources)
|
||||
if err := evaluator.Constraints(requiredResources, inputObject); err != nil {
|
||||
restrictedResources := evaluator.MatchingResources(hardResources)
|
||||
if err := evaluator.Constraints(restrictedResources, inputObject); err != nil {
|
||||
return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err))
|
||||
}
|
||||
if !hasUsageStats(&resourceQuota) {
|
||||
return nil, admission.NewForbidden(a, fmt.Errorf("status unknown for quota: %s", resourceQuota.Name))
|
||||
}
|
||||
|
||||
interestingQuotaIndexes = append(interestingQuotaIndexes, i)
|
||||
localRestrictedResourcesSet := quota.ToSet(restrictedResources)
|
||||
restrictedResourcesSet.Insert(localRestrictedResourcesSet.List()...)
|
||||
}
|
||||
|
||||
// verify that for every resource that had limited by default consumption
|
||||
// enabled that there was a corresponding quota that covered its use.
|
||||
// if not, we reject the request.
|
||||
hasNoCoveringQuota := limitedResourceNamesSet.Difference(restrictedResourcesSet)
|
||||
if len(hasNoCoveringQuota) > 0 {
|
||||
return quotas, fmt.Errorf("insufficient quota to consume: %v", strings.Join(hasNoCoveringQuota.List(), ","))
|
||||
}
|
||||
|
||||
if len(interestingQuotaIndexes) == 0 {
|
||||
return quotas, nil
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ import (
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/v1"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
|
||||
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
|
||||
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
@ -44,6 +44,7 @@ import (
|
||||
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
||||
quotainstall "k8s.io/kubernetes/pkg/quota/install"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota"
|
||||
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
@ -65,7 +66,8 @@ func TestQuota(t *testing.T) {
|
||||
admissionCh := make(chan struct{})
|
||||
clientset := clientset.NewForConfigOrDie(&restclient.Config{QPS: -1, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(v1.GroupName).GroupVersion}})
|
||||
internalClientset := internalclientset.NewForConfigOrDie(&restclient.Config{QPS: -1, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(v1.GroupName).GroupVersion}})
|
||||
admission, err := resourcequota.NewResourceQuota(quotainstall.NewRegistry(nil, nil), 5, admissionCh)
|
||||
config := &resourcequotaapi.Configuration{}
|
||||
admission, err := resourcequota.NewResourceQuota(quotainstall.NewRegistry(nil, nil), config, 5, admissionCh)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@ -226,3 +228,108 @@ func scale(t *testing.T, namespace string, clientset *clientset.Clientset) {
|
||||
t.Fatalf("unexpected error: %v, ended with %v pods", err, len(pods.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotaLimitedResourceDenial(t *testing.T) {
|
||||
// Set up a master
|
||||
h := &framework.MasterHolder{Initialized: make(chan struct{})}
|
||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
<-h.Initialized
|
||||
h.M.GenericAPIServer.Handler.ServeHTTP(w, req)
|
||||
}))
|
||||
defer s.Close()
|
||||
|
||||
admissionCh := make(chan struct{})
|
||||
clientset := clientset.NewForConfigOrDie(&restclient.Config{QPS: -1, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(v1.GroupName).GroupVersion}})
|
||||
internalClientset := internalclientset.NewForConfigOrDie(&restclient.Config{QPS: -1, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(v1.GroupName).GroupVersion}})
|
||||
|
||||
// stop creation of a pod resource unless there is a quota
|
||||
config := &resourcequotaapi.Configuration{
|
||||
LimitedResources: []resourcequotaapi.LimitedResource{
|
||||
{
|
||||
Resource: "pods",
|
||||
MatchContains: []string{"pods"},
|
||||
},
|
||||
},
|
||||
}
|
||||
admission, err := resourcequota.NewResourceQuota(quotainstall.NewRegistry(nil, nil), config, 5, admissionCh)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
admission.(kubeadmission.WantsInternalClientSet).SetInternalClientSet(internalClientset)
|
||||
defer close(admissionCh)
|
||||
|
||||
masterConfig := framework.NewIntegrationTestMasterConfig()
|
||||
masterConfig.GenericConfig.AdmissionControl = admission
|
||||
framework.RunAMasterUsingServer(masterConfig, s, h)
|
||||
|
||||
ns := framework.CreateTestingNamespace("quota", s, t)
|
||||
defer framework.DeleteTestingNamespace(ns, s, t)
|
||||
|
||||
controllerCh := make(chan struct{})
|
||||
defer close(controllerCh)
|
||||
|
||||
informers := informers.NewSharedInformerFactory(clientset, controller.NoResyncPeriodFunc())
|
||||
rm := replicationcontroller.NewReplicationManager(
|
||||
informers.Core().V1().Pods(),
|
||||
informers.Core().V1().ReplicationControllers(),
|
||||
clientset,
|
||||
replicationcontroller.BurstReplicas,
|
||||
4096,
|
||||
false,
|
||||
)
|
||||
rm.SetEventRecorder(&record.FakeRecorder{})
|
||||
go rm.Run(3, controllerCh)
|
||||
|
||||
resourceQuotaRegistry := quotainstall.NewRegistry(clientset, nil)
|
||||
groupKindsToReplenish := []schema.GroupKind{
|
||||
api.Kind("Pod"),
|
||||
}
|
||||
resourceQuotaControllerOptions := &resourcequotacontroller.ResourceQuotaControllerOptions{
|
||||
KubeClient: clientset,
|
||||
ResourceQuotaInformer: informers.Core().V1().ResourceQuotas(),
|
||||
ResyncPeriod: controller.NoResyncPeriodFunc,
|
||||
Registry: resourceQuotaRegistry,
|
||||
GroupKindsToReplenish: groupKindsToReplenish,
|
||||
ReplenishmentResyncPeriod: controller.NoResyncPeriodFunc,
|
||||
ControllerFactory: resourcequotacontroller.NewReplenishmentControllerFactory(informers),
|
||||
}
|
||||
go resourcequotacontroller.NewResourceQuotaController(resourceQuotaControllerOptions).Run(2, controllerCh)
|
||||
informers.Start(controllerCh)
|
||||
|
||||
// try to create a pod
|
||||
pod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "container",
|
||||
Image: "busybox",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if _, err := clientset.Core().Pods(ns.Name).Create(pod); err == nil {
|
||||
t.Fatalf("expected error for insufficient quota")
|
||||
}
|
||||
|
||||
// now create a covering quota
|
||||
quota := &v1.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "quota",
|
||||
Namespace: ns.Name,
|
||||
},
|
||||
Spec: v1.ResourceQuotaSpec{
|
||||
Hard: v1.ResourceList{
|
||||
v1.ResourcePods: resource.MustParse("1000"),
|
||||
},
|
||||
},
|
||||
}
|
||||
waitForQuota(t, quota, clientset)
|
||||
|
||||
if _, err := clientset.Core().Pods(ns.Name).Create(pod); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user