Commit 506950ad authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

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 ```
parents 4e1ca533 3fad0cb5
...@@ -262,6 +262,8 @@ plugin/pkg/admission/gc ...@@ -262,6 +262,8 @@ plugin/pkg/admission/gc
plugin/pkg/admission/imagepolicy plugin/pkg/admission/imagepolicy
plugin/pkg/admission/namespace/autoprovision plugin/pkg/admission/namespace/autoprovision
plugin/pkg/admission/namespace/exists 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/admission/securitycontext/scdeny
plugin/pkg/auth plugin/pkg/auth
plugin/pkg/auth/authorizer plugin/pkg/auth/authorizer
......
...@@ -76,6 +76,7 @@ AUTH_ARGS=${AUTH_ARGS:-""} ...@@ -76,6 +76,7 @@ AUTH_ARGS=${AUTH_ARGS:-""}
KUBE_CACHE_MUTATION_DETECTOR="${KUBE_CACHE_MUTATION_DETECTOR:-true}" KUBE_CACHE_MUTATION_DETECTOR="${KUBE_CACHE_MUTATION_DETECTOR:-true}"
export KUBE_CACHE_MUTATION_DETECTOR export KUBE_CACHE_MUTATION_DETECTOR
ADMISSION_CONTROL_CONFIG_FILE=${ADMISSION_CONTROL_CONFIG_FILE:-""}
# START_MODE can be 'all', 'kubeletonly', or 'nokubelet' # START_MODE can be 'all', 'kubeletonly', or 'nokubelet'
START_MODE=${START_MODE:-"all"} START_MODE=${START_MODE:-"all"}
...@@ -434,6 +435,7 @@ function start_apiserver { ...@@ -434,6 +435,7 @@ function start_apiserver {
--service-account-key-file="${SERVICE_ACCOUNT_KEY}" \ --service-account-key-file="${SERVICE_ACCOUNT_KEY}" \
--service-account-lookup="${SERVICE_ACCOUNT_LOOKUP}" \ --service-account-lookup="${SERVICE_ACCOUNT_LOOKUP}" \
--admission-control="${ADMISSION_CONTROL}" \ --admission-control="${ADMISSION_CONTROL}" \
--admission-control-config-file="${ADMISSION_CONTROL_CONFIG_FILE}" \
--bind-address="${API_BIND_ADDR}" \ --bind-address="${API_BIND_ADDR}" \
--secure-port="${API_SECURE_PORT}" \ --secure-port="${API_SECURE_PORT}" \
--tls-cert-file="${CERT_DIR}/serving-kube-apiserver.crt" \ --tls-cert-file="${CERT_DIR}/serving-kube-apiserver.crt" \
......
...@@ -12,6 +12,7 @@ go_library( ...@@ -12,6 +12,7 @@ go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"admission.go", "admission.go",
"config.go",
"controller.go", "controller.go",
"doc.go", "doc.go",
"resource_access.go", "resource_access.go",
...@@ -24,11 +25,19 @@ go_library( ...@@ -24,11 +25,19 @@ go_library(
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/install:go_default_library", "//pkg/quota/install:go_default_library",
"//pkg/util/workqueue/prometheus: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/golang/glog",
"//vendor:github.com/hashicorp/golang-lru", "//vendor:github.com/hashicorp/golang-lru",
"//vendor:k8s.io/apimachinery/pkg/api/meta", "//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/apis/meta/v1",
"//vendor:k8s.io/apimachinery/pkg/runtime", "//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/runtime",
"//vendor:k8s.io/apimachinery/pkg/util/sets", "//vendor:k8s.io/apimachinery/pkg/util/sets",
"//vendor:k8s.io/apimachinery/pkg/util/wait", "//vendor:k8s.io/apimachinery/pkg/util/wait",
...@@ -51,6 +60,7 @@ go_test( ...@@ -51,6 +60,7 @@ go_test(
"//pkg/quota:go_default_library", "//pkg/quota:go_default_library",
"//pkg/quota/generic:go_default_library", "//pkg/quota/generic:go_default_library",
"//pkg/quota/install: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:github.com/hashicorp/golang-lru",
"//vendor:k8s.io/apimachinery/pkg/api/resource", "//vendor:k8s.io/apimachinery/pkg/api/resource",
"//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1",
...@@ -71,6 +81,9 @@ filegroup( ...@@ -71,6 +81,9 @@ filegroup(
filegroup( filegroup(
name = "all-srcs", name = "all-srcs",
srcs = [":package-srcs"], srcs = [
":package-srcs",
"//plugin/pkg/admission/resourcequota/apis/resourcequota:all-srcs",
],
tags = ["automanaged"], tags = ["automanaged"],
) )
...@@ -27,22 +27,35 @@ import ( ...@@ -27,22 +27,35 @@ import (
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
"k8s.io/kubernetes/pkg/quota/install" "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() { func init() {
admission.RegisterPlugin("ResourceQuota", admission.RegisterPlugin("ResourceQuota",
func(config io.Reader) (admission.Interface, error) { 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 // 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. // does not require us to open watches for all items tracked by quota.
registry := install.NewRegistry(nil, nil) 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 // quotaAdmission implements an admission controller that can enforce quota constraints
type quotaAdmission struct { type quotaAdmission struct {
*admission.Handler *admission.Handler
config *resourcequotaapi.Configuration
stopCh <-chan struct{} stopCh <-chan struct{}
registry quota.Registry registry quota.Registry
numEvaluators int numEvaluators int
...@@ -59,12 +72,13 @@ type liveLookupEntry struct { ...@@ -59,12 +72,13 @@ type liveLookupEntry struct {
// NewResourceQuota configures an admission controller that can enforce quota constraints // 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 // 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 // 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 &quotaAdmission{ return &quotaAdmission{
Handler: admission.NewHandler(admission.Create, admission.Update), Handler: admission.NewHandler(admission.Create, admission.Update),
stopCh: stopCh, stopCh: stopCh,
registry: registry, registry: registry,
numEvaluators: numEvaluators, numEvaluators: numEvaluators,
config: config,
}, nil }, nil
} }
...@@ -77,7 +91,7 @@ func (a *quotaAdmission) SetInternalClientSet(client internalclientset.Interface ...@@ -77,7 +91,7 @@ func (a *quotaAdmission) SetInternalClientSet(client internalclientset.Interface
} }
go quotaAccessor.Run(a.stopCh) 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. // Validate ensures an authorizer is set.
...@@ -89,11 +103,10 @@ func (a *quotaAdmission) Validate() error { ...@@ -89,11 +103,10 @@ func (a *quotaAdmission) Validate() error {
} }
// Admit makes admission decisions while enforcing quota // 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 // ignore all operations that correspond to sub-resource actions
if a.GetSubresource() != "" { if attr.GetSubresource() != "" {
return nil return nil
} }
return a.evaluator.Evaluate(attr)
return q.evaluator.Evaluate(a)
} }
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"],
)
reviewers:
- deads2k
- derekwaynecarr
approvers:
- deads2k
- derekwaynecarr
- smarterclayton
\ No newline at end of file
/*
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"
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"],
)
/*
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)
}
}
/*
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 }
/*
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
}
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"],
)
/*
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) {}
/*
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"
/*
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 }
/*
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"`
}
// +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)
}
// +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
}
}
// +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)
}
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"],
)
/*
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
}
/*
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)
}
}
}
// +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
}
}
/*
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 ( ...@@ -26,6 +26,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
...@@ -34,6 +35,7 @@ import ( ...@@ -34,6 +35,7 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/quota" "k8s.io/kubernetes/pkg/quota"
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus" // for workqueue metric registration _ "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. // Evaluator is used to see if quota constraints are satisfied.
...@@ -65,6 +67,9 @@ type quotaEvaluator struct { ...@@ -65,6 +67,9 @@ type quotaEvaluator struct {
workers int workers int
stopCh <-chan struct{} stopCh <-chan struct{}
init sync.Once init sync.Once
// lets us know what resources are limited by default
config *resourcequotaapi.Configuration
} }
type admissionWaiter struct { type admissionWaiter struct {
...@@ -79,6 +84,7 @@ func (defaultDeny) Error() string { ...@@ -79,6 +84,7 @@ func (defaultDeny) Error() string {
return "DEFAULT DENY" return "DEFAULT DENY"
} }
// IsDefaultDeny returns true if the error is defaultDeny
func IsDefaultDeny(err error) bool { func IsDefaultDeny(err error) bool {
if err == nil { if err == nil {
return false return false
...@@ -99,7 +105,11 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter { ...@@ -99,7 +105,11 @@ func newAdmissionWaiter(a admission.Attributes) *admissionWaiter {
// NewQuotaEvaluator configures an admission controller that can enforce quota constraints // 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 // 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 // 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 &quotaEvaluator{ return &quotaEvaluator{
quotaAccessor: quotaAccessor, quotaAccessor: quotaAccessor,
lockAcquisitionFunc: lockAcquisitionFunc, lockAcquisitionFunc: lockAcquisitionFunc,
...@@ -113,6 +123,7 @@ func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, loc ...@@ -113,6 +123,7 @@ func NewQuotaEvaluator(quotaAccessor QuotaAccessor, registry quota.Registry, loc
workers: workers, workers: workers,
stopCh: stopCh, stopCh: stopCh,
config: config,
} }
} }
...@@ -166,7 +177,9 @@ func (e *quotaEvaluator) checkAttributes(ns string, admissionAttributes []*admis ...@@ -166,7 +177,9 @@ func (e *quotaEvaluator) checkAttributes(ns string, admissionAttributes []*admis
} }
return 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 { for _, admissionAttribute := range admissionAttributes {
admissionAttribute.result = nil admissionAttribute.result = nil
} }
...@@ -316,6 +329,41 @@ func copyQuotas(in []api.ResourceQuota) ([]api.ResourceQuota, error) { ...@@ -316,6 +329,41 @@ func copyQuotas(in []api.ResourceQuota) ([]api.ResourceQuota, error) {
return out, nil 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 // 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 // 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) { 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 ...@@ -331,12 +379,30 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
return quotas, nil 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 // 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 we match the quota, but usage is not calculated yet
// reject if the input object does not satisfy quota constraints // reject if the input object does not satisfy quota constraints
// if there are no pertinent quotas, we can just return // if there are no pertinent quotas, we can just return
inputObject := a.GetObject()
interestingQuotaIndexes := []int{} 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 { for i := range quotas {
resourceQuota := quotas[i] resourceQuota := quotas[i]
match, err := evaluator.Matches(&resourceQuota, inputObject) match, err := evaluator.Matches(&resourceQuota, inputObject)
...@@ -348,16 +414,26 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At ...@@ -348,16 +414,26 @@ func (e *quotaEvaluator) checkRequest(quotas []api.ResourceQuota, a admission.At
} }
hardResources := quota.ResourceNames(resourceQuota.Status.Hard) hardResources := quota.ResourceNames(resourceQuota.Status.Hard)
requiredResources := evaluator.MatchingResources(hardResources) restrictedResources := evaluator.MatchingResources(hardResources)
if err := evaluator.Constraints(requiredResources, inputObject); err != nil { if err := evaluator.Constraints(restrictedResources, inputObject); err != nil {
return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err)) return nil, admission.NewForbidden(a, fmt.Errorf("failed quota: %s: %v", resourceQuota.Name, err))
} }
if !hasUsageStats(&resourceQuota) { if !hasUsageStats(&resourceQuota) {
return nil, admission.NewForbidden(a, fmt.Errorf("status unknown for quota: %s", resourceQuota.Name)) return nil, admission.NewForbidden(a, fmt.Errorf("status unknown for quota: %s", resourceQuota.Name))
} }
interestingQuotaIndexes = append(interestingQuotaIndexes, i) 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 { if len(interestingQuotaIndexes) == 0 {
return quotas, nil return quotas, nil
} }
......
...@@ -35,7 +35,7 @@ import ( ...@@ -35,7 +35,7 @@ import (
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "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" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/externalversions"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
...@@ -44,6 +44,7 @@ import ( ...@@ -44,6 +44,7 @@ import (
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission" kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
quotainstall "k8s.io/kubernetes/pkg/quota/install" quotainstall "k8s.io/kubernetes/pkg/quota/install"
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota" "k8s.io/kubernetes/plugin/pkg/admission/resourcequota"
resourcequotaapi "k8s.io/kubernetes/plugin/pkg/admission/resourcequota/apis/resourcequota"
"k8s.io/kubernetes/test/integration/framework" "k8s.io/kubernetes/test/integration/framework"
) )
...@@ -65,7 +66,8 @@ func TestQuota(t *testing.T) { ...@@ -65,7 +66,8 @@ func TestQuota(t *testing.T) {
admissionCh := make(chan struct{}) admissionCh := make(chan struct{})
clientset := clientset.NewForConfigOrDie(&restclient.Config{QPS: -1, Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(v1.GroupName).GroupVersion}}) 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}}) 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 { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
...@@ -226,3 +228,108 @@ func scale(t *testing.T, namespace string, clientset *clientset.Clientset) { ...@@ -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)) 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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment