mirror of https://github.com/k3s-io/k3s
Remove admission controllers
parent
9e464f5b6a
commit
793d05ff09
|
@ -21,92 +21,50 @@ package options
|
|||
// given binary target.
|
||||
import (
|
||||
// Admission policies
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/admit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/deny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/exec"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/extendedresourcetoleration"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/gc"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/limitranger"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/namespace/exists"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/nodetaint"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podnodeselector"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction"
|
||||
podpriority "k8s.io/kubernetes/plugin/pkg/admission/priority"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/resourcequota"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/serviceaccount"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/storage/persistentvolume/resize"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/storage/storageclass/setdefault"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/storage/storageobjectinuseprotection"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// AllOrderedPlugins is the list of all the plugins in order.
|
||||
var AllOrderedPlugins = []string{
|
||||
admit.PluginName, // AlwaysAdmit
|
||||
autoprovision.PluginName, // NamespaceAutoProvision
|
||||
lifecycle.PluginName, // NamespaceLifecycle
|
||||
exists.PluginName, // NamespaceExists
|
||||
scdeny.PluginName, // SecurityContextDeny
|
||||
antiaffinity.PluginName, // LimitPodHardAntiAffinityTopology
|
||||
limitranger.PluginName, // LimitRanger
|
||||
serviceaccount.PluginName, // ServiceAccount
|
||||
nodetaint.PluginName, // TaintNodesByCondition
|
||||
alwayspullimages.PluginName, // AlwaysPullImages
|
||||
podsecuritypolicy.PluginName, // PodSecurityPolicy
|
||||
podnodeselector.PluginName, // PodNodeSelector
|
||||
podpriority.PluginName, // Priority
|
||||
defaulttolerationseconds.PluginName, // DefaultTolerationSeconds
|
||||
podtolerationrestriction.PluginName, // PodTolerationRestriction
|
||||
exec.DenyEscalatingExec, // DenyEscalatingExec
|
||||
exec.DenyExecOnPrivileged, // DenyExecOnPrivileged
|
||||
eventratelimit.PluginName, // EventRateLimit
|
||||
extendedresourcetoleration.PluginName, // ExtendedResourceToleration
|
||||
setdefault.PluginName, // DefaultStorageClass
|
||||
storageobjectinuseprotection.PluginName, // StorageObjectInUseProtection
|
||||
gc.PluginName, // OwnerReferencesPermissionEnforcement
|
||||
resize.PluginName, // PersistentVolumeClaimResize
|
||||
mutatingwebhook.PluginName, // MutatingAdmissionWebhook
|
||||
validatingwebhook.PluginName, // ValidatingAdmissionWebhook
|
||||
resourcequota.PluginName, // ResourceQuota
|
||||
deny.PluginName, // AlwaysDeny
|
||||
}
|
||||
|
||||
// RegisterAllAdmissionPlugins registers all admission plugins and
|
||||
// sets the recommended plugins order.
|
||||
func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||
admit.Register(plugins) // DEPRECATED as no real meaning
|
||||
alwayspullimages.Register(plugins)
|
||||
antiaffinity.Register(plugins)
|
||||
defaulttolerationseconds.Register(plugins)
|
||||
deny.Register(plugins) // DEPRECATED as no real meaning
|
||||
eventratelimit.Register(plugins)
|
||||
exec.Register(plugins)
|
||||
extendedresourcetoleration.Register(plugins)
|
||||
gc.Register(plugins)
|
||||
limitranger.Register(plugins)
|
||||
autoprovision.Register(plugins)
|
||||
exists.Register(plugins)
|
||||
nodetaint.Register(plugins)
|
||||
podnodeselector.Register(plugins)
|
||||
podtolerationrestriction.Register(plugins)
|
||||
resourcequota.Register(plugins)
|
||||
podsecuritypolicy.Register(plugins)
|
||||
podpriority.Register(plugins)
|
||||
scdeny.Register(plugins)
|
||||
serviceaccount.Register(plugins)
|
||||
setdefault.Register(plugins)
|
||||
resize.Register(plugins)
|
||||
storageobjectinuseprotection.Register(plugins)
|
||||
}
|
||||
|
||||
// DefaultOffAdmissionPlugins get admission plugins off by default for kube-apiserver.
|
||||
|
@ -118,6 +76,8 @@ func DefaultOffAdmissionPlugins() sets.String {
|
|||
setdefault.PluginName, //DefaultStorageClass
|
||||
resize.PluginName, //PersistentVolumeClaimResize
|
||||
defaulttolerationseconds.PluginName, //DefaultTolerationSeconds
|
||||
mutatingwebhook.PluginName, //MutatingAdmissionWebhook
|
||||
validatingwebhook.PluginName, //ValidatingAdmissionWebhook
|
||||
resourcequota.PluginName, //ResourceQuota
|
||||
)
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/admit",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 admit
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "AlwaysAdmit"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewAlwaysAdmit(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// alwaysAdmit is an implementation of admission.Interface which always says yes to an admit request.
|
||||
type alwaysAdmit struct{}
|
||||
|
||||
var _ admission.MutationInterface = alwaysAdmit{}
|
||||
var _ admission.ValidationInterface = alwaysAdmit{}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes
|
||||
func (alwaysAdmit) Admit(a admission.Attributes) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate.
|
||||
func (alwaysAdmit) Validate(a admission.Attributes) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handles returns true if this admission controller can handle the given operation
|
||||
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
|
||||
func (alwaysAdmit) Handles(operation admission.Operation) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewAlwaysAdmit creates a new always admit admission handler
|
||||
func NewAlwaysAdmit() admission.Interface {
|
||||
// DEPRECATED: AlwaysAdmit admit all admission request, it is no use.
|
||||
klog.Warningf("%s admission controller is deprecated. "+
|
||||
"Please remove this controller from your configuration files and scripts.", PluginName)
|
||||
return new(alwaysAdmit)
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 admit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func TestAdmissionNonNilAttribute(t *testing.T) {
|
||||
handler := NewAlwaysAdmit()
|
||||
err := handler.(*alwaysAdmit).Admit(admission.NewAttributesRecord(nil, nil, api.Kind("kind").WithVersion("version"), "namespace", "name", api.Resource("resource").WithVersion("version"), "subresource", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error returned from admission handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmissionNilAttribute(t *testing.T) {
|
||||
handler := NewAlwaysAdmit()
|
||||
err := handler.(*alwaysAdmit).Admit(nil)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error returned from admission handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
handler := NewAlwaysAdmit()
|
||||
tests := []admission.Operation{admission.Create, admission.Connect, admission.Update, admission.Delete}
|
||||
|
||||
for _, test := range tests {
|
||||
if !handler.Handles(test) {
|
||||
t.Errorf("Expected handling all operations, including: %v", test)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,124 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 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 alwayspullimages contains an admission controller that modifies every new Pod to force
|
||||
// the image pull policy to Always. This is useful in a multitenant cluster so that users can be
|
||||
// assured that their private images can only be used by those who have the credentials to pull
|
||||
// them. Without this admission controller, once an image has been pulled to a node, any pod from
|
||||
// any user can use it simply by knowing the image's name (assuming the Pod is scheduled onto the
|
||||
// right node), without any authorization check against the image. With this admission controller
|
||||
// enabled, images are always pulled prior to starting containers, which means valid credentials are
|
||||
// required.
|
||||
package alwayspullimages
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "AlwaysPullImages"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewAlwaysPullImages(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// AlwaysPullImages is an implementation of admission.Interface.
|
||||
// It looks at all new pods and overrides each container's image pull policy to Always.
|
||||
type AlwaysPullImages struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
var _ admission.MutationInterface = &AlwaysPullImages{}
|
||||
var _ admission.ValidationInterface = &AlwaysPullImages{}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes
|
||||
func (a *AlwaysPullImages) Admit(attributes admission.Attributes) (err error) {
|
||||
// Ignore all calls to subresources or resources other than pods.
|
||||
if shouldIgnore(attributes) {
|
||||
return nil
|
||||
}
|
||||
pod, ok := attributes.GetObject().(*api.Pod)
|
||||
if !ok {
|
||||
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
||||
}
|
||||
|
||||
for i := range pod.Spec.InitContainers {
|
||||
pod.Spec.InitContainers[i].ImagePullPolicy = api.PullAlways
|
||||
}
|
||||
|
||||
for i := range pod.Spec.Containers {
|
||||
pod.Spec.Containers[i].ImagePullPolicy = api.PullAlways
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes sure that all containers are set to always pull images
|
||||
func (*AlwaysPullImages) Validate(attributes admission.Attributes) (err error) {
|
||||
if shouldIgnore(attributes) {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod, ok := attributes.GetObject().(*api.Pod)
|
||||
if !ok {
|
||||
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
||||
}
|
||||
|
||||
for i := range pod.Spec.InitContainers {
|
||||
if pod.Spec.InitContainers[i].ImagePullPolicy != api.PullAlways {
|
||||
return admission.NewForbidden(attributes,
|
||||
field.NotSupported(field.NewPath("spec", "initContainers").Index(i).Child("imagePullPolicy"),
|
||||
pod.Spec.InitContainers[i].ImagePullPolicy, []string{string(api.PullAlways)},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
for i := range pod.Spec.Containers {
|
||||
if pod.Spec.Containers[i].ImagePullPolicy != api.PullAlways {
|
||||
return admission.NewForbidden(attributes,
|
||||
field.NotSupported(field.NewPath("spec", "containers").Index(i).Child("imagePullPolicy"),
|
||||
pod.Spec.Containers[i].ImagePullPolicy, []string{string(api.PullAlways)},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldIgnore(attributes admission.Attributes) bool {
|
||||
// Ignore all calls to subresources or resources other than pods.
|
||||
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// NewAlwaysPullImages creates a new always pull images admission control handler
|
||||
func NewAlwaysPullImages() *AlwaysPullImages {
|
||||
return &AlwaysPullImages{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 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 alwayspullimages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// TestAdmission verifies all create requests for pods result in every container's image pull policy
|
||||
// set to Always
|
||||
func TestAdmission(t *testing.T) {
|
||||
namespace := "test"
|
||||
handler := &AlwaysPullImages{}
|
||||
pod := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
|
||||
Spec: api.PodSpec{
|
||||
InitContainers: []api.Container{
|
||||
{Name: "init1", Image: "image"},
|
||||
{Name: "init2", Image: "image", ImagePullPolicy: api.PullNever},
|
||||
{Name: "init3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
|
||||
{Name: "init4", Image: "image", ImagePullPolicy: api.PullAlways},
|
||||
},
|
||||
Containers: []api.Container{
|
||||
{Name: "ctr1", Image: "image"},
|
||||
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
|
||||
{Name: "ctr3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
|
||||
{Name: "ctr4", Image: "image", ImagePullPolicy: api.PullAlways},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error returned from admission handler")
|
||||
}
|
||||
for _, c := range pod.Spec.InitContainers {
|
||||
if c.ImagePullPolicy != api.PullAlways {
|
||||
t.Errorf("Container %v: expected pull always, got %v", c, c.ImagePullPolicy)
|
||||
}
|
||||
}
|
||||
for _, c := range pod.Spec.Containers {
|
||||
if c.ImagePullPolicy != api.PullAlways {
|
||||
t.Errorf("Container %v: expected pull always, got %v", c, c.ImagePullPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
namespace := "test"
|
||||
handler := &AlwaysPullImages{}
|
||||
pod := api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
|
||||
Spec: api.PodSpec{
|
||||
InitContainers: []api.Container{
|
||||
{Name: "init1", Image: "image"},
|
||||
{Name: "init2", Image: "image", ImagePullPolicy: api.PullNever},
|
||||
{Name: "init3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
|
||||
{Name: "init4", Image: "image", ImagePullPolicy: api.PullAlways},
|
||||
},
|
||||
Containers: []api.Container{
|
||||
{Name: "ctr1", Image: "image"},
|
||||
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
|
||||
{Name: "ctr3", Image: "image", ImagePullPolicy: api.PullIfNotPresent},
|
||||
{Name: "ctr4", Image: "image", ImagePullPolicy: api.PullAlways},
|
||||
},
|
||||
},
|
||||
}
|
||||
expectedError := `pods "123" is forbidden: spec.initContainers[0].imagePullPolicy: Unsupported value: "": supported values: "Always"`
|
||||
err := handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err == nil {
|
||||
t.Fatal("missing expected error")
|
||||
}
|
||||
if err.Error() != expectedError {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOtherResources ensures that this admission controller is a no-op for other resources,
|
||||
// subresources, and non-pods.
|
||||
func TestOtherResources(t *testing.T) {
|
||||
namespace := "testnamespace"
|
||||
name := "testname"
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{Name: "ctr2", Image: "image", ImagePullPolicy: api.PullNever},
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
kind string
|
||||
resource string
|
||||
subresource string
|
||||
object runtime.Object
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "non-pod resource",
|
||||
kind: "Foo",
|
||||
resource: "foos",
|
||||
object: pod,
|
||||
},
|
||||
{
|
||||
name: "pod subresource",
|
||||
kind: "Pod",
|
||||
resource: "pods",
|
||||
subresource: "exec",
|
||||
object: pod,
|
||||
},
|
||||
{
|
||||
name: "non-pod object",
|
||||
kind: "Pod",
|
||||
resource: "pods",
|
||||
object: &api.Service{},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
handler := &AlwaysPullImages{}
|
||||
|
||||
err := handler.Admit(admission.NewAttributesRecord(tc.object, nil, api.Kind(tc.kind).WithVersion("version"), namespace, name, api.Resource(tc.resource).WithVersion("version"), tc.subresource, admission.Create, false, nil))
|
||||
|
||||
if tc.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("%s: unexpected nil error", tc.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if e, a := api.PullNever, pod.Spec.Containers[0].ImagePullPolicy; e != a {
|
||||
t.Errorf("%s: image pull policy was changed to %s", tc.name, a)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"doc.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/antiaffinity",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/kubelet/apis:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/kubelet/apis:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package antiaffinity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
|
||||
)
|
||||
|
||||
const PluginName = "LimitPodHardAntiAffinityTopology"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewInterPodAntiAffinity(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin contains the client used by the admission controller
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// NewInterPodAntiAffinity creates a new instance of the LimitPodHardAntiAffinityTopology admission controller
|
||||
func NewInterPodAntiAffinity() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate will deny any pod that defines AntiAffinity topology key other than kubeletapis.LabelHostname i.e. "kubernetes.io/hostname"
|
||||
// in requiredDuringSchedulingRequiredDuringExecution and requiredDuringSchedulingIgnoredDuringExecution.
|
||||
func (p *Plugin) Validate(attributes admission.Attributes) (err error) {
|
||||
// Ignore all calls to subresources or resources other than pods.
|
||||
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != api.Resource("pods") {
|
||||
return nil
|
||||
}
|
||||
pod, ok := attributes.GetObject().(*api.Pod)
|
||||
if !ok {
|
||||
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
||||
}
|
||||
affinity := pod.Spec.Affinity
|
||||
if affinity != nil && affinity.PodAntiAffinity != nil {
|
||||
var podAntiAffinityTerms []api.PodAffinityTerm
|
||||
if len(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution) != 0 {
|
||||
podAntiAffinityTerms = affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution
|
||||
}
|
||||
// TODO: Uncomment this block when implement RequiredDuringSchedulingRequiredDuringExecution.
|
||||
//if len(affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution) != 0 {
|
||||
// podAntiAffinityTerms = append(podAntiAffinityTerms, affinity.PodAntiAffinity.RequiredDuringSchedulingRequiredDuringExecution...)
|
||||
//}
|
||||
for _, v := range podAntiAffinityTerms {
|
||||
if v.TopologyKey != kubeletapis.LabelHostname {
|
||||
return apierrors.NewForbidden(attributes.GetResource().GroupResource(), pod.Name, fmt.Errorf("affinity.PodAntiAffinity.RequiredDuringScheduling has TopologyKey %v but only key %v is allowed", v.TopologyKey, kubeletapis.LabelHostname))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,284 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package antiaffinity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
kubeletapis "k8s.io/kubernetes/pkg/kubelet/apis"
|
||||
)
|
||||
|
||||
// ensures the hard PodAntiAffinity is denied if it defines TopologyKey other than kubernetes.io/hostname.
|
||||
// TODO: Add test case "invalid topologyKey in requiredDuringSchedulingRequiredDuringExecution then admission fails"
|
||||
// after RequiredDuringSchedulingRequiredDuringExecution is implemented.
|
||||
func TestInterPodAffinityAdmission(t *testing.T) {
|
||||
handler := NewInterPodAntiAffinity()
|
||||
pod := api.Pod{
|
||||
Spec: api.PodSpec{},
|
||||
}
|
||||
tests := []struct {
|
||||
affinity *api.Affinity
|
||||
errorExpected bool
|
||||
}{
|
||||
// empty affinity its success.
|
||||
{
|
||||
affinity: &api.Affinity{},
|
||||
errorExpected: false,
|
||||
},
|
||||
// what ever topologyKey in preferredDuringSchedulingIgnoredDuringExecution, the admission should success.
|
||||
{
|
||||
affinity: &api.Affinity{
|
||||
PodAntiAffinity: &api.PodAntiAffinity{
|
||||
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
|
||||
{
|
||||
Weight: 5,
|
||||
PodAffinityTerm: api.PodAffinityTerm{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: "az",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errorExpected: false,
|
||||
},
|
||||
// valid topologyKey in requiredDuringSchedulingIgnoredDuringExecution,
|
||||
// plus any topologyKey in preferredDuringSchedulingIgnoredDuringExecution, then admission success.
|
||||
{
|
||||
affinity: &api.Affinity{
|
||||
PodAntiAffinity: &api.PodAntiAffinity{
|
||||
PreferredDuringSchedulingIgnoredDuringExecution: []api.WeightedPodAffinityTerm{
|
||||
{
|
||||
Weight: 5,
|
||||
PodAffinityTerm: api.PodAffinityTerm{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: "az",
|
||||
},
|
||||
},
|
||||
},
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: kubeletapis.LabelHostname,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errorExpected: false,
|
||||
},
|
||||
// valid topologyKey in requiredDuringSchedulingIgnoredDuringExecution then admission success.
|
||||
{
|
||||
affinity: &api.Affinity{
|
||||
PodAntiAffinity: &api.PodAntiAffinity{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: kubeletapis.LabelHostname,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errorExpected: false,
|
||||
},
|
||||
// invalid topologyKey in requiredDuringSchedulingIgnoredDuringExecution then admission fails.
|
||||
{
|
||||
affinity: &api.Affinity{
|
||||
PodAntiAffinity: &api.PodAntiAffinity{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: " zone ",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errorExpected: true,
|
||||
},
|
||||
// list of requiredDuringSchedulingIgnoredDuringExecution middle element topologyKey is not valid.
|
||||
{
|
||||
affinity: &api.Affinity{
|
||||
PodAntiAffinity: &api.PodAntiAffinity{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution: []api.PodAffinityTerm{
|
||||
{
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: kubeletapis.LabelHostname,
|
||||
}, {
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: " zone ",
|
||||
}, {
|
||||
LabelSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: "security",
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
Values: []string{"S2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
TopologyKey: kubeletapis.LabelHostname,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
errorExpected: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
pod.Spec.Affinity = test.affinity
|
||||
err := handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", false, nil))
|
||||
|
||||
if test.errorExpected && err == nil {
|
||||
t.Errorf("Expected error for Anti Affinity %+v but did not get an error", test.affinity)
|
||||
}
|
||||
|
||||
if !test.errorExpected && err != nil {
|
||||
t.Errorf("Unexpected error %v for AntiAffinity %+v", err, test.affinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestHandles(t *testing.T) {
|
||||
handler := NewInterPodAntiAffinity()
|
||||
tests := map[admission.Operation]bool{
|
||||
admission.Update: true,
|
||||
admission.Create: true,
|
||||
admission.Delete: false,
|
||||
admission.Connect: false,
|
||||
}
|
||||
for op, expected := range tests {
|
||||
result := handler.Handles(op)
|
||||
if result != expected {
|
||||
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOtherResources ensures that this admission controller is a no-op for other resources,
|
||||
// subresources, and non-pods.
|
||||
func TestOtherResources(t *testing.T) {
|
||||
namespace := "testnamespace"
|
||||
name := "testname"
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
kind string
|
||||
resource string
|
||||
subresource string
|
||||
object runtime.Object
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "non-pod resource",
|
||||
kind: "Foo",
|
||||
resource: "foos",
|
||||
object: pod,
|
||||
},
|
||||
{
|
||||
name: "pod subresource",
|
||||
kind: "Pod",
|
||||
resource: "pods",
|
||||
subresource: "eviction",
|
||||
object: pod,
|
||||
},
|
||||
{
|
||||
name: "non-pod object",
|
||||
kind: "Pod",
|
||||
resource: "pods",
|
||||
object: &api.Service{},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
handler := &Plugin{}
|
||||
|
||||
err := handler.Validate(admission.NewAttributesRecord(tc.object, nil, api.Kind(tc.kind).WithVersion("version"), namespace, name, api.Resource(tc.resource).WithVersion("version"), tc.subresource, admission.Create, false, nil))
|
||||
|
||||
if tc.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("%s: unexpected nil error", tc.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error: %v", tc.name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// LimitPodHardAntiAffinityTopology admission controller rejects any pod
|
||||
// that specifies "hard" (RequiredDuringScheduling) anti-affinity
|
||||
// with a TopologyKey other than kubeletapis.LabelHostname.
|
||||
// Because anti-affinity is symmetric, without this admission controller,
|
||||
// a user could maliciously or accidentally specify that their pod (once it has scheduled)
|
||||
// should block other pods from scheduling into the same zone or some other large topology,
|
||||
// essentially DoSing the cluster.
|
||||
// In the future we will address this problem more fully by using quota and priority,
|
||||
// but for now this admission controller provides a simple protection,
|
||||
// on the assumption that the only legitimate use of hard pod anti-affinity
|
||||
// is to exclude other pods from the same node.
|
||||
package antiaffinity // import "k8s.io/kubernetes/plugin/pkg/admission/antiaffinity"
|
|
@ -1,40 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/deny",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 deny
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "AlwaysDeny"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewAlwaysDeny(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// alwaysDeny is an implementation of admission.Interface which always says no to an admission request.
|
||||
type alwaysDeny struct{}
|
||||
|
||||
var _ admission.MutationInterface = alwaysDeny{}
|
||||
var _ admission.ValidationInterface = alwaysDeny{}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (alwaysDeny) Admit(a admission.Attributes) (err error) {
|
||||
return admission.NewForbidden(a, errors.New("admission control is denying all modifications"))
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate.
|
||||
func (alwaysDeny) Validate(a admission.Attributes) (err error) {
|
||||
return admission.NewForbidden(a, errors.New("admission control is denying all modifications"))
|
||||
}
|
||||
|
||||
// Handles returns true if this admission controller can handle the given operation
|
||||
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
|
||||
func (alwaysDeny) Handles(operation admission.Operation) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewAlwaysDeny creates an always deny admission handler
|
||||
func NewAlwaysDeny() admission.Interface {
|
||||
// DEPRECATED: AlwaysDeny denys all admission request, it is no use.
|
||||
klog.Warningf("%s admission controller is deprecated. "+
|
||||
"Please remove this controller from your configuration files and scripts.", PluginName)
|
||||
return new(alwaysDeny)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 deny
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func TestAdmission(t *testing.T) {
|
||||
handler := NewAlwaysDeny()
|
||||
err := handler.(*alwaysDeny).Admit(admission.NewAttributesRecord(nil, nil, api.Kind("kind").WithVersion("version"), "namespace", "name", api.Resource("resource").WithVersion("version"), "subresource", admission.Create, false, nil))
|
||||
if err == nil {
|
||||
t.Error("Expected error returned from admission handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
handler := NewAlwaysDeny()
|
||||
tests := []admission.Operation{admission.Create, admission.Connect, admission.Update, admission.Delete}
|
||||
|
||||
for _, test := range tests {
|
||||
if !handler.Handles(test) {
|
||||
t.Errorf("Expected handling all operations, including: %v", test)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_test.go",
|
||||
"cache_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/clock:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/flowcontrol:go_default_library",
|
||||
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"cache.go",
|
||||
"clock.go",
|
||||
"config.go",
|
||||
"doc.go",
|
||||
"limitenforcer.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/install:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/flowcontrol:go_default_library",
|
||||
"//vendor/github.com/hashicorp/golang-lru:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "EventRateLimit"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName,
|
||||
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()
|
||||
}
|
||||
}
|
||||
return newEventRateLimit(configuration, realClock{})
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin implements an admission controller that can enforce event rate limits
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
// limitEnforcers is the collection of limit enforcers. There is one limit enforcer for each
|
||||
// active limit type. As there are 4 limit types, the length of the array will be at most 4.
|
||||
// The array is read-only after construction.
|
||||
limitEnforcers []*limitEnforcer
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// newEventRateLimit configures an admission controller that can enforce event rate limits
|
||||
func newEventRateLimit(config *eventratelimitapi.Configuration, clock flowcontrol.Clock) (*Plugin, error) {
|
||||
limitEnforcers := make([]*limitEnforcer, 0, len(config.Limits))
|
||||
for _, limitConfig := range config.Limits {
|
||||
enforcer, err := newLimitEnforcer(limitConfig, clock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limitEnforcers = append(limitEnforcers, enforcer)
|
||||
}
|
||||
|
||||
eventRateLimitAdmission := &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
limitEnforcers: limitEnforcers,
|
||||
}
|
||||
|
||||
return eventRateLimitAdmission, nil
|
||||
}
|
||||
|
||||
// Validate makes admission decisions while enforcing event rate limits
|
||||
func (a *Plugin) Validate(attr admission.Attributes) (err error) {
|
||||
// ignore all operations that do not correspond to an Event kind
|
||||
if attr.GetKind().GroupKind() != api.Kind("Event") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ignore all requests that specify dry-run
|
||||
// because they don't correspond to any calls to etcd,
|
||||
// they should not be affected by the ratelimit
|
||||
if attr.IsDryRun() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errors []error
|
||||
// give each limit enforcer a chance to reject the event
|
||||
for _, enforcer := range a.limitEnforcers {
|
||||
if err := enforcer.accept(attr); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
|
||||
if aggregatedErr := utilerrors.NewAggregate(errors); aggregatedErr != nil {
|
||||
return apierrors.NewTooManyRequestsError(aggregatedErr.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,524 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/clock"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
qps = 1
|
||||
eventKind = "Event"
|
||||
nonEventKind = "NonEvent"
|
||||
)
|
||||
|
||||
// attributesForRequest generates the admission.Attributes that for the specified request
|
||||
func attributesForRequest(rq request) admission.Attributes {
|
||||
return admission.NewAttributesRecord(
|
||||
rq.event,
|
||||
nil,
|
||||
api.Kind(rq.kind).WithVersion("version"),
|
||||
rq.namespace,
|
||||
"name",
|
||||
api.Resource("resource").WithVersion("version"),
|
||||
"",
|
||||
admission.Create,
|
||||
rq.dryRun,
|
||||
&user.DefaultInfo{Name: rq.username})
|
||||
}
|
||||
|
||||
type request struct {
|
||||
kind string
|
||||
namespace string
|
||||
username string
|
||||
event *api.Event
|
||||
delay time.Duration
|
||||
accepted bool
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func newRequest(kind string) request {
|
||||
return request{
|
||||
kind: kind,
|
||||
accepted: true,
|
||||
}
|
||||
}
|
||||
|
||||
func newEventRequest() request {
|
||||
return newRequest(eventKind)
|
||||
}
|
||||
|
||||
func newNonEventRequest() request {
|
||||
return newRequest(nonEventKind)
|
||||
}
|
||||
|
||||
func (r request) withNamespace(namespace string) request {
|
||||
r.namespace = namespace
|
||||
return r
|
||||
}
|
||||
|
||||
func (r request) withEvent(event *api.Event) request {
|
||||
r.event = event
|
||||
return r
|
||||
}
|
||||
|
||||
func (r request) withEventComponent(component string) request {
|
||||
return r.withEvent(&api.Event{
|
||||
Source: api.EventSource{
|
||||
Component: component,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (r request) withDryRun(dryRun bool) request {
|
||||
r.dryRun = dryRun
|
||||
return r
|
||||
}
|
||||
|
||||
func (r request) withUser(name string) request {
|
||||
r.username = name
|
||||
return r
|
||||
}
|
||||
|
||||
func (r request) blocked() request {
|
||||
r.accepted = false
|
||||
return r
|
||||
}
|
||||
|
||||
// withDelay will adjust the clock to simulate the specified delay, in seconds
|
||||
func (r request) withDelay(delayInSeconds int) request {
|
||||
r.delay = time.Duration(delayInSeconds) * time.Second
|
||||
return r
|
||||
}
|
||||
|
||||
// createSourceAndObjectKeyInclusionRequests creates a series of requests that can be used
|
||||
// to test that a particular part of the event is included in the source+object key
|
||||
func createSourceAndObjectKeyInclusionRequests(eventFactory func(label string) *api.Event) []request {
|
||||
return []request{
|
||||
newEventRequest().withEvent(eventFactory("A")),
|
||||
newEventRequest().withEvent(eventFactory("A")).blocked(),
|
||||
newEventRequest().withEvent(eventFactory("B")),
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventRateLimiting(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
serverBurst int32
|
||||
namespaceBurst int32
|
||||
namespaceCacheSize int32
|
||||
sourceAndObjectBurst int32
|
||||
sourceAndObjectCacheSize int32
|
||||
userBurst int32
|
||||
userCacheSize int32
|
||||
requests []request
|
||||
}{
|
||||
{
|
||||
name: "event not blocked when tokens available",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-event not blocked",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newNonEventRequest(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event blocked after tokens exhausted",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest().blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event not blocked by dry-run requests",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest().withDryRun(true),
|
||||
newEventRequest().withDryRun(true),
|
||||
newEventRequest().withDryRun(true),
|
||||
newEventRequest().withDryRun(true),
|
||||
newEventRequest(),
|
||||
newEventRequest().blocked(),
|
||||
newEventRequest().withDryRun(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-event not blocked after tokens exhausted",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newNonEventRequest(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-events should not count against limit",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newNonEventRequest(),
|
||||
newEventRequest(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event accepted after token refill",
|
||||
serverBurst: 3,
|
||||
requests: []request{
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest(),
|
||||
newEventRequest().blocked(),
|
||||
newEventRequest().withDelay(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event blocked by namespace limits",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A").blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event from other namespace not blocked",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "events from other namespaces should not count against limit",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event accepted after namespace token refill",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A").blocked(),
|
||||
newEventRequest().withNamespace("A").withDelay(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event from other namespaces should not clear namespace limits",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
newEventRequest().withNamespace("A").blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "namespace limits from lru namespace should clear when cache size exceeded",
|
||||
serverBurst: 100,
|
||||
namespaceBurst: 3,
|
||||
namespaceCacheSize: 2,
|
||||
requests: []request{
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
newEventRequest().withNamespace("B"),
|
||||
newEventRequest().withNamespace("A"),
|
||||
newEventRequest().withNamespace("B").blocked(),
|
||||
newEventRequest().withNamespace("A").blocked(),
|
||||
// This should clear out namespace B from the lru cache
|
||||
newEventRequest().withNamespace("C"),
|
||||
newEventRequest().withNamespace("A").blocked(),
|
||||
newEventRequest().withNamespace("B"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event blocked by source+object limits",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A").blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event from other source+object not blocked",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "events from other source+object should not count against limit",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event accepted after source+object token refill",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A").blocked(),
|
||||
newEventRequest().withEventComponent("A").withDelay(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event from other source+object should not clear source+object limits",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
newEventRequest().withEventComponent("A").blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source+object limits from lru source+object should clear when cache size exceeded",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 3,
|
||||
sourceAndObjectCacheSize: 2,
|
||||
requests: []request{
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
newEventRequest().withEventComponent("A"),
|
||||
newEventRequest().withEventComponent("B").blocked(),
|
||||
newEventRequest().withEventComponent("A").blocked(),
|
||||
// This should clear out component B from the lru cache
|
||||
newEventRequest().withEventComponent("C"),
|
||||
newEventRequest().withEventComponent("A").blocked(),
|
||||
newEventRequest().withEventComponent("B"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "source host should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{Source: api.EventSource{Host: label}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "involved object kind should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{InvolvedObject: api.ObjectReference{Kind: label}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "involved object namespace should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{InvolvedObject: api.ObjectReference{Namespace: label}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "involved object name should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{InvolvedObject: api.ObjectReference{Name: label}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "involved object UID should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{InvolvedObject: api.ObjectReference{UID: types.UID(label)}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "involved object APIVersion should be included in source+object key",
|
||||
serverBurst: 100,
|
||||
sourceAndObjectBurst: 1,
|
||||
sourceAndObjectCacheSize: 10,
|
||||
requests: createSourceAndObjectKeyInclusionRequests(func(label string) *api.Event {
|
||||
return &api.Event{InvolvedObject: api.ObjectReference{APIVersion: label}}
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "event blocked by user limits",
|
||||
userBurst: 3,
|
||||
userCacheSize: 10,
|
||||
requests: []request{
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A").blocked(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "event from other user not blocked",
|
||||
requests: []request{
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("B"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "events from other user should not count against limit",
|
||||
requests: []request{
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("A"),
|
||||
newEventRequest().withUser("B"),
|
||||
newEventRequest().withUser("A"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
clock := clock.NewFakeClock(time.Now())
|
||||
config := &eventratelimitapi.Configuration{}
|
||||
if tc.serverBurst > 0 {
|
||||
serverLimit := eventratelimitapi.Limit{
|
||||
Type: eventratelimitapi.ServerLimitType,
|
||||
QPS: qps,
|
||||
Burst: tc.serverBurst,
|
||||
}
|
||||
config.Limits = append(config.Limits, serverLimit)
|
||||
}
|
||||
if tc.namespaceBurst > 0 {
|
||||
namespaceLimit := eventratelimitapi.Limit{
|
||||
Type: eventratelimitapi.NamespaceLimitType,
|
||||
Burst: tc.namespaceBurst,
|
||||
QPS: qps,
|
||||
CacheSize: tc.namespaceCacheSize,
|
||||
}
|
||||
config.Limits = append(config.Limits, namespaceLimit)
|
||||
}
|
||||
if tc.userBurst > 0 {
|
||||
userLimit := eventratelimitapi.Limit{
|
||||
Type: eventratelimitapi.UserLimitType,
|
||||
Burst: tc.userBurst,
|
||||
QPS: qps,
|
||||
CacheSize: tc.userCacheSize,
|
||||
}
|
||||
config.Limits = append(config.Limits, userLimit)
|
||||
}
|
||||
if tc.sourceAndObjectBurst > 0 {
|
||||
sourceAndObjectLimit := eventratelimitapi.Limit{
|
||||
Type: eventratelimitapi.SourceAndObjectLimitType,
|
||||
Burst: tc.sourceAndObjectBurst,
|
||||
QPS: qps,
|
||||
CacheSize: tc.sourceAndObjectCacheSize,
|
||||
}
|
||||
config.Limits = append(config.Limits, sourceAndObjectLimit)
|
||||
}
|
||||
eventratelimit, err := newEventRateLimit(config, clock)
|
||||
if err != nil {
|
||||
t.Fatalf("%v: Could not create EventRateLimit: %v", tc.name, err)
|
||||
}
|
||||
|
||||
for rqIndex, rq := range tc.requests {
|
||||
if rq.delay > 0 {
|
||||
clock.Step(rq.delay)
|
||||
}
|
||||
attributes := attributesForRequest(rq)
|
||||
err = eventratelimit.Validate(attributes)
|
||||
if rq.accepted != (err == nil) {
|
||||
expectedAction := "admitted"
|
||||
if !rq.accepted {
|
||||
expectedAction = "blocked"
|
||||
}
|
||||
t.Fatalf("%v: Request %v should have been %v: %v", tc.name, rqIndex, expectedAction, err)
|
||||
}
|
||||
if err != nil {
|
||||
statusErr, ok := err.(*errors.StatusError)
|
||||
if ok && statusErr.ErrStatus.Code != errors.StatusTooManyRequests {
|
||||
t.Fatalf("%v: Request %v should yield a 429 response: %v", tc.name, rqIndex, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
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",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/install:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:all-srcs",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,7 +0,0 @@
|
|||
reviewers:
|
||||
- deads2k
|
||||
- derekwaynecarr
|
||||
approvers:
|
||||
- deads2k
|
||||
- derekwaynecarr
|
||||
- smarterclayton
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
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
|
||||
|
||||
package eventratelimit // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
|
@ -1,31 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["install.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/install",
|
||||
deps = [
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
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/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
internalapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
versionedapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"
|
||||
)
|
||||
|
||||
// Install registers the API group and adds types to a scheme
|
||||
func Install(scheme *runtime.Scheme) {
|
||||
utilruntime.Must(internalapi.AddToScheme(scheme))
|
||||
utilruntime.Must(versionedapi.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(versionedapi.SchemeGroupVersion))
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
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 = "eventratelimit.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
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// LimitType is the type of the limit (e.g., per-namespace)
|
||||
type LimitType string
|
||||
|
||||
const (
|
||||
// ServerLimitType is a type of limit where there is one bucket shared by
|
||||
// all of the event queries received by the API Server.
|
||||
ServerLimitType LimitType = "Server"
|
||||
// NamespaceLimitType is a type of limit where there is one bucket used by
|
||||
// each namespace
|
||||
NamespaceLimitType LimitType = "Namespace"
|
||||
// UserLimitType is a type of limit where there is one bucket used by each
|
||||
// user
|
||||
UserLimitType LimitType = "User"
|
||||
// SourceAndObjectLimitType is a type of limit where there is one bucket used
|
||||
// by each combination of source and involved object of the event.
|
||||
SourceAndObjectLimitType LimitType = "SourceAndObject"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// Configuration provides configuration for the EventRateLimit admission
|
||||
// controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// limits are the limits to place on event queries received.
|
||||
// Limits can be placed on events received server-wide, per namespace,
|
||||
// per user, and per source+object.
|
||||
// At least one limit is required.
|
||||
Limits []Limit `json:"limits"`
|
||||
}
|
||||
|
||||
// Limit is the configuration for a particular limit type
|
||||
type Limit struct {
|
||||
// type is the type of limit to which this configuration applies
|
||||
Type LimitType `json:"type"`
|
||||
|
||||
// qps is the number of event queries per second that are allowed for this
|
||||
// type of limit. The qps and burst fields are used together to determine if
|
||||
// a particular event query is accepted. The qps determines how many queries
|
||||
// are accepted once the burst amount of queries has been exhausted.
|
||||
QPS int32 `json:"qps"`
|
||||
|
||||
// burst is the burst number of event queries that are allowed for this type
|
||||
// of limit. The qps and burst fields are used together to determine if a
|
||||
// particular event query is accepted. The burst determines the maximum size
|
||||
// of the allowance granted for a particular bucket. For example, if the burst
|
||||
// is 10 and the qps is 3, then the admission control will accept 10 queries
|
||||
// before blocking any queries. Every second, 3 more queries will be allowed.
|
||||
// If some of that allowance is not used, then it will roll over to the next
|
||||
// second, until the maximum allowance of 10 is reached.
|
||||
Burst int32 `json:"burst"`
|
||||
|
||||
// cacheSize is the size of the LRU cache for this type of limit. If a bucket
|
||||
// is evicted from the cache, then the allowance for that bucket is reset. If
|
||||
// more queries are later received for an evicted bucket, then that bucket
|
||||
// will re-enter the cache with a clean slate, giving that bucket a full
|
||||
// allowance of burst queries.
|
||||
//
|
||||
// The default cache size is 4096.
|
||||
//
|
||||
// If limitType is 'server', then cacheSize is ignored.
|
||||
// +optional
|
||||
CacheSize int32 `json:"cacheSize,omitempty"`
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
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",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1",
|
||||
deps = [
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,25 +0,0 @@
|
|||
/*
|
||||
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 {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
||||
|
||||
func SetDefaults_Configuration(obj *Configuration) {}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
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
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=eventratelimit.admission.k8s.io
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the API.
|
||||
package v1alpha1 // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
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 = "eventratelimit.admission.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
|
||||
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Configuration{},
|
||||
)
|
||||
return nil
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
// LimitType is the type of the limit (e.g., per-namespace)
|
||||
type LimitType string
|
||||
|
||||
const (
|
||||
// ServerLimitType is a type of limit where there is one bucket shared by
|
||||
// all of the event queries received by the API Server.
|
||||
ServerLimitType LimitType = "Server"
|
||||
// NamespaceLimitType is a type of limit where there is one bucket used by
|
||||
// each namespace
|
||||
NamespaceLimitType LimitType = "Namespace"
|
||||
// UserLimitType is a type of limit where there is one bucket used by each
|
||||
// user
|
||||
UserLimitType LimitType = "User"
|
||||
// SourceAndObjectLimitType is a type of limit where there is one bucket used
|
||||
// by each combination of source and involved object of the event.
|
||||
SourceAndObjectLimitType LimitType = "SourceAndObject"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// Configuration provides configuration for the EventRateLimit admission
|
||||
// controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// limits are the limits to place on event queries received.
|
||||
// Limits can be placed on events received server-wide, per namespace,
|
||||
// per user, and per source+object.
|
||||
// At least one limit is required.
|
||||
Limits []Limit `json:"limits"`
|
||||
}
|
||||
|
||||
// Limit is the configuration for a particular limit type
|
||||
type Limit struct {
|
||||
// type is the type of limit to which this configuration applies
|
||||
Type LimitType `json:"type"`
|
||||
|
||||
// qps is the number of event queries per second that are allowed for this
|
||||
// type of limit. The qps and burst fields are used together to determine if
|
||||
// a particular event query is accepted. The qps determines how many queries
|
||||
// are accepted once the burst amount of queries has been exhausted.
|
||||
QPS int32 `json:"qps"`
|
||||
|
||||
// burst is the burst number of event queries that are allowed for this type
|
||||
// of limit. The qps and burst fields are used together to determine if a
|
||||
// particular event query is accepted. The burst determines the maximum size
|
||||
// of the allowance granted for a particular bucket. For example, if the burst
|
||||
// is 10 and the qps is 3, then the admission control will accept 10 queries
|
||||
// before blocking any queries. Every second, 3 more queries will be allowed.
|
||||
// If some of that allowance is not used, then it will roll over to the next
|
||||
// second, until the maximum allowance of 10 is reached.
|
||||
Burst int32 `json:"burst"`
|
||||
|
||||
// cacheSize is the size of the LRU cache for this type of limit. If a bucket
|
||||
// is evicted from the cache, then the allowance for that bucket is reset. If
|
||||
// more queries are later received for an evicted bucket, then that bucket
|
||||
// will re-enter the cache with a clean slate, giving that bucket a full
|
||||
// allowance of burst queries.
|
||||
//
|
||||
// The default cache size is 4096.
|
||||
//
|
||||
// If limitType is 'server', then cacheSize is ignored.
|
||||
// +optional
|
||||
CacheSize int32 `json:"cacheSize,omitempty"`
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by conversion-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
unsafe "unsafe"
|
||||
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
eventratelimit "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(RegisterConversions)
|
||||
}
|
||||
|
||||
// RegisterConversions adds conversion functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterConversions(s *runtime.Scheme) error {
|
||||
if err := s.AddGeneratedConversionFunc((*Configuration)(nil), (*eventratelimit.Configuration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_Configuration_To_eventratelimit_Configuration(a.(*Configuration), b.(*eventratelimit.Configuration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*eventratelimit.Configuration)(nil), (*Configuration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_eventratelimit_Configuration_To_v1alpha1_Configuration(a.(*eventratelimit.Configuration), b.(*Configuration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*Limit)(nil), (*eventratelimit.Limit)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_Limit_To_eventratelimit_Limit(a.(*Limit), b.(*eventratelimit.Limit), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*eventratelimit.Limit)(nil), (*Limit)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_eventratelimit_Limit_To_v1alpha1_Limit(a.(*eventratelimit.Limit), b.(*Limit), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_Configuration_To_eventratelimit_Configuration(in *Configuration, out *eventratelimit.Configuration, s conversion.Scope) error {
|
||||
out.Limits = *(*[]eventratelimit.Limit)(unsafe.Pointer(&in.Limits))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_Configuration_To_eventratelimit_Configuration is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_Configuration_To_eventratelimit_Configuration(in *Configuration, out *eventratelimit.Configuration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_Configuration_To_eventratelimit_Configuration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_eventratelimit_Configuration_To_v1alpha1_Configuration(in *eventratelimit.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
out.Limits = *(*[]Limit)(unsafe.Pointer(&in.Limits))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_eventratelimit_Configuration_To_v1alpha1_Configuration is an autogenerated conversion function.
|
||||
func Convert_eventratelimit_Configuration_To_v1alpha1_Configuration(in *eventratelimit.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
return autoConvert_eventratelimit_Configuration_To_v1alpha1_Configuration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_Limit_To_eventratelimit_Limit(in *Limit, out *eventratelimit.Limit, s conversion.Scope) error {
|
||||
out.Type = eventratelimit.LimitType(in.Type)
|
||||
out.QPS = in.QPS
|
||||
out.Burst = in.Burst
|
||||
out.CacheSize = in.CacheSize
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_Limit_To_eventratelimit_Limit is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_Limit_To_eventratelimit_Limit(in *Limit, out *eventratelimit.Limit, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_Limit_To_eventratelimit_Limit(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_eventratelimit_Limit_To_v1alpha1_Limit(in *eventratelimit.Limit, out *Limit, s conversion.Scope) error {
|
||||
out.Type = LimitType(in.Type)
|
||||
out.QPS = in.QPS
|
||||
out.Burst = in.Burst
|
||||
out.CacheSize = in.CacheSize
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_eventratelimit_Limit_To_v1alpha1_Limit is an autogenerated conversion function.
|
||||
func Convert_eventratelimit_Limit_To_v1alpha1_Limit(in *eventratelimit.Limit, out *Limit, s conversion.Scope) error {
|
||||
return autoConvert_eventratelimit_Limit_To_v1alpha1_Limit(in, out, s)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Limits != nil {
|
||||
in, out := &in.Limits, &out.Limits
|
||||
*out = make([]Limit, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
|
||||
func (in *Configuration) DeepCopy() *Configuration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Configuration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Configuration) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Limit) DeepCopyInto(out *Limit) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Limit.
|
||||
func (in *Limit) DeepCopy() *Limit {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Limit)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["validation.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/validation",
|
||||
deps = [
|
||||
"//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
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"],
|
||||
embed = [":go_default_library"],
|
||||
deps = ["//plugin/pkg/admission/eventratelimit/apis/eventratelimit:go_default_library"],
|
||||
)
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
)
|
||||
|
||||
var limitTypes = map[eventratelimitapi.LimitType]bool{
|
||||
eventratelimitapi.ServerLimitType: true,
|
||||
eventratelimitapi.NamespaceLimitType: true,
|
||||
eventratelimitapi.UserLimitType: true,
|
||||
eventratelimitapi.SourceAndObjectLimitType: true,
|
||||
}
|
||||
|
||||
// ValidateConfiguration validates the configuration.
|
||||
func ValidateConfiguration(config *eventratelimitapi.Configuration) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
limitsPath := field.NewPath("limits")
|
||||
if len(config.Limits) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(limitsPath, config.Limits, "must not be empty"))
|
||||
}
|
||||
for i, limit := range config.Limits {
|
||||
idxPath := limitsPath.Index(i)
|
||||
if !limitTypes[limit.Type] {
|
||||
allowedValues := make([]string, len(limitTypes))
|
||||
i := 0
|
||||
for limitType := range limitTypes {
|
||||
allowedValues[i] = string(limitType)
|
||||
i++
|
||||
}
|
||||
allErrs = append(allErrs, field.NotSupported(idxPath.Child("type"), limit.Type, allowedValues))
|
||||
}
|
||||
if limit.Burst <= 0 {
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("burst"), limit.Burst, "must be positive"))
|
||||
}
|
||||
if limit.QPS <= 0 {
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("qps"), limit.QPS, "must be positive"))
|
||||
}
|
||||
if limit.Type != eventratelimitapi.ServerLimitType {
|
||||
if limit.CacheSize < 0 {
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("cacheSize"), limit.CacheSize, "must not be negative"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
)
|
||||
|
||||
func TestValidateConfiguration(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
config eventratelimitapi.Configuration
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
name: "valid server",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Server",
|
||||
Burst: 5,
|
||||
QPS: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "valid namespace",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Namespace",
|
||||
Burst: 10,
|
||||
QPS: 2,
|
||||
CacheSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "valid user",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "User",
|
||||
Burst: 10,
|
||||
QPS: 2,
|
||||
CacheSize: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "valid source+object",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "SourceAndObject",
|
||||
Burst: 5,
|
||||
QPS: 1,
|
||||
CacheSize: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "valid multiple",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Server",
|
||||
Burst: 5,
|
||||
QPS: 1,
|
||||
},
|
||||
{
|
||||
Type: "Namespace",
|
||||
Burst: 10,
|
||||
QPS: 2,
|
||||
CacheSize: 100,
|
||||
},
|
||||
{
|
||||
Type: "SourceAndObject",
|
||||
Burst: 25,
|
||||
QPS: 10,
|
||||
CacheSize: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
name: "missing limits",
|
||||
config: eventratelimitapi.Configuration{},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "missing type",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Burst: 25,
|
||||
QPS: 10,
|
||||
CacheSize: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "unknown-type",
|
||||
Burst: 25,
|
||||
QPS: 10,
|
||||
CacheSize: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "missing burst",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Server",
|
||||
QPS: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "missing qps",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Server",
|
||||
Burst: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
name: "negative cache size",
|
||||
config: eventratelimitapi.Configuration{
|
||||
Limits: []eventratelimitapi.Limit{
|
||||
{
|
||||
Type: "Namespace",
|
||||
Burst: 10,
|
||||
QPS: 2,
|
||||
CacheSize: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
errs := ValidateConfiguration(&tc.config)
|
||||
if e, a := tc.expectedResult, len(errs) == 0; e != a {
|
||||
if e {
|
||||
t.Errorf("%v: expected success: %v", tc.name, errs)
|
||||
} else {
|
||||
t.Errorf("%v: expected failure", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package eventratelimit
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Limits != nil {
|
||||
in, out := &in.Limits, &out.Limits
|
||||
*out = make([]Limit, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
|
||||
func (in *Configuration) DeepCopy() *Configuration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Configuration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Configuration) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Limit) DeepCopyInto(out *Limit) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Limit.
|
||||
func (in *Limit) DeepCopy() *Limit {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Limit)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/golang-lru"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
)
|
||||
|
||||
// cache is an interface for caching the limits of a particular type
|
||||
type cache interface {
|
||||
// get the rate limiter associated with the specified key
|
||||
get(key interface{}) flowcontrol.RateLimiter
|
||||
}
|
||||
|
||||
// singleCache is a cache that only stores a single, constant item
|
||||
type singleCache struct {
|
||||
// the single rate limiter held by the cache
|
||||
rateLimiter flowcontrol.RateLimiter
|
||||
}
|
||||
|
||||
func (c *singleCache) get(key interface{}) flowcontrol.RateLimiter {
|
||||
return c.rateLimiter
|
||||
}
|
||||
|
||||
// lruCache is a least-recently-used cache
|
||||
type lruCache struct {
|
||||
// factory to use to create new rate limiters
|
||||
rateLimiterFactory func() flowcontrol.RateLimiter
|
||||
// the actual LRU cache
|
||||
cache *lru.Cache
|
||||
}
|
||||
|
||||
func (c *lruCache) get(key interface{}) flowcontrol.RateLimiter {
|
||||
value, found := c.cache.Get(key)
|
||||
if !found {
|
||||
rateLimter := c.rateLimiterFactory()
|
||||
c.cache.Add(key, rateLimter)
|
||||
return rateLimter
|
||||
}
|
||||
return value.(flowcontrol.RateLimiter)
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
)
|
||||
|
||||
func TestSingleCache(t *testing.T) {
|
||||
rateLimiter := flowcontrol.NewTokenBucketRateLimiter(1., 1)
|
||||
cache := singleCache{
|
||||
rateLimiter: rateLimiter,
|
||||
}
|
||||
cases := []interface{}{nil, "key1", "key2"}
|
||||
for _, tc := range cases {
|
||||
actual := cache.get(tc)
|
||||
if e, a := rateLimiter, actual; e != a {
|
||||
t.Errorf("unexpected entry in cache for key %v: expected %v, got %v", tc, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache(t *testing.T) {
|
||||
rateLimiters := []flowcontrol.RateLimiter{
|
||||
flowcontrol.NewTokenBucketRateLimiter(1., 1),
|
||||
flowcontrol.NewTokenBucketRateLimiter(2., 2),
|
||||
flowcontrol.NewTokenBucketRateLimiter(3., 3),
|
||||
flowcontrol.NewTokenBucketRateLimiter(4., 4),
|
||||
}
|
||||
nextRateLimiter := 0
|
||||
rateLimiterFactory := func() flowcontrol.RateLimiter {
|
||||
rateLimiter := rateLimiters[nextRateLimiter]
|
||||
nextRateLimiter++
|
||||
return rateLimiter
|
||||
}
|
||||
underlyingCache, err := lru.New(2)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create LRU cache: %v", err)
|
||||
}
|
||||
cache := lruCache{
|
||||
rateLimiterFactory: rateLimiterFactory,
|
||||
cache: underlyingCache,
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
key int
|
||||
expected flowcontrol.RateLimiter
|
||||
}{
|
||||
{
|
||||
name: "first added",
|
||||
key: 0,
|
||||
expected: rateLimiters[0],
|
||||
},
|
||||
{
|
||||
name: "first obtained",
|
||||
key: 0,
|
||||
expected: rateLimiters[0],
|
||||
},
|
||||
{
|
||||
name: "second added",
|
||||
key: 1,
|
||||
expected: rateLimiters[1],
|
||||
},
|
||||
{
|
||||
name: "second obtained",
|
||||
key: 1,
|
||||
expected: rateLimiters[1],
|
||||
},
|
||||
{
|
||||
name: "first obtained second time",
|
||||
key: 0,
|
||||
expected: rateLimiters[0],
|
||||
},
|
||||
{
|
||||
name: "third added",
|
||||
key: 2,
|
||||
expected: rateLimiters[2],
|
||||
},
|
||||
{
|
||||
name: "third obtained",
|
||||
key: 2,
|
||||
expected: rateLimiters[2],
|
||||
},
|
||||
{
|
||||
name: "first obtained third time",
|
||||
key: 0,
|
||||
expected: rateLimiters[0],
|
||||
},
|
||||
{
|
||||
name: "second re-added after eviction",
|
||||
key: 1,
|
||||
expected: rateLimiters[3],
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
actual := cache.get(tc.key)
|
||||
if e, a := tc.expected, actual; e != a {
|
||||
t.Errorf("%v: unexpected entry in cache for key %v: expected %v, got %v", tc.name, tc.key, e, a)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// realClock implements flowcontrol.Clock in terms of standard time functions.
|
||||
type realClock struct{}
|
||||
|
||||
// Now is identical to time.Now.
|
||||
func (realClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// Sleep is identical to time.Sleep.
|
||||
func (realClock) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/install"
|
||||
eventratelimitv1alpha1 "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(scheme)
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the provided configuration.
|
||||
func LoadConfiguration(config io.Reader) (*eventratelimitapi.Configuration, error) {
|
||||
// if no config is provided, return a default configuration
|
||||
if config == nil {
|
||||
externalConfig := &eventratelimitv1alpha1.Configuration{}
|
||||
scheme.Default(externalConfig)
|
||||
internalConfig := &eventratelimitapi.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.(*eventratelimitapi.Configuration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type: %T", decodedObj)
|
||||
}
|
||||
return resourceQuotaConfiguration, nil
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit contains an admission controller that enforces a rate limit on events
|
||||
package eventratelimit // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit"
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
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 eventratelimit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
eventratelimitapi "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
|
||||
)
|
||||
|
||||
const (
|
||||
// cache size to use if the user did not specify a cache size
|
||||
defaultCacheSize = 4096
|
||||
)
|
||||
|
||||
// limitEnforcer enforces a single type of event rate limit, such as server, namespace, or source+object
|
||||
type limitEnforcer struct {
|
||||
// type of this limit
|
||||
limitType eventratelimitapi.LimitType
|
||||
// cache for holding the rate limiters
|
||||
cache cache
|
||||
// a keyFunc which is responsible for computing a single key based on input
|
||||
keyFunc func(admission.Attributes) string
|
||||
}
|
||||
|
||||
func newLimitEnforcer(config eventratelimitapi.Limit, clock flowcontrol.Clock) (*limitEnforcer, error) {
|
||||
rateLimiterFactory := func() flowcontrol.RateLimiter {
|
||||
return flowcontrol.NewTokenBucketRateLimiterWithClock(float32(config.QPS), int(config.Burst), clock)
|
||||
}
|
||||
|
||||
if config.Type == eventratelimitapi.ServerLimitType {
|
||||
return &limitEnforcer{
|
||||
limitType: config.Type,
|
||||
cache: &singleCache{
|
||||
rateLimiter: rateLimiterFactory(),
|
||||
},
|
||||
keyFunc: getServerKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
cacheSize := int(config.CacheSize)
|
||||
if cacheSize == 0 {
|
||||
cacheSize = defaultCacheSize
|
||||
}
|
||||
underlyingCache, err := lru.New(cacheSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create lru cache: %v", err)
|
||||
}
|
||||
cache := &lruCache{
|
||||
rateLimiterFactory: rateLimiterFactory,
|
||||
cache: underlyingCache,
|
||||
}
|
||||
|
||||
var keyFunc func(admission.Attributes) string
|
||||
switch t := config.Type; t {
|
||||
case eventratelimitapi.NamespaceLimitType:
|
||||
keyFunc = getNamespaceKey
|
||||
case eventratelimitapi.UserLimitType:
|
||||
keyFunc = getUserKey
|
||||
case eventratelimitapi.SourceAndObjectLimitType:
|
||||
keyFunc = getSourceAndObjectKey
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown event rate limit type: %v", t)
|
||||
}
|
||||
|
||||
return &limitEnforcer{
|
||||
limitType: config.Type,
|
||||
cache: cache,
|
||||
keyFunc: keyFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (enforcer *limitEnforcer) accept(attr admission.Attributes) error {
|
||||
key := enforcer.keyFunc(attr)
|
||||
rateLimiter := enforcer.cache.get(key)
|
||||
|
||||
// ensure we have available rate
|
||||
allow := rateLimiter.TryAccept()
|
||||
|
||||
if !allow {
|
||||
return fmt.Errorf("limit reached on type %v for key %v", enforcer.limitType, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServerKey(attr admission.Attributes) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// getNamespaceKey returns a cache key that is based on the namespace of the event request
|
||||
func getNamespaceKey(attr admission.Attributes) string {
|
||||
return attr.GetNamespace()
|
||||
}
|
||||
|
||||
// getUserKey returns a cache key that is based on the user of the event request
|
||||
func getUserKey(attr admission.Attributes) string {
|
||||
userInfo := attr.GetUserInfo()
|
||||
if userInfo == nil {
|
||||
return ""
|
||||
}
|
||||
return userInfo.GetName()
|
||||
}
|
||||
|
||||
// getSourceAndObjectKey returns a cache key that is based on the source+object of the event
|
||||
func getSourceAndObjectKey(attr admission.Attributes) string {
|
||||
object := attr.GetObject()
|
||||
if object == nil {
|
||||
return ""
|
||||
}
|
||||
event, ok := object.(*api.Event)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return strings.Join([]string{
|
||||
event.Source.Component,
|
||||
event.Source.Host,
|
||||
event.InvolvedObject.Kind,
|
||||
event.InvolvedObject.Namespace,
|
||||
event.InvolvedObject.Name,
|
||||
string(event.InvolvedObject.UID),
|
||||
event.InvolvedObject.APIVersion,
|
||||
}, "")
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/exec",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 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 exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
// DenyEscalatingExec indicates name of admission plugin.
|
||||
DenyEscalatingExec = "DenyEscalatingExec"
|
||||
// DenyExecOnPrivileged indicates name of admission plugin.
|
||||
// Deprecated, should use DenyEscalatingExec instead.
|
||||
DenyExecOnPrivileged = "DenyExecOnPrivileged"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(DenyEscalatingExec, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewDenyEscalatingExec(), nil
|
||||
})
|
||||
|
||||
// This is for legacy support of the DenyExecOnPrivileged admission controller. Most
|
||||
// of the time DenyEscalatingExec should be preferred.
|
||||
plugins.Register(DenyExecOnPrivileged, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewDenyExecOnPrivileged(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// DenyExec is an implementation of admission.Interface which says no to a pod/exec on
|
||||
// a pod using host based configurations.
|
||||
type DenyExec struct {
|
||||
*admission.Handler
|
||||
client kubernetes.Interface
|
||||
|
||||
// these flags control which items will be checked to deny exec/attach
|
||||
hostNetwork bool
|
||||
hostIPC bool
|
||||
hostPID bool
|
||||
privileged bool
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &DenyExec{}
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&DenyExec{})
|
||||
|
||||
// NewDenyExecOnPrivileged creates a new admission controller that is only checking the privileged
|
||||
// option. This is for legacy support of the DenyExecOnPrivileged admission controller.
|
||||
// Most of the time NewDenyEscalatingExec should be preferred.
|
||||
func NewDenyExecOnPrivileged() *DenyExec {
|
||||
return &DenyExec{
|
||||
Handler: admission.NewHandler(admission.Connect),
|
||||
hostNetwork: false,
|
||||
hostIPC: false,
|
||||
hostPID: false,
|
||||
privileged: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDenyEscalatingExec creates a new admission controller that denies an exec operation on a pod
|
||||
// using host based configurations.
|
||||
func NewDenyEscalatingExec() *DenyExec {
|
||||
return &DenyExec{
|
||||
Handler: admission.NewHandler(admission.Connect),
|
||||
hostNetwork: true,
|
||||
hostIPC: true,
|
||||
hostPID: true,
|
||||
privileged: true,
|
||||
}
|
||||
}
|
||||
|
||||
// SetExternalKubeClientSet implements the WantsInternalKubeClientSet interface.
|
||||
func (d *DenyExec) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||
d.client = client
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (d *DenyExec) ValidateInitialization() error {
|
||||
if d.client == nil {
|
||||
return fmt.Errorf("missing client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes
|
||||
func (d *DenyExec) Validate(a admission.Attributes) (err error) {
|
||||
path := a.GetResource().Resource
|
||||
if subresource := a.GetSubresource(); subresource != "" {
|
||||
path = path + "/" + subresource
|
||||
}
|
||||
// Only handle exec or attach requests on pods
|
||||
if path != "pods/exec" && path != "pods/attach" {
|
||||
return nil
|
||||
}
|
||||
pod, err := d.client.CoreV1().Pods(a.GetNamespace()).Get(a.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
|
||||
if d.hostNetwork && pod.Spec.HostNetwork {
|
||||
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a container using host network"))
|
||||
}
|
||||
|
||||
if d.hostPID && pod.Spec.HostPID {
|
||||
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a container using host pid"))
|
||||
}
|
||||
|
||||
if d.hostIPC && pod.Spec.HostIPC {
|
||||
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a container using host ipc"))
|
||||
}
|
||||
|
||||
if d.privileged && isPrivileged(pod) {
|
||||
return admission.NewForbidden(a, fmt.Errorf("cannot exec into or attach to a privileged container"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isPrivileged will return true a pod has any privileged containers
|
||||
func isPrivileged(pod *corev1.Pod) bool {
|
||||
for _, c := range pod.Spec.InitContainers {
|
||||
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil {
|
||||
continue
|
||||
}
|
||||
if *c.SecurityContext.Privileged {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, c := range pod.Spec.Containers {
|
||||
if c.SecurityContext == nil || c.SecurityContext.Privileged == nil {
|
||||
continue
|
||||
}
|
||||
if *c.SecurityContext.Privileged {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,205 +0,0 @@
|
|||
/*
|
||||
Copyright 2015 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 exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// newAllowEscalatingExec returns `admission.Interface` that allows execution on
|
||||
// "hostIPC", "hostPID" and "privileged".
|
||||
func newAllowEscalatingExec() *DenyExec {
|
||||
return &DenyExec{
|
||||
Handler: admission.NewHandler(admission.Connect),
|
||||
hostIPC: false,
|
||||
hostPID: false,
|
||||
privileged: false,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmission(t *testing.T) {
|
||||
privPod := validPod("privileged")
|
||||
priv := true
|
||||
privPod.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{
|
||||
Privileged: &priv,
|
||||
}
|
||||
|
||||
hostPIDPod := validPod("hostPID")
|
||||
hostPIDPod.Spec.HostPID = true
|
||||
|
||||
hostIPCPod := validPod("hostIPC")
|
||||
hostIPCPod.Spec.HostIPC = true
|
||||
|
||||
testCases := map[string]struct {
|
||||
pod *corev1.Pod
|
||||
shouldAccept bool
|
||||
}{
|
||||
"priv": {
|
||||
shouldAccept: false,
|
||||
pod: privPod,
|
||||
},
|
||||
"hostPID": {
|
||||
shouldAccept: false,
|
||||
pod: hostPIDPod,
|
||||
},
|
||||
"hostIPC": {
|
||||
shouldAccept: false,
|
||||
pod: hostIPCPod,
|
||||
},
|
||||
"non privileged": {
|
||||
shouldAccept: true,
|
||||
pod: validPod("nonPrivileged"),
|
||||
},
|
||||
}
|
||||
|
||||
// Get the direct object though to allow testAdmission to inject the client
|
||||
handler := NewDenyEscalatingExec()
|
||||
|
||||
for _, tc := range testCases {
|
||||
testAdmission(t, tc.pod, handler, tc.shouldAccept)
|
||||
}
|
||||
|
||||
// run with a permissive config and all cases should pass
|
||||
handler = newAllowEscalatingExec()
|
||||
|
||||
for _, tc := range testCases {
|
||||
testAdmission(t, tc.pod, handler, true)
|
||||
}
|
||||
|
||||
// run against an init container
|
||||
handler = NewDenyEscalatingExec()
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc.pod.Spec.InitContainers = tc.pod.Spec.Containers
|
||||
tc.pod.Spec.Containers = nil
|
||||
testAdmission(t, tc.pod, handler, tc.shouldAccept)
|
||||
}
|
||||
|
||||
// run with a permissive config and all cases should pass
|
||||
handler = newAllowEscalatingExec()
|
||||
|
||||
for _, tc := range testCases {
|
||||
testAdmission(t, tc.pod, handler, true)
|
||||
}
|
||||
}
|
||||
|
||||
func testAdmission(t *testing.T, pod *corev1.Pod, handler *DenyExec, shouldAccept bool) {
|
||||
mockClient := &fake.Clientset{}
|
||||
mockClient.AddReactor("get", "pods", func(action core.Action) (bool, runtime.Object, error) {
|
||||
if action.(core.GetAction).GetName() == pod.Name {
|
||||
return true, pod, nil
|
||||
}
|
||||
t.Errorf("Unexpected API call: %#v", action)
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
handler.SetExternalKubeClientSet(mockClient)
|
||||
admission.ValidateInitialization(handler)
|
||||
|
||||
// pods/exec
|
||||
{
|
||||
err := handler.Validate(admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), "test", pod.Name, api.Resource("pods").WithVersion("version"), "exec", admission.Connect, false, nil))
|
||||
if shouldAccept && err != nil {
|
||||
t.Errorf("Unexpected error returned from admission handler: %v", err)
|
||||
}
|
||||
if !shouldAccept && err == nil {
|
||||
t.Errorf("An error was expected from the admission handler. Received nil")
|
||||
}
|
||||
}
|
||||
|
||||
// pods/attach
|
||||
{
|
||||
err := handler.Validate(admission.NewAttributesRecord(nil, nil, api.Kind("Pod").WithVersion("version"), "test", pod.Name, api.Resource("pods").WithVersion("version"), "attach", admission.Connect, false, nil))
|
||||
if shouldAccept && err != nil {
|
||||
t.Errorf("Unexpected error returned from admission handler: %v", err)
|
||||
}
|
||||
if !shouldAccept && err == nil {
|
||||
t.Errorf("An error was expected from the admission handler. Received nil")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test to ensure legacy admission controller works as expected.
|
||||
func TestDenyExecOnPrivileged(t *testing.T) {
|
||||
privPod := validPod("privileged")
|
||||
priv := true
|
||||
privPod.Spec.Containers[0].SecurityContext = &corev1.SecurityContext{
|
||||
Privileged: &priv,
|
||||
}
|
||||
|
||||
hostPIDPod := validPod("hostPID")
|
||||
hostPIDPod.Spec.HostPID = true
|
||||
|
||||
hostIPCPod := validPod("hostIPC")
|
||||
hostIPCPod.Spec.HostIPC = true
|
||||
|
||||
testCases := map[string]struct {
|
||||
pod *corev1.Pod
|
||||
shouldAccept bool
|
||||
}{
|
||||
"priv": {
|
||||
shouldAccept: false,
|
||||
pod: privPod,
|
||||
},
|
||||
"hostPID": {
|
||||
shouldAccept: true,
|
||||
pod: hostPIDPod,
|
||||
},
|
||||
"hostIPC": {
|
||||
shouldAccept: true,
|
||||
pod: hostIPCPod,
|
||||
},
|
||||
"non privileged": {
|
||||
shouldAccept: true,
|
||||
pod: validPod("nonPrivileged"),
|
||||
},
|
||||
}
|
||||
|
||||
// Get the direct object though to allow testAdmission to inject the client
|
||||
handler := NewDenyExecOnPrivileged()
|
||||
|
||||
for _, tc := range testCases {
|
||||
testAdmission(t, tc.pod, handler, tc.shouldAccept)
|
||||
}
|
||||
|
||||
// test init containers
|
||||
for _, tc := range testCases {
|
||||
tc.pod.Spec.InitContainers = tc.pod.Spec.Containers
|
||||
tc.pod.Spec.Containers = nil
|
||||
testAdmission(t, tc.pod, handler, tc.shouldAccept)
|
||||
}
|
||||
}
|
||||
|
||||
func validPod(name string) *corev1.Pod {
|
||||
return &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{Name: "ctr1", Image: "image"},
|
||||
{Name: "ctr2", Image: "image2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/extendedresourcetoleration",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/helper:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/helper:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
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 extendedresourcetoleration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/helper"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "ExtendedResourceToleration"
|
||||
|
||||
// Register is called by the apiserver to register the plugin factory.
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return newExtendedResourceToleration(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// newExtendedResourceToleration creates a new instance of the ExtendedResourceToleration admission controller.
|
||||
func newExtendedResourceToleration() *plugin {
|
||||
return &plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we are implementing the interface.
|
||||
var _ admission.MutationInterface = &plugin{}
|
||||
|
||||
type plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
// Admit updates the toleration of a pod based on the resources requested by it.
|
||||
// If an extended resource of name "example.com/device" is requested, it adds
|
||||
// a toleration with key "example.com/device", operator "Exists" and effect "NoSchedule".
|
||||
// The rationale for this is described in:
|
||||
// https://github.com/kubernetes/kubernetes/issues/55080
|
||||
func (p *plugin) Admit(attributes admission.Attributes) error {
|
||||
// Ignore all calls to subresources or resources other than pods.
|
||||
if len(attributes.GetSubresource()) != 0 || attributes.GetResource().GroupResource() != core.Resource("pods") {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod, ok := attributes.GetObject().(*core.Pod)
|
||||
if !ok {
|
||||
return errors.NewBadRequest(fmt.Sprintf("expected *core.Pod but got %T", attributes.GetObject()))
|
||||
}
|
||||
|
||||
resources := sets.String{}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
for resourceName := range container.Resources.Requests {
|
||||
if helper.IsExtendedResourceName(resourceName) {
|
||||
resources.Insert(string(resourceName))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
for resourceName := range container.Resources.Requests {
|
||||
if helper.IsExtendedResourceName(resourceName) {
|
||||
resources.Insert(string(resourceName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doing .List() so that we get a stable sorted list.
|
||||
// This allows us to test adding tolerations for multiple extended resources.
|
||||
for _, resource := range resources.List() {
|
||||
helper.AddOrUpdateTolerationInPod(pod, &core.Toleration{
|
||||
Key: resource,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,382 +0,0 @@
|
|||
/*
|
||||
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 extendedresourcetoleration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/helper"
|
||||
)
|
||||
|
||||
func TestAdmit(t *testing.T) {
|
||||
|
||||
plugin := newExtendedResourceToleration()
|
||||
|
||||
containerRequestingCPU := core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
containerRequestingMemory := core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceMemory: *resource.NewQuantity(2048, resource.DecimalSI),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
extendedResource1 := "example.com/device-ek"
|
||||
extendedResource2 := "example.com/device-do"
|
||||
|
||||
containerRequestingExtendedResource1 := core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceName(extendedResource1): *resource.NewQuantity(1, resource.DecimalSI),
|
||||
},
|
||||
},
|
||||
}
|
||||
containerRequestingExtendedResource2 := core.Container{
|
||||
Resources: core.ResourceRequirements{
|
||||
Requests: core.ResourceList{
|
||||
core.ResourceName(extendedResource2): *resource.NewQuantity(2, resource.DecimalSI),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
requestedPod core.Pod
|
||||
expectedPod core.Pod
|
||||
}{
|
||||
{
|
||||
description: "empty pod without any extended resources, expect no change in tolerations",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with container without any extended resources, expect no change in tolerations",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with init container without any extended resources, expect no change in tolerations",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingMemory,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingMemory,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with container with extended resource, expect toleration to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with init container with extended resource, expect toleration to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingExtendedResource2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingExtendedResource2,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource2,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with existing tolerations and container with extended resource, expect existing tolerations to be preserved and new toleration to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: "foo",
|
||||
Operator: core.TolerationOpEqual,
|
||||
Value: "bar",
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: "foo",
|
||||
Operator: core.TolerationOpEqual,
|
||||
Value: "bar",
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with multiple extended resources, expect multiple tolerations to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingExtendedResource2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
InitContainers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingExtendedResource2,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
// Note the order, it's sorted by the Key
|
||||
{
|
||||
Key: extendedResource2,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with container requesting extended resource and existing correct toleration, expect no change in tolerations",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with container requesting extended resource and existing toleration with the same key but different effect and value, expect existing tolerations to be preserved and new toleration to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpEqual,
|
||||
Value: "foo",
|
||||
Effect: core.TaintEffectNoExecute,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpEqual,
|
||||
Value: "foo",
|
||||
Effect: core.TaintEffectNoExecute,
|
||||
},
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "pod with wildcard toleration and container requesting extended resource, expect existing tolerations to be preserved and new toleration to be added",
|
||||
requestedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Operator: core.TolerationOpExists,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedPod: core.Pod{
|
||||
Spec: core.PodSpec{
|
||||
Containers: []core.Container{
|
||||
containerRequestingCPU,
|
||||
containerRequestingMemory,
|
||||
containerRequestingExtendedResource1,
|
||||
},
|
||||
Tolerations: []core.Toleration{
|
||||
{
|
||||
Operator: core.TolerationOpExists,
|
||||
},
|
||||
{
|
||||
Key: extendedResource1,
|
||||
Operator: core.TolerationOpExists,
|
||||
Effect: core.TaintEffectNoSchedule,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, test := range tests {
|
||||
err := plugin.Admit(admission.NewAttributesRecord(&test.requestedPod, nil, core.Kind("Pod").WithVersion("version"), "foo", "name", core.Resource("pods").WithVersion("version"), "", "ignored", false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("[%d: %s] unexpected error %v for pod %+v", i, test.description, err, test.requestedPod)
|
||||
}
|
||||
|
||||
if !helper.Semantic.DeepEqual(test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations) {
|
||||
t.Errorf("[%d: %s] expected %#v got %#v", i, test.description, test.expectedPod.Spec.Tolerations, test.requestedPod.Spec.Tolerations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
plugin := newExtendedResourceToleration()
|
||||
tests := map[admission.Operation]bool{
|
||||
admission.Create: true,
|
||||
admission.Update: true,
|
||||
admission.Delete: false,
|
||||
admission.Connect: false,
|
||||
}
|
||||
for op, expected := range tests {
|
||||
result := plugin.Handles(op)
|
||||
if result != expected {
|
||||
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["gc_admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/gc",
|
||||
deps = [
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/meta:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["gc_admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/kubeapiserver/admission:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/extensions/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/discovery/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/restmapper:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,284 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package gc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "OwnerReferencesPermissionEnforcement"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
// the pods/status endpoint is ignored by this plugin since old kubelets
|
||||
// corrupt them. the pod status strategy ensures status updates cannot mutate
|
||||
// ownerRef.
|
||||
whiteList := []whiteListItem{
|
||||
{
|
||||
groupResource: schema.GroupResource{Resource: "pods"},
|
||||
subresource: "status",
|
||||
},
|
||||
}
|
||||
return &gcPermissionsEnforcement{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
whiteList: whiteList,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// gcPermissionsEnforcement is an implementation of admission.Interface.
|
||||
type gcPermissionsEnforcement struct {
|
||||
*admission.Handler
|
||||
|
||||
authorizer authorizer.Authorizer
|
||||
|
||||
restMapper meta.RESTMapper
|
||||
|
||||
// items in this whitelist are ignored upon admission.
|
||||
// any item in this list must protect against ownerRef mutations
|
||||
// via strategy enforcement.
|
||||
whiteList []whiteListItem
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &gcPermissionsEnforcement{}
|
||||
|
||||
// whiteListItem describes an entry in a whitelist ignored by gc permission enforcement.
|
||||
type whiteListItem struct {
|
||||
groupResource schema.GroupResource
|
||||
subresource string
|
||||
}
|
||||
|
||||
// isWhiteListed returns true if the specified item is in the whitelist.
|
||||
func (a *gcPermissionsEnforcement) isWhiteListed(groupResource schema.GroupResource, subresource string) bool {
|
||||
for _, item := range a.whiteList {
|
||||
if item.groupResource == groupResource && item.subresource == subresource {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *gcPermissionsEnforcement) Validate(attributes admission.Attributes) (err error) {
|
||||
// // if the request is in the whitelist, we skip mutation checks for this resource.
|
||||
if a.isWhiteListed(attributes.GetResource().GroupResource(), attributes.GetSubresource()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we aren't changing owner references, then the edit is always allowed
|
||||
if !isChangingOwnerReference(attributes.GetObject(), attributes.GetOldObject()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if you are creating a thing, you should always be allowed to set an owner ref since you logically had the power
|
||||
// to never create it. We still need to check block owner deletion below, because the power to delete does not
|
||||
// imply the power to prevent deletion on other resources.
|
||||
if attributes.GetOperation() != admission.Create {
|
||||
deleteAttributes := authorizer.AttributesRecord{
|
||||
User: attributes.GetUserInfo(),
|
||||
Verb: "delete",
|
||||
Namespace: attributes.GetNamespace(),
|
||||
APIGroup: attributes.GetResource().Group,
|
||||
APIVersion: attributes.GetResource().Version,
|
||||
Resource: attributes.GetResource().Resource,
|
||||
Subresource: attributes.GetSubresource(),
|
||||
Name: attributes.GetName(),
|
||||
ResourceRequest: true,
|
||||
Path: "",
|
||||
}
|
||||
decision, reason, err := a.authorizer.Authorize(deleteAttributes)
|
||||
if decision != authorizer.DecisionAllow {
|
||||
return admission.NewForbidden(attributes, fmt.Errorf("cannot set an ownerRef on a resource you can't delete: %v, %v", reason, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Further check if the user is setting ownerReference.blockOwnerDeletion to
|
||||
// true. If so, only allows the change if the user has delete permission of
|
||||
// the _OWNER_
|
||||
newBlockingRefs := newBlockingOwnerDeletionRefs(attributes.GetObject(), attributes.GetOldObject())
|
||||
for _, ref := range newBlockingRefs {
|
||||
records, err := a.ownerRefToDeleteAttributeRecords(ref, attributes)
|
||||
if err != nil {
|
||||
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion in this case because cannot find RESTMapping for APIVersion %s Kind %s: %v", ref.APIVersion, ref.Kind, err))
|
||||
}
|
||||
// Multiple records are returned if ref.Kind could map to multiple
|
||||
// resources. User needs to have delete permission on all the
|
||||
// matched Resources.
|
||||
for _, record := range records {
|
||||
decision, reason, err := a.authorizer.Authorize(record)
|
||||
if decision != authorizer.DecisionAllow {
|
||||
return admission.NewForbidden(attributes, fmt.Errorf("cannot set blockOwnerDeletion if an ownerReference refers to a resource you can't set finalizers on: %v, %v", reason, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func isChangingOwnerReference(newObj, oldObj runtime.Object) bool {
|
||||
newMeta, err := meta.Accessor(newObj)
|
||||
if err != nil {
|
||||
// if we don't have objectmeta, we don't have the object reference
|
||||
return false
|
||||
}
|
||||
|
||||
if oldObj == nil {
|
||||
return len(newMeta.GetOwnerReferences()) > 0
|
||||
}
|
||||
oldMeta, err := meta.Accessor(oldObj)
|
||||
if err != nil {
|
||||
// if we don't have objectmeta, we don't have the object reference
|
||||
return false
|
||||
}
|
||||
|
||||
// compare the old and new. If they aren't the same, then we're trying to change an ownerRef
|
||||
oldOwners := oldMeta.GetOwnerReferences()
|
||||
newOwners := newMeta.GetOwnerReferences()
|
||||
if len(oldOwners) != len(newOwners) {
|
||||
return true
|
||||
}
|
||||
for i := range oldOwners {
|
||||
if !apiequality.Semantic.DeepEqual(oldOwners[i], newOwners[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Translates ref to a DeleteAttribute deleting the object referred by the ref.
|
||||
// OwnerReference only records the object kind, which might map to multiple
|
||||
// resources, so multiple DeleteAttribute might be returned.
|
||||
func (a *gcPermissionsEnforcement) ownerRefToDeleteAttributeRecords(ref metav1.OwnerReference, attributes admission.Attributes) ([]authorizer.AttributesRecord, error) {
|
||||
var ret []authorizer.AttributesRecord
|
||||
groupVersion, err := schema.ParseGroupVersion(ref.APIVersion)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
mappings, err := a.restMapper.RESTMappings(schema.GroupKind{Group: groupVersion.Group, Kind: ref.Kind}, groupVersion.Version)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
for _, mapping := range mappings {
|
||||
ar := authorizer.AttributesRecord{
|
||||
User: attributes.GetUserInfo(),
|
||||
Verb: "update",
|
||||
APIGroup: mapping.Resource.Group,
|
||||
APIVersion: mapping.Resource.Version,
|
||||
Resource: mapping.Resource.Resource,
|
||||
Subresource: "finalizers",
|
||||
Name: ref.Name,
|
||||
ResourceRequest: true,
|
||||
Path: "",
|
||||
}
|
||||
if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
|
||||
// if the owner is namespaced, it must be in the same namespace as the dependent is.
|
||||
ar.Namespace = attributes.GetNamespace()
|
||||
}
|
||||
ret = append(ret, ar)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// only keeps the blocking refs
|
||||
func blockingOwnerRefs(refs []metav1.OwnerReference) []metav1.OwnerReference {
|
||||
var ret []metav1.OwnerReference
|
||||
for _, ref := range refs {
|
||||
if ref.BlockOwnerDeletion != nil && *ref.BlockOwnerDeletion == true {
|
||||
ret = append(ret, ref)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func indexByUID(refs []metav1.OwnerReference) map[types.UID]metav1.OwnerReference {
|
||||
ret := make(map[types.UID]metav1.OwnerReference)
|
||||
for _, ref := range refs {
|
||||
ret[ref.UID] = ref
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Returns new blocking ownerReferences, and references whose blockOwnerDeletion
|
||||
// field is changed from nil or false to true.
|
||||
func newBlockingOwnerDeletionRefs(newObj, oldObj runtime.Object) []metav1.OwnerReference {
|
||||
newMeta, err := meta.Accessor(newObj)
|
||||
if err != nil {
|
||||
// if we don't have objectmeta, we don't have the object reference
|
||||
return nil
|
||||
}
|
||||
newRefs := newMeta.GetOwnerReferences()
|
||||
blockingNewRefs := blockingOwnerRefs(newRefs)
|
||||
if len(blockingNewRefs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if oldObj == nil {
|
||||
return blockingNewRefs
|
||||
}
|
||||
oldMeta, err := meta.Accessor(oldObj)
|
||||
if err != nil {
|
||||
// if we don't have objectmeta, treat it as if all the ownerReference are newly created
|
||||
return blockingNewRefs
|
||||
}
|
||||
|
||||
var ret []metav1.OwnerReference
|
||||
indexedOldRefs := indexByUID(oldMeta.GetOwnerReferences())
|
||||
for _, ref := range blockingNewRefs {
|
||||
oldRef, ok := indexedOldRefs[ref.UID]
|
||||
if !ok {
|
||||
// if ref is newly added, and it's blocking, then returns it.
|
||||
ret = append(ret, ref)
|
||||
continue
|
||||
}
|
||||
wasNotBlocking := oldRef.BlockOwnerDeletion == nil || *oldRef.BlockOwnerDeletion == false
|
||||
if wasNotBlocking {
|
||||
ret = append(ret, ref)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (a *gcPermissionsEnforcement) SetAuthorizer(authorizer authorizer.Authorizer) {
|
||||
a.authorizer = authorizer
|
||||
}
|
||||
|
||||
func (a *gcPermissionsEnforcement) SetRESTMapper(restMapper meta.RESTMapper) {
|
||||
a.restMapper = restMapper
|
||||
}
|
||||
|
||||
func (a *gcPermissionsEnforcement) ValidateInitialization() error {
|
||||
if a.authorizer == nil {
|
||||
return fmt.Errorf("missing authorizer")
|
||||
}
|
||||
if a.restMapper == nil {
|
||||
return fmt.Errorf("missing restMapper")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,619 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package gc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
extensionv1beta1 "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
fakediscovery "k8s.io/client-go/discovery/fake"
|
||||
"k8s.io/client-go/restmapper"
|
||||
coretesting "k8s.io/client-go/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
kubeadmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
|
||||
)
|
||||
|
||||
type fakeAuthorizer struct{}
|
||||
|
||||
func (fakeAuthorizer) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
username := a.GetUser().GetName()
|
||||
|
||||
if username == "non-deleter" {
|
||||
if a.GetVerb() == "delete" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
if a.GetVerb() == "update" && a.GetSubresource() == "finalizers" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
if username == "non-pod-deleter" {
|
||||
if a.GetVerb() == "delete" && a.GetResource() == "pods" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
if a.GetVerb() == "update" && a.GetResource() == "pods" && a.GetSubresource() == "finalizers" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
if username == "non-rc-deleter" {
|
||||
if a.GetVerb() == "delete" && a.GetResource() == "replicationcontrollers" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
if a.GetVerb() == "update" && a.GetResource() == "replicationcontrollers" && a.GetSubresource() == "finalizers" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
if username == "non-node-deleter" {
|
||||
if a.GetVerb() == "delete" && a.GetResource() == "nodes" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
if a.GetVerb() == "update" && a.GetResource() == "nodes" && a.GetSubresource() == "finalizers" {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
// newGCPermissionsEnforcement returns the admission controller configured for testing.
|
||||
func newGCPermissionsEnforcement() (*gcPermissionsEnforcement, error) {
|
||||
// the pods/status endpoint is ignored by this plugin since old kubelets
|
||||
// corrupt them. the pod status strategy ensures status updates cannot mutate
|
||||
// ownerRef.
|
||||
whiteList := []whiteListItem{
|
||||
{
|
||||
groupResource: schema.GroupResource{Resource: "pods"},
|
||||
subresource: "status",
|
||||
},
|
||||
}
|
||||
gcAdmit := &gcPermissionsEnforcement{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
whiteList: whiteList,
|
||||
}
|
||||
|
||||
genericPluginInitializer := initializer.New(nil, nil, fakeAuthorizer{}, nil)
|
||||
fakeDiscoveryClient := &fakediscovery.FakeDiscovery{Fake: &coretesting.Fake{}}
|
||||
fakeDiscoveryClient.Resources = []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: corev1.SchemeGroupVersion.String(),
|
||||
APIResources: []metav1.APIResource{
|
||||
{Name: "nodes", Namespaced: false, Kind: "Node"},
|
||||
{Name: "pods", Namespaced: true, Kind: "Pod"},
|
||||
{Name: "replicationcontrollers", Namespaced: true, Kind: "ReplicationController"},
|
||||
},
|
||||
},
|
||||
{
|
||||
GroupVersion: extensionv1beta1.SchemeGroupVersion.String(),
|
||||
APIResources: []metav1.APIResource{
|
||||
{Name: "daemonsets", Namespaced: true, Kind: "DaemonSet"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
restMapperRes, err := restmapper.GetAPIGroupResources(fakeDiscoveryClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error while constructing resource list from fake discovery client: %v", err)
|
||||
}
|
||||
restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)
|
||||
pluginInitializer := kubeadmission.NewPluginInitializer(nil, restMapper, nil)
|
||||
initializersChain := admission.PluginInitializers{}
|
||||
initializersChain = append(initializersChain, genericPluginInitializer)
|
||||
initializersChain = append(initializersChain, pluginInitializer)
|
||||
|
||||
initializersChain.Initialize(gcAdmit)
|
||||
return gcAdmit, nil
|
||||
}
|
||||
|
||||
func TestGCAdmission(t *testing.T) {
|
||||
expectNoError := func(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
expectCantSetOwnerRefError := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "cannot set an ownerRef on a resource you can't delete")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
resource schema.GroupVersionResource
|
||||
subresource string
|
||||
oldObj runtime.Object
|
||||
newObj runtime.Object
|
||||
|
||||
checkError func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "super-user, create, no objectref change",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, create, objectref change",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, create, no objectref change",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, create, objectref change",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, create, no objectref change",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, create, objectref change",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, create, objectref change, but not a pod",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("not-pods"),
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
|
||||
{
|
||||
name: "super-user, update, no objectref change",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, update, no objectref change two",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, update, objectref change",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, update, no objectref change",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, update, no objectref change two",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, update, objectref change",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectCantSetOwnerRefError,
|
||||
},
|
||||
{
|
||||
name: "non-deleter, update, objectref change two",
|
||||
username: "non-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}, {Name: "second"}}}},
|
||||
checkError: expectCantSetOwnerRefError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, update, no objectref change",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, update status, objectref change",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
subresource: "status",
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, update, objectref change",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectCantSetOwnerRefError,
|
||||
},
|
||||
{
|
||||
name: "non-pod-deleter, update, objectref change, but not a pod",
|
||||
username: "non-pod-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("not-pods"),
|
||||
oldObj: &api.Pod{},
|
||||
newObj: &api.Pod{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{Name: "first"}}}},
|
||||
checkError: expectNoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gcAdmit, err := newGCPermissionsEnforcement()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
operation := admission.Create
|
||||
if tc.oldObj != nil {
|
||||
operation = admission.Update
|
||||
}
|
||||
user := &user.DefaultInfo{Name: tc.username}
|
||||
attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", tc.resource, tc.subresource, operation, false, user)
|
||||
|
||||
err = gcAdmit.Validate(attributes)
|
||||
if !tc.checkError(err) {
|
||||
t.Errorf("unexpected err: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockOwnerDeletionAdmission(t *testing.T) {
|
||||
podWithOwnerRefs := func(refs ...metav1.OwnerReference) *api.Pod {
|
||||
var refSlice []metav1.OwnerReference
|
||||
refSlice = append(refSlice, refs...)
|
||||
|
||||
return &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
OwnerReferences: refSlice,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
getTrueVar := func() *bool {
|
||||
ret := true
|
||||
return &ret
|
||||
}
|
||||
|
||||
getFalseVar := func() *bool {
|
||||
ret := false
|
||||
return &ret
|
||||
}
|
||||
blockRC1 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc1",
|
||||
BlockOwnerDeletion: getTrueVar(),
|
||||
}
|
||||
blockRC2 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc2",
|
||||
BlockOwnerDeletion: getTrueVar(),
|
||||
}
|
||||
notBlockRC1 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc1",
|
||||
BlockOwnerDeletion: getFalseVar(),
|
||||
}
|
||||
notBlockRC2 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc2",
|
||||
BlockOwnerDeletion: getFalseVar(),
|
||||
}
|
||||
nilBlockRC1 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc1",
|
||||
}
|
||||
nilBlockRC2 := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "ReplicationController",
|
||||
Name: "rc2",
|
||||
}
|
||||
blockDS1 := metav1.OwnerReference{
|
||||
APIVersion: "extensions/v1beta1",
|
||||
Kind: "DaemonSet",
|
||||
Name: "ds1",
|
||||
BlockOwnerDeletion: getTrueVar(),
|
||||
}
|
||||
notBlockDS1 := metav1.OwnerReference{
|
||||
APIVersion: "extensions/v1beta1",
|
||||
Kind: "DaemonSet",
|
||||
Name: "ds1",
|
||||
BlockOwnerDeletion: getFalseVar(),
|
||||
}
|
||||
blockNode := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "Node",
|
||||
Name: "node1",
|
||||
BlockOwnerDeletion: getTrueVar(),
|
||||
}
|
||||
notBlockNode := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "Node",
|
||||
Name: "node",
|
||||
BlockOwnerDeletion: getFalseVar(),
|
||||
}
|
||||
nilBlockNode := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "Node",
|
||||
Name: "node",
|
||||
}
|
||||
|
||||
expectNoError := func(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
expectCantSetBlockOwnerDeletionError := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "cannot set blockOwnerDeletion if an ownerReference refers to a resource you can't set finalizers on")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
resource schema.GroupVersionResource
|
||||
subresource string
|
||||
oldObj runtime.Object
|
||||
newObj runtime.Object
|
||||
|
||||
checkError func(error) bool
|
||||
}{
|
||||
// cases for create
|
||||
{
|
||||
name: "super-user, create, no ownerReferences",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, create, all ownerReferences have blockOwnerDeletion=false",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(notBlockRC1, notBlockRC2),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, create, some ownerReferences have blockOwnerDeletion=true",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(blockRC1, blockRC2, blockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, create, no ownerReferences",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, create, all ownerReferences have blockOwnerDeletion=false or nil",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(notBlockRC1, nilBlockRC2),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-node-deleter, create, all ownerReferences have blockOwnerDeletion=false",
|
||||
username: "non-node-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(notBlockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, create, some ownerReferences have blockOwnerDeletion=true",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(blockRC1, notBlockRC2),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, create, some ownerReferences have blockOwnerDeletion=true, but are pointing to daemonset",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(blockDS1),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-node-deleter, create, some ownerReferences have blockOwnerDeletion=true",
|
||||
username: "non-node-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
newObj: podWithOwnerRefs(blockNode),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
// cases are for update
|
||||
{
|
||||
name: "super-user, update, no ownerReferences change blockOwnerDeletion",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(nilBlockRC1, nilBlockNode),
|
||||
newObj: podWithOwnerRefs(notBlockRC1, notBlockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, update, some ownerReferences change to blockOwnerDeletion=true",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(notBlockRC1, notBlockNode),
|
||||
newObj: podWithOwnerRefs(blockRC1, blockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "super-user, update, add new ownerReferences with blockOwnerDeletion=true",
|
||||
username: "super",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(),
|
||||
newObj: podWithOwnerRefs(blockRC1, blockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, no ownerReferences change blockOwnerDeletion",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(nilBlockRC1),
|
||||
newObj: podWithOwnerRefs(notBlockRC1),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=false to true",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(notBlockRC1),
|
||||
newObj: podWithOwnerRefs(blockRC1),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=nil to true",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(nilBlockRC1),
|
||||
newObj: podWithOwnerRefs(blockRC1),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
{
|
||||
name: "non-node-deleter, update, some ownerReferences change from blockOwnerDeletion=nil to true",
|
||||
username: "non-node-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(nilBlockNode),
|
||||
newObj: podWithOwnerRefs(blockNode),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, some ownerReferences change from blockOwnerDeletion=true to false",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(blockRC1),
|
||||
newObj: podWithOwnerRefs(notBlockRC1),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-node-deleter, update, some ownerReferences change from blockOwnerDeletion=true to false",
|
||||
username: "non-node-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(blockNode),
|
||||
newObj: podWithOwnerRefs(notBlockNode),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, some ownerReferences change blockOwnerDeletion, but all such references are to daemonset",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(notBlockDS1),
|
||||
newObj: podWithOwnerRefs(blockDS1),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=nil or false",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(),
|
||||
newObj: podWithOwnerRefs(notBlockRC1, nilBlockRC2),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=true",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(),
|
||||
newObj: podWithOwnerRefs(blockRC1),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
{
|
||||
name: "non-rc-deleter, update, add new ownerReferences with blockOwnerDeletion=true, but the references are to daemonset",
|
||||
username: "non-rc-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(),
|
||||
newObj: podWithOwnerRefs(blockDS1),
|
||||
checkError: expectNoError,
|
||||
},
|
||||
{
|
||||
name: "non-node-deleter, update, add ownerReferences with blockOwnerDeletion=true",
|
||||
username: "non-node-deleter",
|
||||
resource: api.SchemeGroupVersion.WithResource("pods"),
|
||||
oldObj: podWithOwnerRefs(),
|
||||
newObj: podWithOwnerRefs(blockNode),
|
||||
checkError: expectCantSetBlockOwnerDeletionError,
|
||||
},
|
||||
}
|
||||
gcAdmit, err := newGCPermissionsEnforcement()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
operation := admission.Create
|
||||
if tc.oldObj != nil {
|
||||
operation = admission.Update
|
||||
}
|
||||
user := &user.DefaultInfo{Name: tc.username}
|
||||
attributes := admission.NewAttributesRecord(tc.newObj, tc.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", tc.resource, tc.subresource, operation, false, user)
|
||||
|
||||
err := gcAdmit.Validate(attributes)
|
||||
if !tc.checkError(err) {
|
||||
t.Errorf("%v: unexpected err: %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 autoprovision
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "NamespaceAutoProvision"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewProvision(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Provision is an implementation of admission.Interface.
|
||||
// It looks at all incoming requests in a namespace context, and if the namespace does not exist, it creates one.
|
||||
// It is useful in deployments that do not want to restrict creation of a namespace prior to its usage.
|
||||
type Provision struct {
|
||||
*admission.Handler
|
||||
client kubernetes.Interface
|
||||
namespaceLister corev1listers.NamespaceLister
|
||||
}
|
||||
|
||||
var _ admission.MutationInterface = &Provision{}
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Provision{})
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Provision{})
|
||||
|
||||
// Admit makes an admission decision based on the request attributes
|
||||
func (p *Provision) Admit(a admission.Attributes) error {
|
||||
// Don't create a namespace if the request is for a dry-run.
|
||||
if a.IsDryRun() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do
|
||||
// if we're here, then the API server has found a route, which means that if we have a non-empty namespace
|
||||
// its a namespaced resource.
|
||||
if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") {
|
||||
return nil
|
||||
}
|
||||
// we need to wait for our caches to warm
|
||||
if !p.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
|
||||
_, err := p.namespaceLister.Get(a.GetNamespace())
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !errors.IsNotFound(err) {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: a.GetNamespace(),
|
||||
Namespace: "",
|
||||
},
|
||||
Status: corev1.NamespaceStatus{},
|
||||
}
|
||||
|
||||
_, err = p.client.Core().Namespaces().Create(namespace)
|
||||
if err != nil && !errors.IsAlreadyExists(err) {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewProvision creates a new namespace provision admission control handler
|
||||
func NewProvision() *Provision {
|
||||
return &Provision{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
}
|
||||
}
|
||||
|
||||
// SetExternalKubeClientSet implements the WantsExternalKubeClientSet interface.
|
||||
func (p *Provision) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||
p.client = client
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
||||
func (p *Provision) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
p.namespaceLister = namespaceInformer.Lister()
|
||||
p.SetReadyFunc(namespaceInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (p *Provision) ValidateInitialization() error {
|
||||
if p.namespaceLister == nil {
|
||||
return fmt.Errorf("missing namespaceLister")
|
||||
}
|
||||
if p.client == nil {
|
||||
return fmt.Errorf("missing client")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 autoprovision
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// newHandlerForTest returns the admission controller configured for testing.
|
||||
func newHandlerForTest(c clientset.Interface) (admission.MutationInterface, informers.SharedInformerFactory, error) {
|
||||
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
|
||||
handler := NewProvision()
|
||||
pluginInitializer := genericadmissioninitializer.New(c, f, nil, nil)
|
||||
pluginInitializer.Initialize(handler)
|
||||
err := admission.ValidateInitialization(handler)
|
||||
return handler, f, err
|
||||
}
|
||||
|
||||
// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces.
|
||||
func newMockClientForTest(namespaces []string) *fake.Clientset {
|
||||
mockClient := &fake.Clientset{}
|
||||
mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
|
||||
namespaceList := &corev1.NamespaceList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: fmt.Sprintf("%d", len(namespaces)),
|
||||
},
|
||||
}
|
||||
for i, ns := range namespaces {
|
||||
namespaceList.Items = append(namespaceList.Items, corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ns,
|
||||
ResourceVersion: fmt.Sprintf("%d", i),
|
||||
},
|
||||
})
|
||||
}
|
||||
return true, namespaceList, nil
|
||||
})
|
||||
return mockClient
|
||||
}
|
||||
|
||||
// newPod returns a new pod for the specified namespace
|
||||
func newPod(namespace string) api.Pod {
|
||||
return api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{{Name: "vol"}},
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// hasCreateNamespaceAction returns true if it has the create namespace action
|
||||
func hasCreateNamespaceAction(mockClient *fake.Clientset) bool {
|
||||
for _, action := range mockClient.Actions() {
|
||||
if action.GetVerb() == "create" && action.GetResource().Resource == "namespaces" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TestAdmission verifies a namespace is created on create requests for namespace managed resources
|
||||
func TestAdmission(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
if !hasCreateNamespaceAction(mockClient) {
|
||||
t.Errorf("expected create namespace action")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmissionNamespaceExists verifies that no client call is made when a namespace already exists
|
||||
func TestAdmissionNamespaceExists(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{namespace})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
if hasCreateNamespaceAction(mockClient) {
|
||||
t.Errorf("unexpected create namespace action")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmissionDryRun verifies that no client call is made on a dry run request
|
||||
func TestAdmissionDryRun(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, true, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
if hasCreateNamespaceAction(mockClient) {
|
||||
t.Errorf("unexpected create namespace action")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIgnoreAdmission validates that a request is ignored if its not a create
|
||||
func TestIgnoreAdmission(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
chainHandler := admission.NewChainHandler(handler)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = chainHandler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
if hasCreateNamespaceAction(mockClient) {
|
||||
t.Errorf("unexpected create namespace action")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdmissionWithLatentCache(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{})
|
||||
mockClient.AddReactor("create", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, errors.NewAlreadyExists(api.Resource("namespaces"), namespace)
|
||||
})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Admit(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
|
||||
if !hasCreateNamespaceAction(mockClient) {
|
||||
t.Errorf("expected create namespace action")
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 exists
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
informers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "NamespaceExists"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewExists(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Exists is an implementation of admission.Interface.
|
||||
// It rejects all incoming requests in a namespace context if the namespace does not exist.
|
||||
// It is useful in deployments that want to enforce pre-declaration of a Namespace resource.
|
||||
type Exists struct {
|
||||
*admission.Handler
|
||||
client kubernetes.Interface
|
||||
namespaceLister corev1listers.NamespaceLister
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Exists{}
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Exists{})
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Exists{})
|
||||
|
||||
// Validate makes an admission decision based on the request attributes
|
||||
func (e *Exists) Validate(a admission.Attributes) error {
|
||||
// if we're here, then we've already passed authentication, so we're allowed to do what we're trying to do
|
||||
// if we're here, then the API server has found a route, which means that if we have a non-empty namespace
|
||||
// its a namespaced resource.
|
||||
if len(a.GetNamespace()) == 0 || a.GetKind().GroupKind() == api.Kind("Namespace") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we need to wait for our caches to warm
|
||||
if !e.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
_, err := e.namespaceLister.Get(a.GetNamespace())
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.IsNotFound(err) {
|
||||
return errors.NewInternalError(err)
|
||||
}
|
||||
|
||||
// in case of latency in our caches, make a call direct to storage to verify that it truly exists or not
|
||||
_, err = e.client.Core().Namespaces().Get(a.GetNamespace(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return errors.NewInternalError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewExists creates a new namespace exists admission control handler
|
||||
func NewExists() *Exists {
|
||||
return &Exists{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
|
||||
}
|
||||
}
|
||||
|
||||
// SetExternalKubeClientSet implements the WantsExternalKubeClientSet interface.
|
||||
func (e *Exists) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||
e.client = client
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
||||
func (e *Exists) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
e.namespaceLister = namespaceInformer.Lister()
|
||||
e.SetReadyFunc(namespaceInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (e *Exists) ValidateInitialization() error {
|
||||
if e.namespaceLister == nil {
|
||||
return fmt.Errorf("missing namespaceLister")
|
||||
}
|
||||
if e.client == nil {
|
||||
return fmt.Errorf("missing client")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 exists
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
informers "k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// newHandlerForTest returns the admission controller configured for testing.
|
||||
func newHandlerForTest(c kubernetes.Interface) (admission.ValidationInterface, informers.SharedInformerFactory, error) {
|
||||
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
|
||||
handler := NewExists()
|
||||
pluginInitializer := genericadmissioninitializer.New(c, f, nil, nil)
|
||||
pluginInitializer.Initialize(handler)
|
||||
err := admission.ValidateInitialization(handler)
|
||||
return handler, f, err
|
||||
}
|
||||
|
||||
// newMockClientForTest creates a mock client that returns a client configured for the specified list of namespaces.
|
||||
func newMockClientForTest(namespaces []string) *fake.Clientset {
|
||||
mockClient := &fake.Clientset{}
|
||||
mockClient.AddReactor("list", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
|
||||
namespaceList := &corev1.NamespaceList{
|
||||
ListMeta: metav1.ListMeta{
|
||||
ResourceVersion: fmt.Sprintf("%d", len(namespaces)),
|
||||
},
|
||||
}
|
||||
for i, ns := range namespaces {
|
||||
namespaceList.Items = append(namespaceList.Items, corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ns,
|
||||
ResourceVersion: fmt.Sprintf("%d", i),
|
||||
},
|
||||
})
|
||||
}
|
||||
return true, namespaceList, nil
|
||||
})
|
||||
return mockClient
|
||||
}
|
||||
|
||||
// newPod returns a new pod for the specified namespace
|
||||
func newPod(namespace string) api.Pod {
|
||||
return api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: namespace},
|
||||
Spec: api.PodSpec{
|
||||
Volumes: []api.Volume{{Name: "vol"}},
|
||||
Containers: []api.Container{{Name: "ctr", Image: "image"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmissionNamespaceExists verifies pod is admitted only if namespace exists.
|
||||
func TestAdmissionNamespaceExists(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{namespace})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error returned from admission handler")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmissionNamespaceDoesNotExist verifies pod is not admitted if namespace does not exist.
|
||||
func TestAdmissionNamespaceDoesNotExist(t *testing.T) {
|
||||
namespace := "test"
|
||||
mockClient := newMockClientForTest([]string{})
|
||||
mockClient.AddReactor("get", "namespaces", func(action core.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, fmt.Errorf("nope, out of luck")
|
||||
})
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
pod := newPod(namespace)
|
||||
err = handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if err == nil {
|
||||
actions := ""
|
||||
for _, action := range mockClient.Actions() {
|
||||
actions = actions + action.GetVerb() + ":" + action.GetResource().Resource + ":" + action.GetSubresource() + ", "
|
||||
}
|
||||
t.Errorf("expected error returned from admission handler: %v", actions)
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podnodeselector",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/kubeapiserver/admission/util:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,264 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package podnodeselector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/util"
|
||||
)
|
||||
|
||||
// The annotation key scheduler.alpha.kubernetes.io/node-selector is for assigning
|
||||
// node selectors labels to namespaces
|
||||
var NamespaceNodeSelectors = []string{"scheduler.alpha.kubernetes.io/node-selector"}
|
||||
|
||||
const PluginName = "PodNodeSelector"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
// TODO move this to a versioned configuration file format.
|
||||
pluginConfig := readConfig(config)
|
||||
plugin := NewPodNodeSelector(pluginConfig.PodNodeSelectorPluginConfig)
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// podNodeSelector is an implementation of admission.Interface.
|
||||
type podNodeSelector struct {
|
||||
*admission.Handler
|
||||
client kubernetes.Interface
|
||||
namespaceLister corev1listers.NamespaceLister
|
||||
// global default node selector and namespace whitelists in a cluster.
|
||||
clusterNodeSelectors map[string]string
|
||||
}
|
||||
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&podNodeSelector{})
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&podNodeSelector{})
|
||||
|
||||
type pluginConfig struct {
|
||||
PodNodeSelectorPluginConfig map[string]string
|
||||
}
|
||||
|
||||
// readConfig reads default value of clusterDefaultNodeSelector
|
||||
// from the file provided with --admission-control-config-file
|
||||
// If the file is not supplied, it defaults to ""
|
||||
// The format in a file:
|
||||
// podNodeSelectorPluginConfig:
|
||||
// clusterDefaultNodeSelector: <node-selectors-labels>
|
||||
// namespace1: <node-selectors-labels>
|
||||
// namespace2: <node-selectors-labels>
|
||||
func readConfig(config io.Reader) *pluginConfig {
|
||||
defaultConfig := &pluginConfig{}
|
||||
if config == nil || reflect.ValueOf(config).IsNil() {
|
||||
return defaultConfig
|
||||
}
|
||||
d := yaml.NewYAMLOrJSONDecoder(config, 4096)
|
||||
for {
|
||||
if err := d.Decode(defaultConfig); err != nil {
|
||||
if err != io.EOF {
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
// Admit enforces that pod and its namespace node label selectors matches at least a node in the cluster.
|
||||
func (p *podNodeSelector) Admit(a admission.Attributes) error {
|
||||
if shouldIgnore(a) {
|
||||
return nil
|
||||
}
|
||||
updateInitialized, err := util.IsUpdatingInitializedObject(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if updateInitialized {
|
||||
// node selector of an initialized pod is immutable
|
||||
return nil
|
||||
}
|
||||
if !p.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
|
||||
resource := a.GetResource().GroupResource()
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
namespaceNodeSelector, err := p.getNamespaceNodeSelectorMap(a.GetNamespace())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if labels.Conflicts(namespaceNodeSelector, labels.Set(pod.Spec.NodeSelector)) {
|
||||
return errors.NewForbidden(resource, pod.Name, fmt.Errorf("pod node label selector conflicts with its namespace node label selector"))
|
||||
}
|
||||
|
||||
// Merge pod node selector = namespace node selector + current pod node selector
|
||||
// second selector wins
|
||||
podNodeSelectorLabels := labels.Merge(namespaceNodeSelector, pod.Spec.NodeSelector)
|
||||
pod.Spec.NodeSelector = map[string]string(podNodeSelectorLabels)
|
||||
return p.Validate(a)
|
||||
}
|
||||
|
||||
// Validate ensures that the pod node selector is allowed
|
||||
func (p *podNodeSelector) Validate(a admission.Attributes) error {
|
||||
if shouldIgnore(a) {
|
||||
return nil
|
||||
}
|
||||
if !p.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
|
||||
resource := a.GetResource().GroupResource()
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
|
||||
namespaceNodeSelector, err := p.getNamespaceNodeSelectorMap(a.GetNamespace())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if labels.Conflicts(namespaceNodeSelector, labels.Set(pod.Spec.NodeSelector)) {
|
||||
return errors.NewForbidden(resource, pod.Name, fmt.Errorf("pod node label selector conflicts with its namespace node label selector"))
|
||||
}
|
||||
|
||||
// whitelist verification
|
||||
whitelist, err := labels.ConvertSelectorToLabelsMap(p.clusterNodeSelectors[a.GetNamespace()])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !labels.AreLabelsInWhiteList(pod.Spec.NodeSelector, whitelist) {
|
||||
return errors.NewForbidden(resource, pod.Name, fmt.Errorf("pod node label selector labels conflict with its namespace whitelist"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *podNodeSelector) getNamespaceNodeSelectorMap(namespaceName string) (labels.Set, error) {
|
||||
namespace, err := p.namespaceLister.Get(namespaceName)
|
||||
if errors.IsNotFound(err) {
|
||||
namespace, err = p.defaultGetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.NewInternalError(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, errors.NewInternalError(err)
|
||||
}
|
||||
|
||||
return p.getNodeSelectorMap(namespace)
|
||||
}
|
||||
|
||||
func shouldIgnore(a admission.Attributes) bool {
|
||||
resource := a.GetResource().GroupResource()
|
||||
if resource != api.Resource("pods") {
|
||||
return true
|
||||
}
|
||||
if a.GetSubresource() != "" {
|
||||
// only run the checks below on pods proper and not subresources
|
||||
return true
|
||||
}
|
||||
|
||||
_, ok := a.GetObject().(*api.Pod)
|
||||
if !ok {
|
||||
klog.Errorf("expected pod but got %s", a.GetKind().Kind)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewPodNodeSelector(clusterNodeSelectors map[string]string) *podNodeSelector {
|
||||
return &podNodeSelector{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
clusterNodeSelectors: clusterNodeSelectors,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *podNodeSelector) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||
a.client = client
|
||||
}
|
||||
|
||||
func (p *podNodeSelector) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
p.namespaceLister = namespaceInformer.Lister()
|
||||
p.SetReadyFunc(namespaceInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
func (p *podNodeSelector) ValidateInitialization() error {
|
||||
if p.namespaceLister == nil {
|
||||
return fmt.Errorf("missing namespaceLister")
|
||||
}
|
||||
if p.client == nil {
|
||||
return fmt.Errorf("missing client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *podNodeSelector) defaultGetNamespace(name string) (*corev1.Namespace, error) {
|
||||
namespace, err := p.client.Core().Namespaces().Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("namespace %s does not exist", name)
|
||||
}
|
||||
return namespace, nil
|
||||
}
|
||||
|
||||
func (p *podNodeSelector) getNodeSelectorMap(namespace *corev1.Namespace) (labels.Set, error) {
|
||||
selector := labels.Set{}
|
||||
labelsMap := labels.Set{}
|
||||
var err error
|
||||
found := false
|
||||
if len(namespace.ObjectMeta.Annotations) > 0 {
|
||||
for _, annotation := range NamespaceNodeSelectors {
|
||||
if ns, ok := namespace.ObjectMeta.Annotations[annotation]; ok {
|
||||
labelsMap, err = labels.ConvertSelectorToLabelsMap(ns)
|
||||
if err != nil {
|
||||
return labels.Set{}, err
|
||||
}
|
||||
|
||||
if labels.Conflicts(selector, labelsMap) {
|
||||
nsName := namespace.ObjectMeta.Name
|
||||
return labels.Set{}, fmt.Errorf("%s annotations' node label selectors conflict", nsName)
|
||||
}
|
||||
selector = labels.Merge(selector, labelsMap)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
selector, err = labels.ConvertSelectorToLabelsMap(p.clusterNodeSelectors["clusterDefaultNodeSelector"])
|
||||
if err != nil {
|
||||
return labels.Set{}, err
|
||||
}
|
||||
}
|
||||
return selector, nil
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package podnodeselector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// TestPodAdmission verifies various scenarios involving pod/namespace/global node label selectors
|
||||
func TestPodAdmission(t *testing.T) {
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "testNamespace",
|
||||
Namespace: "",
|
||||
},
|
||||
}
|
||||
|
||||
mockClient := fake.NewSimpleClientset(namespace)
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
informerFactory.Start(stopCh)
|
||||
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
}
|
||||
|
||||
oldPod := *pod
|
||||
oldPod.Initializers = &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init"}}}
|
||||
oldPod.Spec.NodeSelector = map[string]string{
|
||||
"old": "true",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
defaultNodeSelector string
|
||||
namespaceNodeSelector string
|
||||
whitelist string
|
||||
podNodeSelector map[string]string
|
||||
mergedNodeSelector labels.Set
|
||||
ignoreTestNamespaceNodeSelector bool
|
||||
admit bool
|
||||
testName string
|
||||
}{
|
||||
{
|
||||
defaultNodeSelector: "",
|
||||
podNodeSelector: map[string]string{},
|
||||
mergedNodeSelector: labels.Set{},
|
||||
ignoreTestNamespaceNodeSelector: true,
|
||||
admit: true,
|
||||
testName: "No node selectors",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "infra = false",
|
||||
podNodeSelector: map[string]string{},
|
||||
mergedNodeSelector: labels.Set{"infra": "false"},
|
||||
ignoreTestNamespaceNodeSelector: true,
|
||||
admit: true,
|
||||
testName: "Default node selector and no conflicts",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "",
|
||||
namespaceNodeSelector: " infra = false ",
|
||||
podNodeSelector: map[string]string{},
|
||||
mergedNodeSelector: labels.Set{"infra": "false"},
|
||||
admit: true,
|
||||
testName: "TestNamespace node selector with whitespaces and no conflicts",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "infra = false",
|
||||
namespaceNodeSelector: "infra=true",
|
||||
podNodeSelector: map[string]string{},
|
||||
mergedNodeSelector: labels.Set{"infra": "true"},
|
||||
admit: true,
|
||||
testName: "Default and namespace node selector, no conflicts",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "infra = false",
|
||||
namespaceNodeSelector: "",
|
||||
podNodeSelector: map[string]string{},
|
||||
mergedNodeSelector: labels.Set{},
|
||||
admit: true,
|
||||
testName: "Empty namespace node selector and no conflicts",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "infra = false",
|
||||
namespaceNodeSelector: "infra=true",
|
||||
podNodeSelector: map[string]string{"env": "test"},
|
||||
mergedNodeSelector: labels.Set{"infra": "true", "env": "test"},
|
||||
admit: true,
|
||||
testName: "TestNamespace and pod node selector, no conflicts",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "env = test",
|
||||
namespaceNodeSelector: "infra=true",
|
||||
podNodeSelector: map[string]string{"infra": "false"},
|
||||
admit: false,
|
||||
testName: "Conflicting pod and namespace node selector, one label",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "env=dev",
|
||||
namespaceNodeSelector: "infra=false, env = test",
|
||||
podNodeSelector: map[string]string{"env": "dev", "color": "blue"},
|
||||
admit: false,
|
||||
testName: "Conflicting pod and namespace node selector, multiple labels",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "env=dev",
|
||||
namespaceNodeSelector: "infra=false, env = dev",
|
||||
whitelist: "env=dev, infra=false, color=blue",
|
||||
podNodeSelector: map[string]string{"env": "dev", "color": "blue"},
|
||||
mergedNodeSelector: labels.Set{"infra": "false", "env": "dev", "color": "blue"},
|
||||
admit: true,
|
||||
testName: "Merged pod node selectors satisfy the whitelist",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "env=dev",
|
||||
namespaceNodeSelector: "infra=false, env = dev",
|
||||
whitelist: "env=dev, infra=true, color=blue",
|
||||
podNodeSelector: map[string]string{"env": "dev", "color": "blue"},
|
||||
admit: false,
|
||||
testName: "Merged pod node selectors conflict with the whitelist",
|
||||
},
|
||||
{
|
||||
defaultNodeSelector: "env=dev",
|
||||
ignoreTestNamespaceNodeSelector: true,
|
||||
whitelist: "env=prd",
|
||||
podNodeSelector: map[string]string{},
|
||||
admit: false,
|
||||
testName: "Default node selector conflict with the whitelist",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if !test.ignoreTestNamespaceNodeSelector {
|
||||
namespace.ObjectMeta.Annotations = map[string]string{"scheduler.alpha.kubernetes.io/node-selector": test.namespaceNodeSelector}
|
||||
informerFactory.Core().V1().Namespaces().Informer().GetStore().Update(namespace)
|
||||
}
|
||||
handler.clusterNodeSelectors = make(map[string]string)
|
||||
handler.clusterNodeSelectors["clusterDefaultNodeSelector"] = test.defaultNodeSelector
|
||||
handler.clusterNodeSelectors[namespace.Name] = test.whitelist
|
||||
pod.Spec = api.PodSpec{NodeSelector: test.podNodeSelector}
|
||||
|
||||
err := handler.Admit(admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
if test.admit && !labels.Equals(test.mergedNodeSelector, labels.Set(pod.Spec.NodeSelector)) {
|
||||
t.Errorf("Test: %s, expected: %s but got: %s", test.testName, test.mergedNodeSelector, pod.Spec.NodeSelector)
|
||||
}
|
||||
err = handler.Validate(admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
|
||||
// handles update of uninitialized pod like it's newly created.
|
||||
err = handler.Admit(admission.NewAttributesRecord(pod, &oldPod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
if test.admit && !labels.Equals(test.mergedNodeSelector, labels.Set(pod.Spec.NodeSelector)) {
|
||||
t.Errorf("Test: %s, expected: %s but got: %s", test.testName, test.mergedNodeSelector, pod.Spec.NodeSelector)
|
||||
}
|
||||
err = handler.Validate(admission.NewAttributesRecord(pod, &oldPod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
for op, shouldHandle := range map[admission.Operation]bool{
|
||||
admission.Create: true,
|
||||
admission.Update: true,
|
||||
admission.Connect: false,
|
||||
admission.Delete: false,
|
||||
} {
|
||||
nodeEnvionment := NewPodNodeSelector(nil)
|
||||
if e, a := shouldHandle, nodeEnvionment.Handles(op); e != a {
|
||||
t.Errorf("%v: shouldHandle=%t, handles=%t", op, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoreUpdatingInitializedPod(t *testing.T) {
|
||||
namespaceNodeSelector := "infra=true"
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "testNamespace",
|
||||
Namespace: "",
|
||||
Annotations: map[string]string{"scheduler.alpha.kubernetes.io/node-selector": namespaceNodeSelector},
|
||||
},
|
||||
}
|
||||
mockClient := fake.NewSimpleClientset(namespace)
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
handler.SetReadyFunc(func() bool { return true })
|
||||
|
||||
podNodeSelector := map[string]string{"infra": "false"}
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
Spec: api.PodSpec{NodeSelector: podNodeSelector},
|
||||
}
|
||||
// this conflicts with podNodeSelector
|
||||
err = informerFactory.Core().V1().Namespaces().Informer().GetStore().Update(namespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// if the update of initialized pod is not ignored, an error will be returned because the pod's nodeSelector conflicts with namespace's nodeSelector.
|
||||
err = handler.Admit(admission.NewAttributesRecord(pod, pod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// newHandlerForTest returns the admission controller configured for testing.
|
||||
func newHandlerForTest(c kubernetes.Interface) (*podNodeSelector, informers.SharedInformerFactory, error) {
|
||||
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
|
||||
handler := NewPodNodeSelector(nil)
|
||||
pluginInitializer := genericadmissioninitializer.New(c, f, nil, nil)
|
||||
pluginInitializer.Initialize(handler)
|
||||
err := admission.ValidateInitialization(handler)
|
||||
return handler, f, err
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_test.go",
|
||||
"main_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//pkg/scheduler/api:go_default_library",
|
||||
"//pkg/util/tolerations:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"admission.go",
|
||||
"config.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/helper/qos:go_default_library",
|
||||
"//pkg/apis/core/v1:go_default_library",
|
||||
"//pkg/kubeapiserver/admission/util:go_default_library",
|
||||
"//pkg/scheduler/api:go_default_library",
|
||||
"//pkg/util/tolerations:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/install:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/validation:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,297 +0,0 @@
|
|||
/*
|
||||
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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
qoshelper "k8s.io/kubernetes/pkg/apis/core/helper/qos"
|
||||
k8s_api_v1 "k8s.io/kubernetes/pkg/apis/core/v1"
|
||||
"k8s.io/kubernetes/pkg/kubeapiserver/admission/util"
|
||||
schedulerapi "k8s.io/kubernetes/pkg/scheduler/api"
|
||||
"k8s.io/kubernetes/pkg/util/tolerations"
|
||||
pluginapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
)
|
||||
|
||||
const PluginName = "PodTolerationRestriction"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
pluginConfig, err := loadConfiguration(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPodTolerationsPlugin(pluginConfig), nil
|
||||
})
|
||||
}
|
||||
|
||||
// The annotation keys for default and whitelist of tolerations
|
||||
const (
|
||||
NSDefaultTolerations string = "scheduler.alpha.kubernetes.io/defaultTolerations"
|
||||
NSWLTolerations string = "scheduler.alpha.kubernetes.io/tolerationsWhitelist"
|
||||
)
|
||||
|
||||
var _ admission.MutationInterface = &podTolerationsPlugin{}
|
||||
var _ admission.ValidationInterface = &podTolerationsPlugin{}
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&podTolerationsPlugin{})
|
||||
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&podTolerationsPlugin{})
|
||||
|
||||
type podTolerationsPlugin struct {
|
||||
*admission.Handler
|
||||
client kubernetes.Interface
|
||||
namespaceLister corev1listers.NamespaceLister
|
||||
pluginConfig *pluginapi.Configuration
|
||||
}
|
||||
|
||||
// This plugin first verifies any conflict between a pod's tolerations and
|
||||
// its namespace's tolerations, and rejects the pod if there's a conflict.
|
||||
// If there's no conflict, the pod's tolerations are merged with its namespace's
|
||||
// toleration. Resulting pod's tolerations are verified against its namespace's
|
||||
// whitelist of tolerations. If the verification is successful, the pod is admitted
|
||||
// otherwise rejected. If a namespace does not have associated default or whitelist
|
||||
// of tolerations, then cluster level default or whitelist of tolerations are used
|
||||
// instead if specified. Tolerations to a namespace are assigned via
|
||||
// scheduler.alpha.kubernetes.io/defaultTolerations and scheduler.alpha.kubernetes.io/tolerationsWhitelist
|
||||
// annotations keys.
|
||||
func (p *podTolerationsPlugin) Admit(a admission.Attributes) error {
|
||||
if shouldIgnore(a) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !p.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
var finalTolerations []api.Toleration
|
||||
updateUninitialized, err := util.IsUpdatingUninitializedObject(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.GetOperation() == admission.Create || updateUninitialized {
|
||||
ts, err := p.getNamespaceDefaultTolerations(a.GetNamespace())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the namespace has not specified its default tolerations,
|
||||
// fall back to cluster's default tolerations.
|
||||
if ts == nil {
|
||||
ts = p.pluginConfig.Default
|
||||
}
|
||||
|
||||
if len(ts) > 0 {
|
||||
if len(pod.Spec.Tolerations) > 0 {
|
||||
if tolerations.IsConflict(ts, pod.Spec.Tolerations) {
|
||||
return fmt.Errorf("namespace tolerations and pod tolerations conflict")
|
||||
}
|
||||
|
||||
// modified pod tolerations = namespace tolerations + current pod tolerations
|
||||
finalTolerations = tolerations.MergeTolerations(ts, pod.Spec.Tolerations)
|
||||
} else {
|
||||
finalTolerations = ts
|
||||
|
||||
}
|
||||
} else {
|
||||
finalTolerations = pod.Spec.Tolerations
|
||||
}
|
||||
} else {
|
||||
finalTolerations = pod.Spec.Tolerations
|
||||
}
|
||||
|
||||
if qoshelper.GetPodQOS(pod) != api.PodQOSBestEffort {
|
||||
finalTolerations = tolerations.MergeTolerations(finalTolerations, []api.Toleration{
|
||||
{
|
||||
Key: schedulerapi.TaintNodeMemoryPressure,
|
||||
Operator: api.TolerationOpExists,
|
||||
Effect: api.TaintEffectNoSchedule,
|
||||
},
|
||||
})
|
||||
}
|
||||
pod.Spec.Tolerations = finalTolerations
|
||||
|
||||
return p.Validate(a)
|
||||
}
|
||||
func (p *podTolerationsPlugin) Validate(a admission.Attributes) error {
|
||||
if shouldIgnore(a) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !p.WaitForReady() {
|
||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||
}
|
||||
|
||||
// whitelist verification.
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
if len(pod.Spec.Tolerations) > 0 {
|
||||
whitelist, err := p.getNamespaceTolerationsWhitelist(a.GetNamespace())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the namespace has not specified its tolerations whitelist,
|
||||
// fall back to cluster's whitelist of tolerations.
|
||||
if whitelist == nil {
|
||||
whitelist = p.pluginConfig.Whitelist
|
||||
}
|
||||
|
||||
if len(whitelist) > 0 {
|
||||
// check if the merged pod tolerations satisfy its namespace whitelist
|
||||
if !tolerations.VerifyAgainstWhitelist(pod.Spec.Tolerations, whitelist) {
|
||||
return fmt.Errorf("pod tolerations (possibly merged with namespace default tolerations) conflict with its namespace whitelist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldIgnore(a admission.Attributes) bool {
|
||||
resource := a.GetResource().GroupResource()
|
||||
if resource != api.Resource("pods") {
|
||||
return true
|
||||
}
|
||||
if a.GetSubresource() != "" {
|
||||
// only run the checks below on pods proper and not subresources
|
||||
return true
|
||||
}
|
||||
|
||||
obj := a.GetObject()
|
||||
_, ok := obj.(*api.Pod)
|
||||
if !ok {
|
||||
klog.Errorf("expected pod but got %s", a.GetKind().Kind)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewPodTolerationsPlugin(pluginConfig *pluginapi.Configuration) *podTolerationsPlugin {
|
||||
return &podTolerationsPlugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
pluginConfig: pluginConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *podTolerationsPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||
a.client = client
|
||||
}
|
||||
|
||||
func (p *podTolerationsPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
p.namespaceLister = namespaceInformer.Lister()
|
||||
p.SetReadyFunc(namespaceInformer.Informer().HasSynced)
|
||||
|
||||
}
|
||||
|
||||
func (p *podTolerationsPlugin) ValidateInitialization() error {
|
||||
if p.namespaceLister == nil {
|
||||
return fmt.Errorf("missing namespaceLister")
|
||||
}
|
||||
if p.client == nil {
|
||||
return fmt.Errorf("missing client")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// in exceptional cases, this can result in two live calls, but once the cache catches up, that will stop.
|
||||
func (p *podTolerationsPlugin) getNamespace(nsName string) (*corev1.Namespace, error) {
|
||||
namespace, err := p.namespaceLister.Get(nsName)
|
||||
if errors.IsNotFound(err) {
|
||||
// in case of latency in our caches, make a call direct to storage to verify that it truly exists or not
|
||||
namespace, err = p.client.CoreV1().Namespaces().Get(nsName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.NewInternalError(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, errors.NewInternalError(err)
|
||||
}
|
||||
|
||||
return namespace, nil
|
||||
}
|
||||
|
||||
func (p *podTolerationsPlugin) getNamespaceDefaultTolerations(nsName string) ([]api.Toleration, error) {
|
||||
ns, err := p.getNamespace(nsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractNSTolerations(ns, NSDefaultTolerations)
|
||||
}
|
||||
|
||||
func (p *podTolerationsPlugin) getNamespaceTolerationsWhitelist(nsName string) ([]api.Toleration, error) {
|
||||
ns, err := p.getNamespace(nsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return extractNSTolerations(ns, NSWLTolerations)
|
||||
}
|
||||
|
||||
// extractNSTolerations extracts default or whitelist of tolerations from
|
||||
// following namespace annotations keys: "scheduler.alpha.kubernetes.io/defaultTolerations"
|
||||
// and "scheduler.alpha.kubernetes.io/tolerationsWhitelist". If these keys are
|
||||
// unset (nil), extractNSTolerations returns nil. If the value to these
|
||||
// keys are set to empty, an empty toleration is returned, otherwise
|
||||
// configured tolerations are returned.
|
||||
func extractNSTolerations(ns *corev1.Namespace, key string) ([]api.Toleration, error) {
|
||||
// if a namespace does not have any annotations
|
||||
if len(ns.Annotations) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if NSWLTolerations or NSDefaultTolerations does not exist
|
||||
if _, ok := ns.Annotations[key]; !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if value is set to empty
|
||||
if len(ns.Annotations[key]) == 0 {
|
||||
return []api.Toleration{}, nil
|
||||
}
|
||||
|
||||
var v1Tolerations []v1.Toleration
|
||||
err := json.Unmarshal([]byte(ns.Annotations[key]), &v1Tolerations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts := make([]api.Toleration, len(v1Tolerations))
|
||||
for i := range v1Tolerations {
|
||||
if err := k8s_api_v1.Convert_v1_Toleration_To_core_Toleration(&v1Tolerations[i], &ts[i], nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ts, nil
|
||||
}
|
|
@ -1,371 +0,0 @@
|
|||
/*
|
||||
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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
schedulerapi "k8s.io/kubernetes/pkg/scheduler/api"
|
||||
"k8s.io/kubernetes/pkg/util/tolerations"
|
||||
pluginapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
)
|
||||
|
||||
// TestPodAdmission verifies various scenarios involving pod/namespace tolerations
|
||||
func TestPodAdmission(t *testing.T) {
|
||||
|
||||
CPU1000m := resource.MustParse("1000m")
|
||||
CPU500m := resource.MustParse("500m")
|
||||
|
||||
burstablePod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "test",
|
||||
Resources: api.ResourceRequirements{
|
||||
Limits: api.ResourceList{api.ResourceCPU: CPU1000m},
|
||||
Requests: api.ResourceList{api.ResourceCPU: CPU500m},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
guaranteedPod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "test",
|
||||
Resources: api.ResourceRequirements{
|
||||
Limits: api.ResourceList{api.ResourceCPU: CPU1000m},
|
||||
Requests: api.ResourceList{api.ResourceCPU: CPU1000m},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bestEffortPod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.TaintNodesByCondition, true)()
|
||||
|
||||
tests := []struct {
|
||||
pod *api.Pod
|
||||
defaultClusterTolerations []api.Toleration
|
||||
namespaceTolerations []api.Toleration
|
||||
whitelist []api.Toleration
|
||||
clusterWhitelist []api.Toleration
|
||||
podTolerations []api.Toleration
|
||||
mergedTolerations []api.Toleration
|
||||
admit bool
|
||||
testName string
|
||||
}{
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
namespaceTolerations: nil,
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "default cluster tolerations with empty pod tolerations and nil namespace tolerations",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
namespaceTolerations: []api.Toleration{},
|
||||
podTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "default cluster tolerations with pod tolerations specified",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "namespace tolerations",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "no pod tolerations",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: false,
|
||||
testName: "conflicting pod and namespace tolerations",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue2", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
namespaceTolerations: []api.Toleration{},
|
||||
podTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "conflicting pod and default cluster tolerations but overridden by empty namespace tolerations",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
whitelist: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "merged pod tolerations satisfy whitelist",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
namespaceTolerations: []api.Toleration{},
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{},
|
||||
admit: true,
|
||||
testName: "Override default cluster toleration by empty namespace level toleration",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
whitelist: []api.Toleration{},
|
||||
clusterWhitelist: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
mergedTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
admit: true,
|
||||
testName: "pod toleration conflicts with default cluster white list which is overridden by empty namespace whitelist",
|
||||
},
|
||||
{
|
||||
pod: bestEffortPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
whitelist: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
podTolerations: []api.Toleration{},
|
||||
admit: false,
|
||||
testName: "merged pod tolerations conflict with the whitelist",
|
||||
},
|
||||
{
|
||||
pod: burstablePod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
whitelist: []api.Toleration{},
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{
|
||||
{Key: schedulerapi.TaintNodeMemoryPressure, Operator: api.TolerationOpExists, Effect: api.TaintEffectNoSchedule, TolerationSeconds: nil},
|
||||
{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil},
|
||||
},
|
||||
admit: true,
|
||||
testName: "added memoryPressure/DiskPressure for Burstable pod",
|
||||
},
|
||||
{
|
||||
pod: guaranteedPod,
|
||||
defaultClusterTolerations: []api.Toleration{},
|
||||
namespaceTolerations: []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil}},
|
||||
whitelist: []api.Toleration{},
|
||||
podTolerations: []api.Toleration{},
|
||||
mergedTolerations: []api.Toleration{
|
||||
{Key: schedulerapi.TaintNodeMemoryPressure, Operator: api.TolerationOpExists, Effect: api.TaintEffectNoSchedule, TolerationSeconds: nil},
|
||||
{Key: "testKey", Operator: "Equal", Value: "testValue", Effect: "NoSchedule", TolerationSeconds: nil},
|
||||
},
|
||||
admit: true,
|
||||
testName: "added memoryPressure/DiskPressure for Guaranteed pod",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "testNamespace",
|
||||
Namespace: "",
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
if test.namespaceTolerations != nil {
|
||||
tolerationStr, err := json.Marshal(test.namespaceTolerations)
|
||||
if err != nil {
|
||||
t.Errorf("error in marshalling namespace tolerations %v", test.namespaceTolerations)
|
||||
}
|
||||
namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationStr)}
|
||||
}
|
||||
|
||||
if test.whitelist != nil {
|
||||
tolerationStr, err := json.Marshal(test.whitelist)
|
||||
if err != nil {
|
||||
t.Errorf("error in marshalling namespace whitelist %v", test.whitelist)
|
||||
}
|
||||
namespace.Annotations[NSWLTolerations] = string(tolerationStr)
|
||||
}
|
||||
|
||||
mockClient := fake.NewSimpleClientset(namespace)
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
stopCh := make(chan struct{})
|
||||
defer close(stopCh)
|
||||
informerFactory.Start(stopCh)
|
||||
|
||||
handler.pluginConfig = &pluginapi.Configuration{Default: test.defaultClusterTolerations, Whitelist: test.clusterWhitelist}
|
||||
pod := test.pod
|
||||
pod.Spec.Tolerations = test.podTolerations
|
||||
|
||||
// copy the original pod for tests of uninitialized pod updates.
|
||||
oldPod := *pod
|
||||
oldPod.Initializers = &metav1.Initializers{Pending: []metav1.Initializer{{Name: "init"}}}
|
||||
oldPod.Spec.Tolerations = []api.Toleration{{Key: "testKey", Operator: "Equal", Value: "testValue1", Effect: "NoSchedule", TolerationSeconds: nil}}
|
||||
|
||||
err = handler.Admit(admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Create, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
|
||||
updatedPodTolerations := pod.Spec.Tolerations
|
||||
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
|
||||
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
|
||||
}
|
||||
|
||||
// handles update of uninitialized pod like it's newly created.
|
||||
err = handler.Admit(admission.NewAttributesRecord(pod, &oldPod, api.Kind("Pod").WithVersion("version"), "testNamespace", namespace.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if test.admit && err != nil {
|
||||
t.Errorf("Test: %s, expected no error but got: %s", test.testName, err)
|
||||
} else if !test.admit && err == nil {
|
||||
t.Errorf("Test: %s, expected an error", test.testName)
|
||||
}
|
||||
|
||||
updatedPodTolerations = pod.Spec.Tolerations
|
||||
if test.admit && !tolerations.EqualTolerations(updatedPodTolerations, test.mergedTolerations) {
|
||||
t.Errorf("Test: %s, expected: %#v but got: %#v", test.testName, test.mergedTolerations, updatedPodTolerations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
for op, shouldHandle := range map[admission.Operation]bool{
|
||||
admission.Create: true,
|
||||
admission.Update: true,
|
||||
admission.Connect: false,
|
||||
admission.Delete: false,
|
||||
} {
|
||||
|
||||
pluginConfig, err := loadConfiguration(nil)
|
||||
// must not fail
|
||||
if err != nil {
|
||||
t.Errorf("%v: error reading default configuration", op)
|
||||
}
|
||||
ptPlugin := NewPodTolerationsPlugin(pluginConfig)
|
||||
if e, a := shouldHandle, ptPlugin.Handles(op); e != a {
|
||||
t.Errorf("%v: shouldHandle=%t, handles=%t", op, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoreUpdatingInitializedPod(t *testing.T) {
|
||||
mockClient := &fake.Clientset{}
|
||||
handler, informerFactory, err := newHandlerForTest(mockClient)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error initializing handler: %v", err)
|
||||
}
|
||||
handler.SetReadyFunc(func() bool { return true })
|
||||
|
||||
pod := &api.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "testNamespace"},
|
||||
Spec: api.PodSpec{},
|
||||
}
|
||||
podToleration := api.Toleration{
|
||||
Key: "testKey",
|
||||
Operator: "Equal",
|
||||
Value: "testValue1",
|
||||
Effect: "NoSchedule",
|
||||
TolerationSeconds: nil,
|
||||
}
|
||||
pod.Spec.Tolerations = []api.Toleration{podToleration}
|
||||
|
||||
// this conflicts with pod's Tolerations
|
||||
namespaceToleration := podToleration
|
||||
namespaceToleration.Value = "testValue2"
|
||||
namespaceTolerations := []api.Toleration{namespaceToleration}
|
||||
tolerationsStr, err := json.Marshal(namespaceTolerations)
|
||||
if err != nil {
|
||||
t.Errorf("error in marshalling namespace tolerations %v", namespaceTolerations)
|
||||
}
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "testNamespace",
|
||||
Namespace: "",
|
||||
},
|
||||
}
|
||||
namespace.Annotations = map[string]string{NSDefaultTolerations: string(tolerationsStr)}
|
||||
err = informerFactory.Core().V1().Namespaces().Informer().GetStore().Update(namespace)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// if the update of initialized pod is not ignored, an error will be returned because the pod's Tolerations conflicts with namespace's Tolerations.
|
||||
err = handler.Admit(admission.NewAttributesRecord(pod, pod, api.Kind("Pod").WithVersion("version"), "testNamespace", pod.ObjectMeta.Name, api.Resource("pods").WithVersion("version"), "", admission.Update, false, nil))
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// newHandlerForTest returns the admission controller configured for testing.
|
||||
func newHandlerForTest(c kubernetes.Interface) (*podTolerationsPlugin, informers.SharedInformerFactory, error) {
|
||||
f := informers.NewSharedInformerFactory(c, 5*time.Minute)
|
||||
pluginConfig, err := loadConfiguration(nil)
|
||||
// must not fail
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
handler := NewPodTolerationsPlugin(pluginConfig)
|
||||
pluginInitializer := genericadmissioninitializer.New(c, f, nil, nil)
|
||||
pluginInitializer.Initialize(handler)
|
||||
err = admission.ValidateInitialization(handler)
|
||||
return handler, f, err
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
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",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/install:all-srcs",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1:all-srcs",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/validation:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
reviewers:
|
||||
approvers:
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
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
|
||||
|
||||
package podtolerationrestriction // import "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
|
@ -1,31 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["install.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/install",
|
||||
deps = [
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
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/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
versionedapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1"
|
||||
)
|
||||
|
||||
// Install registers the API group and adds types to a scheme
|
||||
func Install(scheme *runtime.Scheme) {
|
||||
utilruntime.Must(internalapi.AddToScheme(scheme))
|
||||
utilruntime.Must(versionedapi.AddToScheme(scheme))
|
||||
utilruntime.Must(scheme.SetVersionPriority(versionedapi.SchemeGroupVersion))
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// GroupName is the group name use in this package
|
||||
const GroupName = "podtolerationrestriction.admission.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
|
||||
|
||||
var (
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// 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 {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Configuration{},
|
||||
)
|
||||
return nil
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// Configuration provides configuration for the PodTolerationRestriction admission controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta
|
||||
|
||||
// cluster level default tolerations
|
||||
Default []api.Toleration
|
||||
|
||||
// cluster level whitelist of tolerations
|
||||
Whitelist []api.Toleration
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
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",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||
return RegisterDefaults(scheme)
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
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
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=podtolerationrestriction.admission.k8s.io
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the API.
|
||||
package v1alpha1 // import "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1"
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
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 = "podtolerationrestriction.admission.k8s.io"
|
||||
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
|
||||
|
||||
var (
|
||||
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
|
||||
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We only register manually written functions here. The registration of the
|
||||
// generated functions takes place in the generated files. The separation
|
||||
// makes the code compile even when the generated files are missing.
|
||||
localSchemeBuilder.Register(addKnownTypes, addDefaultingFuncs)
|
||||
}
|
||||
|
||||
func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Configuration{},
|
||||
)
|
||||
return nil
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
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/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
// Configuration provides configuration for the PodTolerationRestriction admission controller.
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
|
||||
// cluster level default tolerations
|
||||
Default []v1.Toleration `json:"default,omitempty"`
|
||||
|
||||
// cluster level whitelist of tolerations
|
||||
Whitelist []v1.Toleration `json:"whitelist,omitempty"`
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by conversion-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
unsafe "unsafe"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
core "k8s.io/kubernetes/pkg/apis/core"
|
||||
podtolerationrestriction "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(RegisterConversions)
|
||||
}
|
||||
|
||||
// RegisterConversions adds conversion functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterConversions(s *runtime.Scheme) error {
|
||||
if err := s.AddGeneratedConversionFunc((*Configuration)(nil), (*podtolerationrestriction.Configuration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_Configuration_To_podtolerationrestriction_Configuration(a.(*Configuration), b.(*podtolerationrestriction.Configuration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*podtolerationrestriction.Configuration)(nil), (*Configuration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_podtolerationrestriction_Configuration_To_v1alpha1_Configuration(a.(*podtolerationrestriction.Configuration), b.(*Configuration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_Configuration_To_podtolerationrestriction_Configuration(in *Configuration, out *podtolerationrestriction.Configuration, s conversion.Scope) error {
|
||||
out.Default = *(*[]core.Toleration)(unsafe.Pointer(&in.Default))
|
||||
out.Whitelist = *(*[]core.Toleration)(unsafe.Pointer(&in.Whitelist))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_Configuration_To_podtolerationrestriction_Configuration is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_Configuration_To_podtolerationrestriction_Configuration(in *Configuration, out *podtolerationrestriction.Configuration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_Configuration_To_podtolerationrestriction_Configuration(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_podtolerationrestriction_Configuration_To_v1alpha1_Configuration(in *podtolerationrestriction.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
out.Default = *(*[]v1.Toleration)(unsafe.Pointer(&in.Default))
|
||||
out.Whitelist = *(*[]v1.Toleration)(unsafe.Pointer(&in.Whitelist))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_podtolerationrestriction_Configuration_To_v1alpha1_Configuration is an autogenerated conversion function.
|
||||
func Convert_podtolerationrestriction_Configuration_To_v1alpha1_Configuration(in *podtolerationrestriction.Configuration, out *Configuration, s conversion.Scope) error {
|
||||
return autoConvert_podtolerationrestriction_Configuration_To_v1alpha1_Configuration(in, out, s)
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Default != nil {
|
||||
in, out := &in.Default, &out.Default
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Whitelist != nil {
|
||||
in, out := &in.Whitelist, &out.Whitelist
|
||||
*out = make([]v1.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
|
||||
func (in *Configuration) DeepCopy() *Configuration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Configuration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Configuration) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["validation.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/validation",
|
||||
deps = [
|
||||
"//pkg/apis/core/validation:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
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"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction:go_default_library",
|
||||
],
|
||||
)
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
)
|
||||
|
||||
// ValidateConfiguration validates the configuration.
|
||||
func ValidateConfiguration(config *internalapi.Configuration) error {
|
||||
allErrs := field.ErrorList{}
|
||||
fldpath := field.NewPath("podtolerationrestriction")
|
||||
allErrs = append(allErrs, validation.ValidateTolerations(config.Default, fldpath.Child("default"))...)
|
||||
allErrs = append(allErrs, validation.ValidateTolerations(config.Whitelist, fldpath.Child("whitelist"))...)
|
||||
if len(allErrs) > 0 {
|
||||
return fmt.Errorf("invalid config: %v", allErrs)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
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 (
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateConfiguration(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
config internalapi.Configuration
|
||||
testName string
|
||||
testStatus bool
|
||||
}{
|
||||
{
|
||||
config: internalapi.Configuration{
|
||||
Default: []api.Toleration{
|
||||
{Key: "foo", Operator: "Exists", Value: "", Effect: "NoExecute", TolerationSeconds: &[]int64{60}[0]},
|
||||
{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoExecute", TolerationSeconds: &[]int64{60}[0]},
|
||||
{Key: "foo", Operator: "Equal", Value: "bar", Effect: "NoSchedule"},
|
||||
{Operator: "Exists", Effect: "NoSchedule"},
|
||||
},
|
||||
Whitelist: []api.Toleration{
|
||||
{Key: "foo", Value: "bar", Effect: "NoSchedule"},
|
||||
{Key: "foo", Operator: "Equal", Value: "bar"},
|
||||
},
|
||||
},
|
||||
testName: "Valid cases",
|
||||
testStatus: true,
|
||||
},
|
||||
{
|
||||
config: internalapi.Configuration{
|
||||
Whitelist: []api.Toleration{{Key: "foo", Operator: "Exists", Value: "bar", Effect: "NoSchedule"}},
|
||||
},
|
||||
testName: "Invalid case",
|
||||
testStatus: false,
|
||||
},
|
||||
{
|
||||
config: internalapi.Configuration{
|
||||
Default: []api.Toleration{{Operator: "Equal", Value: "bar", Effect: "NoSchedule"}},
|
||||
},
|
||||
testName: "Invalid case",
|
||||
testStatus: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
errs := ValidateConfiguration(&tests[i].config)
|
||||
if tests[i].testStatus && errs != nil {
|
||||
t.Errorf("Test: %s, expected success: %v", tests[i].testName, errs)
|
||||
}
|
||||
if !tests[i].testStatus && errs == nil {
|
||||
t.Errorf("Test: %s, expected errors: %v", tests[i].testName, errs)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package podtolerationrestriction
|
||||
|
||||
import (
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
core "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.Default != nil {
|
||||
in, out := &in.Default, &out.Default
|
||||
*out = make([]core.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Whitelist != nil {
|
||||
in, out := &in.Whitelist, &out.Whitelist
|
||||
*out = make([]core.Toleration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
|
||||
func (in *Configuration) DeepCopy() *Configuration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Configuration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Configuration) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
internalapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/install"
|
||||
versionedapi "k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/v1alpha1"
|
||||
"k8s.io/kubernetes/plugin/pkg/admission/podtolerationrestriction/apis/podtolerationrestriction/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(scheme)
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the provided configuration.
|
||||
func loadConfiguration(config io.Reader) (*internalapi.Configuration, error) {
|
||||
// if no config is provided, return a default configuration
|
||||
if config == nil {
|
||||
externalConfig := &versionedapi.Configuration{}
|
||||
scheme.Default(externalConfig)
|
||||
internalConfig := &internalapi.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
|
||||
}
|
||||
externalConfig, ok := decodedObj.(*internalapi.Configuration)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected type: %T", decodedObj)
|
||||
}
|
||||
|
||||
if err := validation.ValidateConfiguration(externalConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return externalConfig, nil
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 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 podtolerationrestriction
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
_ "k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
utilfeaturetesting.VerifyFeatureGatesUnchanged(utilfeature.DefaultFeatureGate, m.Run)
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["doc.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/security",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//plugin/pkg/admission/security/podsecuritypolicy:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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.
|
||||
*/
|
||||
|
||||
// security contains admission plugins specific to cluster security.
|
||||
package security // import "k8s.io/kubernetes/plugin/pkg/admission/security"
|
|
@ -1,75 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/security/podsecuritypolicy",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/extensions:go_default_library",
|
||||
"//pkg/apis/policy:go_default_library",
|
||||
"//pkg/registry/rbac:go_default_library",
|
||||
"//pkg/security/podsecuritypolicy:go_default_library",
|
||||
"//pkg/security/podsecuritypolicy/util:go_default_library",
|
||||
"//pkg/serviceaccount:go_default_library",
|
||||
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/policy/v1beta1:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/api/legacyscheme:go_default_library",
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/v1:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/security/apparmor:go_default_library",
|
||||
"//pkg/security/podsecuritypolicy:go_default_library",
|
||||
"//pkg/security/podsecuritypolicy/seccomp:go_default_library",
|
||||
"//pkg/security/podsecuritypolicy/util:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/api/policy/v1beta1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/k8s.io/utils/pointer:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,7 +0,0 @@
|
|||
approvers:
|
||||
- sig-auth-policy-approvers
|
||||
reviewers:
|
||||
- sig-auth-policy-reviewers
|
||||
labels:
|
||||
- sig/auth
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
package podsecuritypolicy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/informers"
|
||||
policylisters "k8s.io/client-go/listers/policy/v1beta1"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/apis/policy"
|
||||
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
|
||||
psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
|
||||
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginName = "PodSecurityPolicy"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
plugin := newPlugin(psp.NewSimpleStrategyFactory(), true)
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// PodSecurityPolicyPlugin holds state for and implements the admission plugin.
|
||||
type PodSecurityPolicyPlugin struct {
|
||||
*admission.Handler
|
||||
strategyFactory psp.StrategyFactory
|
||||
failOnNoPolicies bool
|
||||
authz authorizer.Authorizer
|
||||
lister policylisters.PodSecurityPolicyLister
|
||||
}
|
||||
|
||||
// SetAuthorizer sets the authorizer.
|
||||
func (plugin *PodSecurityPolicyPlugin) SetAuthorizer(authz authorizer.Authorizer) {
|
||||
plugin.authz = authz
|
||||
}
|
||||
|
||||
// ValidateInitialization ensures an authorizer is set.
|
||||
func (plugin *PodSecurityPolicyPlugin) ValidateInitialization() error {
|
||||
if plugin.authz == nil {
|
||||
return fmt.Errorf("%s requires an authorizer", PluginName)
|
||||
}
|
||||
if plugin.lister == nil {
|
||||
return fmt.Errorf("%s requires a lister", PluginName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ admission.MutationInterface = &PodSecurityPolicyPlugin{}
|
||||
var _ admission.ValidationInterface = &PodSecurityPolicyPlugin{}
|
||||
var _ genericadmissioninit.WantsAuthorizer = &PodSecurityPolicyPlugin{}
|
||||
var _ genericadmissioninit.WantsExternalKubeInformerFactory = &PodSecurityPolicyPlugin{}
|
||||
var auditKeyPrefix = strings.ToLower(PluginName) + "." + policy.GroupName + ".k8s.io"
|
||||
|
||||
// newPlugin creates a new PSP admission plugin.
|
||||
func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *PodSecurityPolicyPlugin {
|
||||
return &PodSecurityPolicyPlugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
strategyFactory: strategyFactory,
|
||||
failOnNoPolicies: failOnNoPolicies,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *PodSecurityPolicyPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
podSecurityPolicyInformer := f.Policy().V1beta1().PodSecurityPolicies()
|
||||
a.lister = podSecurityPolicyInformer.Lister()
|
||||
a.SetReadyFunc(podSecurityPolicyInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
// Admit determines if the pod should be admitted based on the requested security context
|
||||
// and the available PSPs.
|
||||
//
|
||||
// 1. Find available PSPs.
|
||||
// 2. Create the providers, includes setting pre-allocated values if necessary.
|
||||
// 3. Try to generate and validate a PSP with providers. If we find one then admit the pod
|
||||
// with the validated PSP. If we don't find any reject the pod and give all errors from the
|
||||
// failed attempts.
|
||||
func (c *PodSecurityPolicyPlugin) Admit(a admission.Attributes) error {
|
||||
if ignore, err := shouldIgnore(a); err != nil {
|
||||
return err
|
||||
} else if ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
// only mutate if this is a CREATE request. On updates we only validate.
|
||||
// TODO(liggitt): allow spec mutation during initializing updates?
|
||||
if a.GetOperation() != admission.Create {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
|
||||
// compute the context. Mutation is allowed. ValidatedPSPAnnotation is not taken into account.
|
||||
allowedPod, pspName, validationErrs, err := c.computeSecurityContext(a, pod, true, "")
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
if allowedPod != nil {
|
||||
*pod = *allowedPod
|
||||
// annotate and accept the pod
|
||||
klog.V(4).Infof("pod %s (generate: %s) in namespace %s validated against provider %s", pod.Name, pod.GenerateName, a.GetNamespace(), pspName)
|
||||
if pod.ObjectMeta.Annotations == nil {
|
||||
pod.ObjectMeta.Annotations = map[string]string{}
|
||||
}
|
||||
pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = pspName
|
||||
key := auditKeyPrefix + "/" + "admit-policy"
|
||||
if err := a.AddAnnotation(key, pspName); err != nil {
|
||||
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
||||
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
||||
return admission.NewForbidden(a, fmt.Errorf("unable to validate against any pod security policy: %v", validationErrs))
|
||||
}
|
||||
|
||||
func (c *PodSecurityPolicyPlugin) Validate(a admission.Attributes) error {
|
||||
if ignore, err := shouldIgnore(a); err != nil {
|
||||
return err
|
||||
} else if ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod := a.GetObject().(*api.Pod)
|
||||
|
||||
// compute the context. Mutation is not allowed. ValidatedPSPAnnotation is used as a hint to gain same speed-up.
|
||||
allowedPod, pspName, validationErrs, err := c.computeSecurityContext(a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
|
||||
if err != nil {
|
||||
return admission.NewForbidden(a, err)
|
||||
}
|
||||
if apiequality.Semantic.DeepEqual(pod, allowedPod) {
|
||||
key := auditKeyPrefix + "/" + "validate-policy"
|
||||
if err := a.AddAnnotation(key, pspName); err != nil {
|
||||
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
||||
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
||||
return admission.NewForbidden(a, fmt.Errorf("unable to validate against any pod security policy: %v", validationErrs))
|
||||
}
|
||||
|
||||
func shouldIgnore(a admission.Attributes) (bool, error) {
|
||||
if a.GetResource().GroupResource() != api.Resource("pods") {
|
||||
return true, nil
|
||||
}
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if we can't convert then fail closed since we've already checked that this is supposed to be a pod object.
|
||||
// this shouldn't normally happen during admission but could happen if an integrator passes a versioned
|
||||
// pod object rather than an internal object.
|
||||
if _, ok := a.GetObject().(*api.Pod); !ok {
|
||||
return false, admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
||||
}
|
||||
|
||||
// if this is an update, see if we are only updating the ownerRef/finalizers. Garbage collection does this
|
||||
// and we should allow it in general, since you had the power to update and the power to delete.
|
||||
// The worst that happens is that you delete something, but you aren't controlling the privileged object itself
|
||||
if a.GetOperation() == admission.Update && rbacregistry.IsOnlyMutatingGCFields(a.GetObject(), a.GetOldObject(), apiequality.Semantic) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// computeSecurityContext derives a valid security context while trying to avoid any changes to the given pod. I.e.
|
||||
// if there is a matching policy with the same security context as given, it will be reused. If there is no
|
||||
// matching policy the returned pod will be nil and the pspName empty. validatedPSPHint is the validated psp name
|
||||
// saved in kubernetes.io/psp annotation. This psp is usually the one we are looking for.
|
||||
func (c *PodSecurityPolicyPlugin) computeSecurityContext(a admission.Attributes, pod *api.Pod, specMutationAllowed bool, validatedPSPHint string) (*api.Pod, string, field.ErrorList, error) {
|
||||
// get all constraints that are usable by the user
|
||||
klog.V(4).Infof("getting pod security policies for pod %s (generate: %s)", pod.Name, pod.GenerateName)
|
||||
var saInfo user.Info
|
||||
if len(pod.Spec.ServiceAccountName) > 0 {
|
||||
saInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "")
|
||||
}
|
||||
|
||||
policies, err := c.lister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
// if we have no policies and want to succeed then return. Otherwise we'll end up with no
|
||||
// providers and fail with "unable to validate against any pod security policy" below.
|
||||
if len(policies) == 0 && !c.failOnNoPolicies {
|
||||
return pod, "", nil, nil
|
||||
}
|
||||
|
||||
// sort policies by name to make order deterministic
|
||||
// If mutation is not allowed and validatedPSPHint is provided, check the validated policy first.
|
||||
// TODO(liggitt): add priority field to allow admins to bucket differently
|
||||
sort.SliceStable(policies, func(i, j int) bool {
|
||||
if !specMutationAllowed {
|
||||
if policies[i].Name == validatedPSPHint {
|
||||
return true
|
||||
}
|
||||
if policies[j].Name == validatedPSPHint {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return strings.Compare(policies[i].Name, policies[j].Name) < 0
|
||||
})
|
||||
|
||||
providers, errs := c.createProvidersFromPolicies(policies, pod.Namespace)
|
||||
for _, err := range errs {
|
||||
klog.V(4).Infof("provider creation error: %v", err)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, "", nil, fmt.Errorf("no providers available to validate pod request")
|
||||
}
|
||||
|
||||
var (
|
||||
allowedMutatedPod *api.Pod
|
||||
allowingMutatingPSP string
|
||||
// Map of PSP name to associated validation errors.
|
||||
validationErrs = map[string]field.ErrorList{}
|
||||
)
|
||||
|
||||
for _, provider := range providers {
|
||||
podCopy := pod.DeepCopy()
|
||||
|
||||
if errs := assignSecurityContext(provider, podCopy); len(errs) > 0 {
|
||||
validationErrs[provider.GetPSPName()] = errs
|
||||
continue
|
||||
}
|
||||
|
||||
// the entire pod validated
|
||||
mutated := !apiequality.Semantic.DeepEqual(pod, podCopy)
|
||||
if mutated && !specMutationAllowed {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isAuthorizedForPolicy(a.GetUserInfo(), saInfo, a.GetNamespace(), provider.GetPSPName(), c.authz) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case !mutated:
|
||||
// if it validated without mutating anything, use this result
|
||||
return podCopy, provider.GetPSPName(), nil, nil
|
||||
|
||||
case specMutationAllowed && allowedMutatedPod == nil:
|
||||
// if mutation is allowed and this is the first PSP to allow the pod, remember it,
|
||||
// but continue to see if another PSP allows without mutating
|
||||
allowedMutatedPod = podCopy
|
||||
allowingMutatingPSP = provider.GetPSPName()
|
||||
}
|
||||
}
|
||||
|
||||
if allowedMutatedPod != nil {
|
||||
return allowedMutatedPod, allowingMutatingPSP, nil, nil
|
||||
}
|
||||
|
||||
// Pod is rejected. Filter the validation errors to only include errors from authorized PSPs.
|
||||
aggregate := field.ErrorList{}
|
||||
for psp, errs := range validationErrs {
|
||||
if isAuthorizedForPolicy(a.GetUserInfo(), saInfo, a.GetNamespace(), psp, c.authz) {
|
||||
aggregate = append(aggregate, errs...)
|
||||
}
|
||||
}
|
||||
return nil, "", aggregate, nil
|
||||
}
|
||||
|
||||
// assignSecurityContext creates a security context for each container in the pod
|
||||
// and validates that the sc falls within the psp constraints. All containers must validate against
|
||||
// the same psp or is not considered valid.
|
||||
func assignSecurityContext(provider psp.Provider, pod *api.Pod) field.ErrorList {
|
||||
errs := field.ErrorList{}
|
||||
|
||||
err := provider.DefaultPodSecurityContext(pod)
|
||||
if err != nil {
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "securityContext"), pod.Spec.SecurityContext, err.Error()))
|
||||
}
|
||||
|
||||
errs = append(errs, provider.ValidatePod(pod)...)
|
||||
|
||||
for i := range pod.Spec.InitContainers {
|
||||
err := provider.DefaultContainerSecurityContext(pod, &pod.Spec.InitContainers[i])
|
||||
if err != nil {
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "initContainers").Index(i).Child("securityContext"), "", err.Error()))
|
||||
continue
|
||||
}
|
||||
errs = append(errs, provider.ValidateContainer(pod, &pod.Spec.InitContainers[i], field.NewPath("spec", "initContainers").Index(i))...)
|
||||
}
|
||||
|
||||
for i := range pod.Spec.Containers {
|
||||
err := provider.DefaultContainerSecurityContext(pod, &pod.Spec.Containers[i])
|
||||
if err != nil {
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "containers").Index(i).Child("securityContext"), "", err.Error()))
|
||||
continue
|
||||
}
|
||||
errs = append(errs, provider.ValidateContainer(pod, &pod.Spec.Containers[i], field.NewPath("spec", "containers").Index(i))...)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createProvidersFromPolicies creates providers from the constraints supplied.
|
||||
func (c *PodSecurityPolicyPlugin) createProvidersFromPolicies(psps []*policyv1beta1.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
|
||||
var (
|
||||
// collected providers
|
||||
providers []psp.Provider
|
||||
// collected errors to return
|
||||
errs []error
|
||||
)
|
||||
|
||||
for _, constraint := range psps {
|
||||
provider, err := psp.NewSimpleProvider(constraint, namespace, c.strategyFactory)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("error creating provider for PSP %s: %v", constraint.Name, err))
|
||||
continue
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
return providers, errs
|
||||
}
|
||||
|
||||
func isAuthorizedForPolicy(user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
|
||||
// Check the service account first, as that is the more common use case.
|
||||
return authorizedForPolicy(sa, namespace, policyName, authz) ||
|
||||
authorizedForPolicy(user, namespace, policyName, authz)
|
||||
}
|
||||
|
||||
// authorizedForPolicy returns true if info is authorized to perform the "use" verb on the policy resource.
|
||||
// TODO: check against only the policy group when PSP will be completely moved out of the extensions
|
||||
func authorizedForPolicy(info user.Info, namespace string, policyName string, authz authorizer.Authorizer) bool {
|
||||
// Check against extensions API group for backward compatibility
|
||||
return authorizedForPolicyInAPIGroup(info, namespace, policyName, policy.GroupName, authz) ||
|
||||
authorizedForPolicyInAPIGroup(info, namespace, policyName, extensions.GroupName, authz)
|
||||
}
|
||||
|
||||
// authorizedForPolicyInAPIGroup returns true if info is authorized to perform the "use" verb on the policy resource in the specified API group.
|
||||
func authorizedForPolicyInAPIGroup(info user.Info, namespace, policyName, apiGroupName string, authz authorizer.Authorizer) bool {
|
||||
if info == nil {
|
||||
return false
|
||||
}
|
||||
attr := buildAttributes(info, namespace, policyName, apiGroupName)
|
||||
decision, reason, err := authz.Authorize(attr)
|
||||
if err != nil {
|
||||
klog.V(5).Infof("cannot authorize for policy: %v,%v", reason, err)
|
||||
}
|
||||
return (decision == authorizer.DecisionAllow)
|
||||
}
|
||||
|
||||
// buildAttributes builds an attributes record for a SAR based on the user info and policy.
|
||||
func buildAttributes(info user.Info, namespace, policyName, apiGroupName string) authorizer.Attributes {
|
||||
// check against the namespace that the pod is being created in to allow per-namespace PSP grants.
|
||||
attr := authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: "use",
|
||||
Namespace: namespace,
|
||||
Name: policyName,
|
||||
APIGroup: apiGroupName,
|
||||
Resource: "podsecuritypolicies",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
return attr
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,41 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/securitycontext/scdeny",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["admission_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 scdeny
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// PluginName indicates name of admission plugin.
|
||||
const PluginName = "SecurityContextDeny"
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
return NewSecurityContextDeny(), nil
|
||||
})
|
||||
}
|
||||
|
||||
// Plugin implements admission.Interface.
|
||||
type Plugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
|
||||
// NewSecurityContextDeny creates a new instance of the SecurityContextDeny admission controller
|
||||
func NewSecurityContextDeny() *Plugin {
|
||||
return &Plugin{
|
||||
Handler: admission.NewHandler(admission.Create, admission.Update),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate will deny any pod that defines SupplementalGroups, SELinuxOptions, RunAsUser or FSGroup
|
||||
func (p *Plugin) Validate(a admission.Attributes) (err error) {
|
||||
if a.GetSubresource() != "" || a.GetResource().GroupResource() != api.Resource("pods") {
|
||||
return nil
|
||||
}
|
||||
|
||||
pod, ok := a.GetObject().(*api.Pod)
|
||||
if !ok {
|
||||
return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
|
||||
}
|
||||
|
||||
if pod.Spec.SecurityContext != nil {
|
||||
if pod.Spec.SecurityContext.SupplementalGroups != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.SupplementalGroups is forbidden"))
|
||||
}
|
||||
if pod.Spec.SecurityContext.SELinuxOptions != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.SELinuxOptions is forbidden"))
|
||||
}
|
||||
if pod.Spec.SecurityContext.RunAsUser != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.RunAsUser is forbidden"))
|
||||
}
|
||||
if pod.Spec.SecurityContext.FSGroup != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("pod.Spec.SecurityContext.FSGroup is forbidden"))
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range pod.Spec.InitContainers {
|
||||
if v.SecurityContext != nil {
|
||||
if v.SecurityContext.SELinuxOptions != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden"))
|
||||
}
|
||||
if v.SecurityContext.RunAsUser != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range pod.Spec.Containers {
|
||||
if v.SecurityContext != nil {
|
||||
if v.SecurityContext.SELinuxOptions != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden"))
|
||||
}
|
||||
if v.SecurityContext.RunAsUser != nil {
|
||||
return apierrors.NewForbidden(a.GetResource().GroupResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 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 scdeny
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
// ensures the SecurityContext is denied if it defines anything more than Caps or Privileged
|
||||
func TestAdmission(t *testing.T) {
|
||||
handler := NewSecurityContextDeny()
|
||||
|
||||
runAsUser := int64(1)
|
||||
priv := true
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
sc *api.SecurityContext
|
||||
podSc *api.PodSecurityContext
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "unset",
|
||||
},
|
||||
{
|
||||
name: "empty container.SecurityContext",
|
||||
sc: &api.SecurityContext{},
|
||||
},
|
||||
{
|
||||
name: "empty pod.Spec.SecurityContext",
|
||||
podSc: &api.PodSecurityContext{},
|
||||
},
|
||||
{
|
||||
name: "valid container.SecurityContext",
|
||||
sc: &api.SecurityContext{Privileged: &priv, Capabilities: &api.Capabilities{}},
|
||||
},
|
||||
{
|
||||
name: "valid pod.Spec.SecurityContext",
|
||||
podSc: &api.PodSecurityContext{},
|
||||
},
|
||||
{
|
||||
name: "container.SecurityContext.RunAsUser",
|
||||
sc: &api.SecurityContext{RunAsUser: &runAsUser},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "container.SecurityContext.SELinuxOptions",
|
||||
sc: &api.SecurityContext{SELinuxOptions: &api.SELinuxOptions{}},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "pod.Spec.SecurityContext.RunAsUser",
|
||||
podSc: &api.PodSecurityContext{RunAsUser: &runAsUser},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "pod.Spec.SecurityContext.SELinuxOptions",
|
||||
podSc: &api.PodSecurityContext{SELinuxOptions: &api.SELinuxOptions{}},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
p := pod()
|
||||
p.Spec.SecurityContext = tc.podSc
|
||||
p.Spec.Containers[0].SecurityContext = tc.sc
|
||||
|
||||
err := handler.Validate(admission.NewAttributesRecord(p, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", false, nil))
|
||||
if err != nil && !tc.expectError {
|
||||
t.Errorf("%v: unexpected error: %v", tc.name, err)
|
||||
} else if err == nil && tc.expectError {
|
||||
t.Errorf("%v: expected error", tc.name)
|
||||
}
|
||||
|
||||
// verify init containers are also checked
|
||||
p = pod()
|
||||
p.Spec.SecurityContext = tc.podSc
|
||||
p.Spec.Containers[0].SecurityContext = tc.sc
|
||||
p.Spec.InitContainers = p.Spec.Containers
|
||||
p.Spec.Containers = nil
|
||||
|
||||
err = handler.Validate(admission.NewAttributesRecord(p, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", false, nil))
|
||||
if err != nil && !tc.expectError {
|
||||
t.Errorf("%v: unexpected error: %v", tc.name, err)
|
||||
} else if err == nil && tc.expectError {
|
||||
t.Errorf("%v: expected error", tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPodSecurityContextAdmission(t *testing.T) {
|
||||
handler := NewSecurityContextDeny()
|
||||
pod := api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fsGroup := int64(1001)
|
||||
|
||||
tests := []struct {
|
||||
securityContext api.PodSecurityContext
|
||||
errorExpected bool
|
||||
}{
|
||||
{
|
||||
securityContext: api.PodSecurityContext{},
|
||||
errorExpected: false,
|
||||
},
|
||||
{
|
||||
securityContext: api.PodSecurityContext{
|
||||
SupplementalGroups: []int64{int64(1234)},
|
||||
},
|
||||
errorExpected: true,
|
||||
},
|
||||
{
|
||||
securityContext: api.PodSecurityContext{
|
||||
FSGroup: &fsGroup,
|
||||
},
|
||||
errorExpected: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
pod.Spec.SecurityContext = &test.securityContext
|
||||
err := handler.Validate(admission.NewAttributesRecord(&pod, nil, api.Kind("Pod").WithVersion("version"), "foo", "name", api.Resource("pods").WithVersion("version"), "", "ignored", false, nil))
|
||||
|
||||
if test.errorExpected && err == nil {
|
||||
t.Errorf("Expected error for security context %+v but did not get an error", test.securityContext)
|
||||
}
|
||||
|
||||
if !test.errorExpected && err != nil {
|
||||
t.Errorf("Unexpected error %v for security context %+v", err, test.securityContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandles(t *testing.T) {
|
||||
handler := NewSecurityContextDeny()
|
||||
tests := map[admission.Operation]bool{
|
||||
admission.Update: true,
|
||||
admission.Create: true,
|
||||
admission.Delete: false,
|
||||
admission.Connect: false,
|
||||
}
|
||||
for op, expected := range tests {
|
||||
result := handler.Handles(op)
|
||||
if result != expected {
|
||||
t.Errorf("Unexpected result for operation %s: %v\n", op, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pod() *api.Pod {
|
||||
return &api.Pod{
|
||||
Spec: api.PodSpec{
|
||||
Containers: []api.Container{
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["admission.go"],
|
||||
importpath = "k8s.io/kubernetes/plugin/pkg/admission/storage/storageobjectinuseprotection",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//pkg/volume/util:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"admission_test.go",
|
||||
"main_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/features:go_default_library",
|
||||
"//pkg/volume/util:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
|
||||
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
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 storageobjectinuseprotection
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/util/feature"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName is the name of this admission controller plugin
|
||||
PluginName = "StorageObjectInUseProtection"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
||||
plugin := newPlugin()
|
||||
return plugin, nil
|
||||
})
|
||||
}
|
||||
|
||||
// storageProtectionPlugin holds state for and implements the admission plugin.
|
||||
type storageProtectionPlugin struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
var _ admission.Interface = &storageProtectionPlugin{}
|
||||
|
||||
// newPlugin creates a new admission plugin.
|
||||
func newPlugin() *storageProtectionPlugin {
|
||||
return &storageProtectionPlugin{
|
||||
Handler: admission.NewHandler(admission.Create),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
pvResource = api.Resource("persistentvolumes")
|
||||
pvcResource = api.Resource("persistentvolumeclaims")
|
||||
)
|
||||
|
||||
// Admit sets finalizer on all PVCs(PVs). The finalizer is removed by
|
||||
// PVCProtectionController(PVProtectionController) when it's not referenced.
|
||||
//
|
||||
// This prevents users from deleting a PVC that's used by a running pod.
|
||||
// This also prevents admin from deleting a PV that's bound by a PVC
|
||||
func (c *storageProtectionPlugin) Admit(a admission.Attributes) error {
|
||||
if !feature.DefaultFeatureGate.Enabled(features.StorageObjectInUseProtection) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch a.GetResource().GroupResource() {
|
||||
case pvResource:
|
||||
return c.admitPV(a)
|
||||
case pvcResource:
|
||||
return c.admitPVC(a)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *storageProtectionPlugin) admitPV(a admission.Attributes) error {
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pv, ok := a.GetObject().(*api.PersistentVolume)
|
||||
// if we can't convert the obj to PV, just return
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, f := range pv.Finalizers {
|
||||
if f == volumeutil.PVProtectionFinalizer {
|
||||
// Finalizer is already present, nothing to do
|
||||
return nil
|
||||
}
|
||||
}
|
||||
klog.V(4).Infof("adding PV protection finalizer to %s", pv.Name)
|
||||
pv.Finalizers = append(pv.Finalizers, volumeutil.PVProtectionFinalizer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *storageProtectionPlugin) admitPVC(a admission.Attributes) error {
|
||||
if len(a.GetSubresource()) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pvc, ok := a.GetObject().(*api.PersistentVolumeClaim)
|
||||
// if we can't convert the obj to PVC, just return
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range pvc.Finalizers {
|
||||
if f == volumeutil.PVCProtectionFinalizer {
|
||||
// Finalizer is already present, nothing to do
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
klog.V(4).Infof("adding PVC protection finalizer to %s/%s", pvc.Namespace, pvc.Name)
|
||||
pvc.Finalizers = append(pvc.Finalizers, volumeutil.PVCProtectionFinalizer)
|
||||
return nil
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
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 storageobjectinuseprotection
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
volumeutil "k8s.io/kubernetes/pkg/volume/util"
|
||||
)
|
||||
|
||||
func TestAdmit(t *testing.T) {
|
||||
claim := &api.PersistentVolumeClaim{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "PersistentVolumeClaim",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "claim",
|
||||
Namespace: "ns",
|
||||
},
|
||||
}
|
||||
|
||||
pv := &api.PersistentVolume{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "PersistentVolume",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pv",
|
||||
},
|
||||
}
|
||||
claimWithFinalizer := claim.DeepCopy()
|
||||
claimWithFinalizer.Finalizers = []string{volumeutil.PVCProtectionFinalizer}
|
||||
|
||||
pvWithFinalizer := pv.DeepCopy()
|
||||
pvWithFinalizer.Finalizers = []string{volumeutil.PVProtectionFinalizer}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
resource schema.GroupVersionResource
|
||||
object runtime.Object
|
||||
expectedObject runtime.Object
|
||||
featureEnabled bool
|
||||
namespace string
|
||||
}{
|
||||
{
|
||||
"create -> add finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
claim,
|
||||
claimWithFinalizer,
|
||||
true,
|
||||
claim.Namespace,
|
||||
},
|
||||
{
|
||||
"finalizer already exists -> no new finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
claimWithFinalizer,
|
||||
claimWithFinalizer,
|
||||
true,
|
||||
claimWithFinalizer.Namespace,
|
||||
},
|
||||
{
|
||||
"disabled feature -> no finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
|
||||
claim,
|
||||
claim,
|
||||
false,
|
||||
claim.Namespace,
|
||||
},
|
||||
{
|
||||
"create -> add finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumes"),
|
||||
pv,
|
||||
pvWithFinalizer,
|
||||
true,
|
||||
pv.Namespace,
|
||||
},
|
||||
{
|
||||
"finalizer already exists -> no new finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumes"),
|
||||
pvWithFinalizer,
|
||||
pvWithFinalizer,
|
||||
true,
|
||||
pvWithFinalizer.Namespace,
|
||||
},
|
||||
{
|
||||
"disabled feature -> no finalizer",
|
||||
api.SchemeGroupVersion.WithResource("persistentvolumes"),
|
||||
pv,
|
||||
pv,
|
||||
false,
|
||||
pv.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := newPlugin()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StorageObjectInUseProtection, test.featureEnabled)()
|
||||
obj := test.object.DeepCopyObject()
|
||||
attrs := admission.NewAttributesRecord(
|
||||
obj, // new object
|
||||
obj.DeepCopyObject(), // old object, copy to be sure it's not modified
|
||||
schema.GroupVersionKind{},
|
||||
test.namespace,
|
||||
"foo",
|
||||
test.resource,
|
||||
"", // subresource
|
||||
admission.Create,
|
||||
false, // dryRun
|
||||
nil, // userInfo
|
||||
)
|
||||
|
||||
err := ctrl.Admit(attrs)
|
||||
if err != nil {
|
||||
t.Errorf("Test %q: got unexpected error: %v", test.name, err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedObject, obj) {
|
||||
t.Errorf("Test %q: Expected object:\n%s\ngot:\n%s", test.name, spew.Sdump(test.expectedObject), spew.Sdump(obj))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 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 storageobjectinuseprotection
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
_ "k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
utilfeaturetesting.VerifyFeatureGatesUnchanged(utilfeature.DefaultFeatureGate, m.Run)
|
||||
}
|
Loading…
Reference in New Issue