Commit efd3ddac authored by Darren Shepherd's avatar Darren Shepherd

Remove admission controllers

parent fe40b4a4
......@@ -21,30 +21,15 @@ 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/noderestriction"
"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"
......@@ -57,63 +42,32 @@ import (
// 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
noderestriction.PluginName, // NodeRestriction
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)
noderestriction.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.
......
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"],
)
/*
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, o admission.ObjectInterfaces) (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, o admission.ObjectInterfaces) (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)
}
/*
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), nil)
if err != nil {
t.Errorf("Unexpected error returned from admission handler")
}
}
func TestAdmissionNilAttribute(t *testing.T) {
handler := NewAlwaysAdmit()
err := handler.(*alwaysAdmit).Admit(nil, 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)
}
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) (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, o admission.ObjectInterfaces) (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),
}
}
/*
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), 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), 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), 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)
}
}
}
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",
"//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/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/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",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)
/*
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"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apiserver/pkg/admission"
api "k8s.io/kubernetes/pkg/apis/core"
)
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 v1.LabelHostname i.e. "kubernetes.io/hostname"
// in requiredDuringSchedulingRequiredDuringExecution and requiredDuringSchedulingIgnoredDuringExecution.
func (p *Plugin) Validate(attributes admission.Attributes, o admission.ObjectInterfaces) (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 != v1.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, v1.LabelHostname))
}
}
}
return nil
}
/*
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"
"k8s.io/api/core/v1"
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"
)
// 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: v1.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: v1.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: v1.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: v1.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), 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), 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
}
}
}
/*
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 v1.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"
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"],
)
/*
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, o admission.ObjectInterfaces) (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, o admission.ObjectInterfaces) (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)
}
/*
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), 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)
}
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) (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
}
/*
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, nil)
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)
}
}
}
})
}
}
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"],
)
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- deads2k
- derekwaynecarr
approvers:
- deads2k
- derekwaynecarr
- smarterclayton
\ No newline at end of file
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:deepcopy-gen=package
package eventratelimit // import "k8s.io/kubernetes/plugin/pkg/admission/eventratelimit/apis/eventratelimit"
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"],
)
/*
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))
}
/*
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 is the scheme builder with scheme init functions to run for this API package
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
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
}
/*
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"`
}
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"],
)
/*
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) {}
/*
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"
/*
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
}
/*
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"`
}
// +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)
}
// +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
}
// +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)
}
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"],
)
/*
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
}
/*
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)
}
}
}
}
// +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
}
/*
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)
}
/*
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)
}
}
}
/*
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)
}
/*
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
}
/*
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"
/*
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,
}, "")
}
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",
"//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/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"],
)
/*
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"
"k8s.io/klog"
)
const (
// DenyEscalatingExec indicates name of admission plugin.
// Deprecated, will be removed in v1.18.
// Use of PodSecurityPolicy or a custom admission plugin to limit creation of pods is recommended instead.
DenyEscalatingExec = "DenyEscalatingExec"
// DenyExecOnPrivileged indicates name of admission plugin.
// Deprecated, will be removed in v1.18.
// Use of PodSecurityPolicy or a custom admission plugin to limit creation of pods is recommended instead.
DenyExecOnPrivileged = "DenyExecOnPrivileged"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(DenyEscalatingExec, func(config io.Reader) (admission.Interface, error) {
klog.Warningf("the %s admission plugin is deprecated and will be removed in v1.18", DenyEscalatingExec)
klog.Warningf("use of PodSecurityPolicy or a custom admission plugin to limit creation of pods is recommended instead")
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) {
klog.Warningf("the %s admission plugin is deprecated and will be removed in v1.18", DenyExecOnPrivileged)
klog.Warningf("use of PodSecurityPolicy or a custom admission plugin to limit creation of pods is recommended instead")
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, o admission.ObjectInterfaces) (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
}
/*
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), 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), 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"},
},
},
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) 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
}
/*
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), 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)
}
}
}
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/apps/v1: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/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"],
)
/*
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, o admission.ObjectInterfaces) (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
}
/*
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"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
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{})
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: appsv1.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, nil)
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: "apps/v1",
Kind: "DaemonSet",
Name: "ds1",
BlockOwnerDeletion: getTrueVar(),
}
notBlockDS1 := metav1.OwnerReference{
APIVersion: "apps/v1",
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, nil)
if !tc.checkError(err) {
t.Errorf("%v: unexpected err: %v", tc.name, err)
}
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) 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.CoreV1().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
}
/*
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)
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), 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), 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), 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), 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), nil)
if err != nil {
t.Errorf("unexpected error returned from admission handler")
}
if !hasCreateNamespaceAction(mockClient) {
t.Errorf("expected create namespace action")
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) 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.CoreV1().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
}
/*
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)
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), 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), 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)
}
}
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",
"//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"],
)
/*
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"
)
// 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, o admission.ObjectInterfaces) 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"))
}
// 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, o)
}
// Validate ensures that the pod node selector is allowed
func (p *podNodeSelector) Validate(a admission.Attributes, o admission.ObjectInterfaces) 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),
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.CoreV1().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
}
/*
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"},
}
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), 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), 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: false,
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)
}
}
}
// 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)
pluginInitializer.Initialize(handler)
err := admission.ValidateInitialization(handler)
return handler, f, err
}
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"],
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/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"],
)
/*
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"
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, o admission.ObjectInterfaces) 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
if a.GetOperation() == admission.Create {
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, o)
}
func (p *podTolerationsPlugin) Validate(a admission.Attributes, o admission.ObjectInterfaces) 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
}
/*
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
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), 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), 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)
pluginInitializer.Initialize(handler)
err = admission.ValidateInitialization(handler)
return handler, f, err
}
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"],
)
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
approvers:
/*
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"
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"],
)
/*
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))
}
/*
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 is the scheme builder with scheme init functions to run for this API package
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
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
}
/*
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
}
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"],
)
/*
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)
}
/*
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"
/*
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
}
/*
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"`
}
// +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)
}
// +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
}
// +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
}
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",
],
)
/*
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
}
/*
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)
}
}
}
// +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
}
/*
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
}
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"],
)
/*
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 security contains admission plugins specific to cluster security.
package security // import "k8s.io/kubernetes/plugin/pkg/admission/security"
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"],
)
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- sig-auth-policy-approvers
reviewers:
- sig-auth-policy-reviewers
labels:
- sig/auth
/*
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, o admission.ObjectInterfaces) 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, o admission.ObjectInterfaces) 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
}
/*
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"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1beta1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
kadmission "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/client-go/informers"
"k8s.io/kubernetes/pkg/api/legacyscheme"
kapi "k8s.io/kubernetes/pkg/apis/core"
k8s_api_v1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/security/apparmor"
kpsp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
"k8s.io/kubernetes/pkg/security/podsecuritypolicy/seccomp"
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
utilpointer "k8s.io/utils/pointer"
)
const defaultContainerName = "test-c"
// NewTestAdmission provides an admission plugin with test implementations of internal structs.
func NewTestAdmission(psps []*policy.PodSecurityPolicy, authz authorizer.Authorizer) *PodSecurityPolicyPlugin {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
store := informerFactory.Policy().V1beta1().PodSecurityPolicies().Informer().GetStore()
for _, psp := range psps {
store.Add(psp)
}
lister := informerFactory.Policy().V1beta1().PodSecurityPolicies().Lister()
if authz == nil {
authz = &TestAuthorizer{}
}
return &PodSecurityPolicyPlugin{
Handler: kadmission.NewHandler(kadmission.Create, kadmission.Update),
strategyFactory: kpsp.NewSimpleStrategyFactory(),
authz: authz,
lister: lister,
}
}
// TestAuthorizer is a testing struct for testing that fulfills the authorizer interface.
type TestAuthorizer struct {
// usernameToNamespaceToAllowedPSPs contains the map of allowed PSPs.
// if nil, all PSPs are allowed.
usernameToNamespaceToAllowedPSPs map[string]map[string]map[string]bool
// allowedAPIGroupName specifies an API Group name that contains PSP resources.
// In order to be authorized, AttributesRecord must have this group name.
// When empty, API Group name isn't taken into account.
// TODO: remove this when PSP will be completely moved out of the extensions and we'll lookup only in "policy" group.
allowedAPIGroupName string
}
func (t *TestAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if t.usernameToNamespaceToAllowedPSPs == nil {
return authorizer.DecisionAllow, "", nil
}
allowedInNamespace := t.usernameToNamespaceToAllowedPSPs[a.GetUser().GetName()][a.GetNamespace()][a.GetName()]
allowedClusterWide := t.usernameToNamespaceToAllowedPSPs[a.GetUser().GetName()][""][a.GetName()]
allowedAPIGroup := len(t.allowedAPIGroupName) == 0 || a.GetAPIGroup() == t.allowedAPIGroupName
if allowedAPIGroup && (allowedInNamespace || allowedClusterWide) {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionNoOpinion, "", nil
}
var _ authorizer.Authorizer = &TestAuthorizer{}
func useInitContainers(pod *kapi.Pod) *kapi.Pod {
pod.Spec.InitContainers = pod.Spec.Containers
pod.Spec.Containers = []kapi.Container{}
return pod
}
func TestAdmitSeccomp(t *testing.T) {
containerName := "container"
tests := map[string]struct {
pspAnnotations map[string]string
podAnnotations map[string]string
shouldPassAdmit bool
shouldPassValidate bool
}{
"no seccomp, no pod annotations": {
pspAnnotations: nil,
podAnnotations: nil,
shouldPassAdmit: true,
shouldPassValidate: true,
},
"no seccomp, pod annotations": {
pspAnnotations: nil,
podAnnotations: map[string]string{
kapi.SeccompPodAnnotationKey: "foo",
},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"no seccomp, container annotations": {
pspAnnotations: nil,
podAnnotations: map[string]string{
kapi.SeccompContainerAnnotationKeyPrefix + containerName: "foo",
},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"seccomp, allow any no pod annotation": {
pspAnnotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny,
},
podAnnotations: nil,
shouldPassAdmit: true,
shouldPassValidate: true,
},
"seccomp, allow any pod annotation": {
pspAnnotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny,
},
podAnnotations: map[string]string{
kapi.SeccompPodAnnotationKey: "foo",
},
shouldPassAdmit: true,
shouldPassValidate: true,
},
"seccomp, allow any container annotation": {
pspAnnotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: seccomp.AllowAny,
},
podAnnotations: map[string]string{
kapi.SeccompContainerAnnotationKeyPrefix + containerName: "foo",
},
shouldPassAdmit: true,
shouldPassValidate: true,
},
"seccomp, allow specific pod annotation failure": {
pspAnnotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: "foo",
},
podAnnotations: map[string]string{
kapi.SeccompPodAnnotationKey: "bar",
},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"seccomp, allow specific container annotation failure": {
pspAnnotations: map[string]string{
// provide a default so we don't have to give the pod annotation
seccomp.DefaultProfileAnnotationKey: "foo",
seccomp.AllowedProfilesAnnotationKey: "foo",
},
podAnnotations: map[string]string{
kapi.SeccompContainerAnnotationKeyPrefix + containerName: "bar",
},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"seccomp, allow specific pod annotation pass": {
pspAnnotations: map[string]string{
seccomp.AllowedProfilesAnnotationKey: "foo",
},
podAnnotations: map[string]string{
kapi.SeccompPodAnnotationKey: "foo",
},
shouldPassAdmit: true,
shouldPassValidate: true,
},
"seccomp, allow specific container annotation pass": {
pspAnnotations: map[string]string{
// provide a default so we don't have to give the pod annotation
seccomp.DefaultProfileAnnotationKey: "foo",
seccomp.AllowedProfilesAnnotationKey: "foo,bar",
},
podAnnotations: map[string]string{
kapi.SeccompContainerAnnotationKeyPrefix + containerName: "bar",
},
shouldPassAdmit: true,
shouldPassValidate: true,
},
}
for k, v := range tests {
psp := restrictivePSP()
psp.Annotations = v.pspAnnotations
pod := &kapi.Pod{
ObjectMeta: metav1.ObjectMeta{
Annotations: v.podAnnotations,
},
Spec: kapi.PodSpec{
Containers: []kapi.Container{
{Name: containerName},
},
},
}
testPSPAdmit(k, []*policy.PodSecurityPolicy{psp}, pod, v.shouldPassAdmit, v.shouldPassValidate, psp.Name, t)
}
}
func TestAdmitPrivileged(t *testing.T) {
createPodWithPriv := func(priv bool) *kapi.Pod {
pod := goodPod()
pod.Spec.Containers[0].SecurityContext.Privileged = &priv
return pod
}
nonPrivilegedPSP := restrictivePSP()
nonPrivilegedPSP.Name = "non-priv"
nonPrivilegedPSP.Spec.Privileged = false
privilegedPSP := restrictivePSP()
privilegedPSP.Name = "priv"
privilegedPSP.Spec.Privileged = true
trueValue := true
falseValue := false
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPriv *bool
expectedPSP string
}{
"pod with priv=nil allowed under non priv PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{nonPrivilegedPSP},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPriv: nil,
expectedPSP: nonPrivilegedPSP.Name,
},
"pod with priv=nil allowed under priv PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{privilegedPSP},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPriv: nil,
expectedPSP: privilegedPSP.Name,
},
"pod with priv=false allowed under non priv PSP": {
pod: createPodWithPriv(false),
psps: []*policy.PodSecurityPolicy{nonPrivilegedPSP},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPriv: &falseValue,
expectedPSP: nonPrivilegedPSP.Name,
},
"pod with priv=false allowed under priv PSP": {
pod: createPodWithPriv(false),
psps: []*policy.PodSecurityPolicy{privilegedPSP},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPriv: &falseValue,
expectedPSP: privilegedPSP.Name,
},
"pod with priv=true denied by non priv PSP": {
pod: createPodWithPriv(true),
psps: []*policy.PodSecurityPolicy{nonPrivilegedPSP},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with priv=true allowed by priv PSP": {
pod: createPodWithPriv(true),
psps: []*policy.PodSecurityPolicy{nonPrivilegedPSP, privilegedPSP},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPriv: &trueValue,
expectedPSP: privilegedPSP.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
priv := v.pod.Spec.Containers[0].SecurityContext.Privileged
if (priv == nil) != (v.expectedPriv == nil) {
t.Errorf("%s expected privileged to be %v, got %v", k, v.expectedPriv, priv)
} else if priv != nil && *priv != *v.expectedPriv {
t.Errorf("%s expected privileged to be %v, got %v", k, *v.expectedPriv, *priv)
}
}
}
}
func defaultPod(t *testing.T, pod *kapi.Pod) *kapi.Pod {
v1Pod := &v1.Pod{}
if err := legacyscheme.Scheme.Convert(pod, v1Pod, nil); err != nil {
t.Fatal(err)
}
legacyscheme.Scheme.Default(v1Pod)
apiPod := &kapi.Pod{}
if err := legacyscheme.Scheme.Convert(v1Pod, apiPod, nil); err != nil {
t.Fatal(err)
}
return apiPod
}
func TestAdmitPreferNonmutating(t *testing.T) {
mutating1 := restrictivePSP()
mutating1.Name = "mutating1"
mutating1.Spec.RunAsUser.Ranges = []policy.IDRange{{Min: int64(1), Max: int64(1)}}
mutating2 := restrictivePSP()
mutating2.Name = "mutating2"
mutating2.Spec.RunAsUser.Ranges = []policy.IDRange{{Min: int64(2), Max: int64(2)}}
privilegedPSP := permissivePSP()
privilegedPSP.Name = "privileged"
unprivilegedRunAsAnyPod := defaultPod(t, &kapi.Pod{
ObjectMeta: metav1.ObjectMeta{},
Spec: kapi.PodSpec{
ServiceAccountName: "default",
Containers: []kapi.Container{{Name: "mycontainer", Image: "myimage"}},
},
})
changedPod := unprivilegedRunAsAnyPod.DeepCopy()
changedPod.Spec.Containers[0].Image = "myimage2"
podWithSC := unprivilegedRunAsAnyPod.DeepCopy()
podWithSC.Annotations = map[string]string{psputil.ValidatedPSPAnnotation: privilegedPSP.Name}
changedPodWithSC := changedPod.DeepCopy()
changedPodWithSC.Annotations = map[string]string{psputil.ValidatedPSPAnnotation: privilegedPSP.Name}
gcChangedPod := unprivilegedRunAsAnyPod.DeepCopy()
gcChangedPod.OwnerReferences = []metav1.OwnerReference{{Kind: "Foo", Name: "bar"}}
gcChangedPod.Finalizers = []string{"foo"}
podWithAnnotation := unprivilegedRunAsAnyPod.DeepCopy()
podWithAnnotation.ObjectMeta.Annotations = map[string]string{
// "mutating2" is lexicographically behind "mutating1", so "mutating1" should be
// chosen because it's the canonical PSP order.
psputil.ValidatedPSPAnnotation: mutating2.Name,
}
tests := map[string]struct {
operation kadmission.Operation
pod *kapi.Pod
podBeforeUpdate *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassValidate bool
expectMutation bool
expectedContainerUser *int64
expectedPSP string
}{
"pod should not be mutated by allow-all strategies": {
operation: kadmission.Create,
pod: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{privilegedPSP},
shouldPassValidate: true,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: privilegedPSP.Name,
},
"pod should prefer non-mutating PSP on create": {
operation: kadmission.Create,
pod: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1, privilegedPSP},
shouldPassValidate: true,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: privilegedPSP.Name,
},
"pod should use deterministic mutating PSP on create": {
operation: kadmission.Create,
pod: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1},
shouldPassValidate: true,
expectMutation: true,
expectedContainerUser: &mutating1.Spec.RunAsUser.Ranges[0].Min,
expectedPSP: mutating1.Name,
},
"pod should use deterministic mutating PSP on create even if ValidatedPSPAnnotation is set": {
operation: kadmission.Create,
pod: podWithAnnotation,
psps: []*policy.PodSecurityPolicy{mutating2, mutating1},
shouldPassValidate: true,
expectMutation: true,
expectedContainerUser: &mutating1.Spec.RunAsUser.Ranges[0].Min,
expectedPSP: mutating1.Name,
},
"pod should prefer non-mutating PSP on update": {
operation: kadmission.Update,
pod: changedPodWithSC.DeepCopy(),
podBeforeUpdate: podWithSC.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1, privilegedPSP},
shouldPassValidate: true,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: privilegedPSP.Name,
},
"pod should not mutate on update, but fail validation": {
operation: kadmission.Update,
pod: changedPod.DeepCopy(),
podBeforeUpdate: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1},
shouldPassValidate: false,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: "",
},
"pod should be allowed if completely unchanged on update": {
operation: kadmission.Update,
pod: unprivilegedRunAsAnyPod.DeepCopy(),
podBeforeUpdate: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1},
shouldPassValidate: true,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: "",
},
"pod should be allowed if unchanged on update except finalizers,ownerrefs": {
operation: kadmission.Update,
pod: gcChangedPod.DeepCopy(),
podBeforeUpdate: unprivilegedRunAsAnyPod.DeepCopy(),
psps: []*policy.PodSecurityPolicy{mutating2, mutating1},
shouldPassValidate: true,
expectMutation: false,
expectedContainerUser: nil,
expectedPSP: "",
},
}
for k, v := range tests {
testPSPAdmitAdvanced(k, v.operation, v.psps, nil, &user.DefaultInfo{}, v.pod, v.podBeforeUpdate, true, v.shouldPassValidate, v.expectMutation, v.expectedPSP, t)
actualPodUser := (*int64)(nil)
if v.pod.Spec.SecurityContext != nil {
actualPodUser = v.pod.Spec.SecurityContext.RunAsUser
}
if actualPodUser != nil {
t.Errorf("%s expected pod user nil, got %v", k, *actualPodUser)
}
actualContainerUser := (*int64)(nil)
if v.pod.Spec.Containers[0].SecurityContext != nil {
actualContainerUser = v.pod.Spec.Containers[0].SecurityContext.RunAsUser
}
if (actualContainerUser == nil) != (v.expectedContainerUser == nil) {
t.Errorf("%s expected container user %v, got %v", k, v.expectedContainerUser, actualContainerUser)
} else if actualContainerUser != nil && *actualContainerUser != *v.expectedContainerUser {
t.Errorf("%s expected container user %v, got %v", k, *v.expectedContainerUser, *actualContainerUser)
}
}
}
func TestFailClosedOnInvalidPod(t *testing.T) {
plugin := NewTestAdmission(nil, nil)
pod := &v1.Pod{}
attrs := kadmission.NewAttributesRecord(pod, nil, kapi.Kind("Pod").WithVersion("version"), pod.Namespace, pod.Name, kapi.Resource("pods").WithVersion("version"), "", kadmission.Create, false, &user.DefaultInfo{})
err := plugin.Admit(attrs, nil)
if err == nil {
t.Fatalf("expected versioned pod object to fail mutating admission")
}
if !strings.Contains(err.Error(), "unexpected type") {
t.Errorf("expected type error on Admit but got: %v", err)
}
err = plugin.Validate(attrs, nil)
if err == nil {
t.Fatalf("expected versioned pod object to fail validating admission")
}
if !strings.Contains(err.Error(), "unexpected type") {
t.Errorf("expected type error on Validate but got: %v", err)
}
}
func TestAdmitCaps(t *testing.T) {
createPodWithCaps := func(caps *kapi.Capabilities) *kapi.Pod {
pod := goodPod()
pod.Spec.Containers[0].SecurityContext.Capabilities = caps
return pod
}
restricted := restrictivePSP()
allowsFooInAllowed := restrictivePSP()
allowsFooInAllowed.Name = "allowCapInAllowed"
allowsFooInAllowed.Spec.AllowedCapabilities = []v1.Capability{"foo"}
allowsFooInRequired := restrictivePSP()
allowsFooInRequired.Name = "allowCapInRequired"
allowsFooInRequired.Spec.DefaultAddCapabilities = []v1.Capability{"foo"}
requiresFooToBeDropped := restrictivePSP()
requiresFooToBeDropped.Name = "requireDrop"
requiresFooToBeDropped.Spec.RequiredDropCapabilities = []v1.Capability{"foo"}
allowAllInAllowed := restrictivePSP()
allowAllInAllowed.Name = "allowAllCapsInAllowed"
allowAllInAllowed.Spec.AllowedCapabilities = []v1.Capability{policy.AllowAllCapabilities}
tc := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedCapabilities *kapi.Capabilities
expectedPSP string
}{
// UC 1: if a PSP does not define allowed or required caps then a pod requesting a cap
// should be rejected.
"should reject cap add when not allowed or required": {
pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{restricted},
shouldPassAdmit: false,
shouldPassValidate: false,
},
// UC 2: if a PSP allows a cap in the allowed field it should accept the pod request
// to add the cap.
"should accept cap add when in allowed": {
pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{restricted, allowsFooInAllowed},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: allowsFooInAllowed.Name,
},
// UC 3: if a PSP requires a cap then it should accept the pod request
// to add the cap.
"should accept cap add when in required": {
pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{restricted, allowsFooInRequired},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: allowsFooInRequired.Name,
},
// UC 4: if a PSP requires a cap to be dropped then it should fail both
// in the verification of adds and verification of drops
"should reject cap add when requested cap is required to be dropped": {
pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{restricted, requiresFooToBeDropped},
shouldPassAdmit: false,
shouldPassValidate: false,
},
// UC 5: if a PSP requires a cap to be dropped it should accept
// a manual request to drop the cap.
"should accept cap drop when cap is required to be dropped": {
pod: createPodWithCaps(&kapi.Capabilities{Drop: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{requiresFooToBeDropped},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: requiresFooToBeDropped.Name,
},
// UC 6: required add is defaulted
"required add is defaulted": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{allowsFooInRequired},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedCapabilities: &kapi.Capabilities{
Add: []kapi.Capability{"foo"},
},
expectedPSP: allowsFooInRequired.Name,
},
// UC 7: required drop is defaulted
"required drop is defaulted": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{requiresFooToBeDropped},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedCapabilities: &kapi.Capabilities{
Drop: []kapi.Capability{"foo"},
},
expectedPSP: requiresFooToBeDropped.Name,
},
// UC 8: using '*' in allowed caps
"should accept cap add when all caps are allowed": {
pod: createPodWithCaps(&kapi.Capabilities{Add: []kapi.Capability{"foo"}}),
psps: []*policy.PodSecurityPolicy{restricted, allowAllInAllowed},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: allowAllInAllowed.Name,
},
}
for k, v := range tc {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.expectedCapabilities != nil {
if !reflect.DeepEqual(v.expectedCapabilities, v.pod.Spec.Containers[0].SecurityContext.Capabilities) {
t.Errorf("%s resulted in caps that were not expected - expected: %v, received: %v", k, v.expectedCapabilities, v.pod.Spec.Containers[0].SecurityContext.Capabilities)
}
}
}
for k, v := range tc {
useInitContainers(v.pod)
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.expectedCapabilities != nil {
if !reflect.DeepEqual(v.expectedCapabilities, v.pod.Spec.InitContainers[0].SecurityContext.Capabilities) {
t.Errorf("%s resulted in caps that were not expected - expected: %v, received: %v", k, v.expectedCapabilities, v.pod.Spec.InitContainers[0].SecurityContext.Capabilities)
}
}
}
}
func TestAdmitVolumes(t *testing.T) {
val := reflect.ValueOf(kapi.VolumeSource{})
for i := 0; i < val.NumField(); i++ {
// reflectively create the volume source
fieldVal := val.Type().Field(i)
volumeSource := kapi.VolumeSource{}
volumeSourceVolume := reflect.New(fieldVal.Type.Elem())
reflect.ValueOf(&volumeSource).Elem().FieldByName(fieldVal.Name).Set(volumeSourceVolume)
volume := kapi.Volume{VolumeSource: volumeSource}
// sanity check before moving on
fsType, err := psputil.GetVolumeFSType(volume)
if err != nil {
t.Errorf("error getting FSType for %s: %s", fieldVal.Name, err.Error())
continue
}
// add the volume to the pod
pod := goodPod()
pod.Spec.Volumes = []kapi.Volume{volume}
// create a PSP that allows no volumes
psp := restrictivePSP()
// expect a denial for this PSP
testPSPAdmit(fmt.Sprintf("%s denial", string(fsType)), []*policy.PodSecurityPolicy{psp}, pod, false, false, "", t)
// also expect a denial for this PSP if it's an init container
useInitContainers(pod)
testPSPAdmit(fmt.Sprintf("%s denial", string(fsType)), []*policy.PodSecurityPolicy{psp}, pod, false, false, "", t)
// now add the fstype directly to the psp and it should validate
psp.Spec.Volumes = []policy.FSType{fsType}
testPSPAdmit(fmt.Sprintf("%s direct accept", string(fsType)), []*policy.PodSecurityPolicy{psp}, pod, true, true, psp.Name, t)
// now change the psp to allow any volumes and the pod should still validate
psp.Spec.Volumes = []policy.FSType{policy.All}
testPSPAdmit(fmt.Sprintf("%s wildcard accept", string(fsType)), []*policy.PodSecurityPolicy{psp}, pod, true, true, psp.Name, t)
}
}
func TestAdmitHostNetwork(t *testing.T) {
createPodWithHostNetwork := func(hostNetwork bool) *kapi.Pod {
pod := goodPod()
pod.Spec.SecurityContext.HostNetwork = hostNetwork
return pod
}
noHostNetwork := restrictivePSP()
noHostNetwork.Name = "no-hostnetwork"
noHostNetwork.Spec.HostNetwork = false
hostNetwork := restrictivePSP()
hostNetwork.Name = "hostnetwork"
hostNetwork.Spec.HostNetwork = true
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedHostNetwork bool
expectedPSP string
}{
"pod without hostnetwork request allowed under noHostNetwork PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{noHostNetwork},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostNetwork: false,
expectedPSP: noHostNetwork.Name,
},
"pod without hostnetwork request allowed under hostNetwork PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{hostNetwork},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostNetwork: false,
expectedPSP: hostNetwork.Name,
},
"pod with hostnetwork request denied by noHostNetwork PSP": {
pod: createPodWithHostNetwork(true),
psps: []*policy.PodSecurityPolicy{noHostNetwork},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with hostnetwork request allowed by hostNetwork PSP": {
pod: createPodWithHostNetwork(true),
psps: []*policy.PodSecurityPolicy{noHostNetwork, hostNetwork},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostNetwork: true,
expectedPSP: hostNetwork.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.SecurityContext.HostNetwork != v.expectedHostNetwork {
t.Errorf("%s expected hostNetwork to be %t", k, v.expectedHostNetwork)
}
}
}
// test again with init containers
for k, v := range tests {
useInitContainers(v.pod)
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.SecurityContext.HostNetwork != v.expectedHostNetwork {
t.Errorf("%s expected hostNetwork to be %t", k, v.expectedHostNetwork)
}
}
}
}
func TestAdmitHostPorts(t *testing.T) {
createPodWithHostPorts := func(port int32) *kapi.Pod {
pod := goodPod()
pod.Spec.Containers[0].Ports = []kapi.ContainerPort{
{HostPort: port},
}
return pod
}
noHostPorts := restrictivePSP()
noHostPorts.Name = "noHostPorts"
hostPorts := restrictivePSP()
hostPorts.Name = "hostPorts"
hostPorts.Spec.HostPorts = []policy.HostPortRange{
{Min: 1, Max: 10},
}
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPSP string
}{
"host port out of range": {
pod: createPodWithHostPorts(11),
psps: []*policy.PodSecurityPolicy{hostPorts},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"host port in range": {
pod: createPodWithHostPorts(5),
psps: []*policy.PodSecurityPolicy{hostPorts},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: hostPorts.Name,
},
"no host ports with range": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{hostPorts},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: hostPorts.Name,
},
"no host ports without range": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{noHostPorts},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: noHostPorts.Name,
},
"host ports without range": {
pod: createPodWithHostPorts(5),
psps: []*policy.PodSecurityPolicy{noHostPorts},
shouldPassAdmit: false,
shouldPassValidate: false,
},
}
for i := 0; i < 2; i++ {
for k, v := range tests {
v.pod.Spec.Containers, v.pod.Spec.InitContainers = v.pod.Spec.InitContainers, v.pod.Spec.Containers
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
}
}
}
func TestAdmitHostPID(t *testing.T) {
createPodWithHostPID := func(hostPID bool) *kapi.Pod {
pod := goodPod()
pod.Spec.SecurityContext.HostPID = hostPID
return pod
}
noHostPID := restrictivePSP()
noHostPID.Name = "no-hostpid"
noHostPID.Spec.HostPID = false
hostPID := restrictivePSP()
hostPID.Name = "hostpid"
hostPID.Spec.HostPID = true
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedHostPID bool
expectedPSP string
}{
"pod without hostpid request allowed under noHostPID PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{noHostPID},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostPID: false,
expectedPSP: noHostPID.Name,
},
"pod without hostpid request allowed under hostPID PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{hostPID},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostPID: false,
expectedPSP: hostPID.Name,
},
"pod with hostpid request denied by noHostPID PSP": {
pod: createPodWithHostPID(true),
psps: []*policy.PodSecurityPolicy{noHostPID},
shouldPassAdmit: false,
},
"pod with hostpid request allowed by hostPID PSP": {
pod: createPodWithHostPID(true),
psps: []*policy.PodSecurityPolicy{noHostPID, hostPID},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostPID: true,
expectedPSP: hostPID.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.SecurityContext.HostPID != v.expectedHostPID {
t.Errorf("%s expected hostPID to be %t", k, v.expectedHostPID)
}
}
}
}
func TestAdmitHostIPC(t *testing.T) {
createPodWithHostIPC := func(hostIPC bool) *kapi.Pod {
pod := goodPod()
pod.Spec.SecurityContext.HostIPC = hostIPC
return pod
}
noHostIPC := restrictivePSP()
noHostIPC.Name = "no-hostIPC"
noHostIPC.Spec.HostIPC = false
hostIPC := restrictivePSP()
hostIPC.Name = "hostIPC"
hostIPC.Spec.HostIPC = true
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedHostIPC bool
expectedPSP string
}{
"pod without hostIPC request allowed under noHostIPC PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{noHostIPC},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostIPC: false,
expectedPSP: noHostIPC.Name,
},
"pod without hostIPC request allowed under hostIPC PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{hostIPC},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostIPC: false,
expectedPSP: hostIPC.Name,
},
"pod with hostIPC request denied by noHostIPC PSP": {
pod: createPodWithHostIPC(true),
psps: []*policy.PodSecurityPolicy{noHostIPC},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with hostIPC request allowed by hostIPC PSP": {
pod: createPodWithHostIPC(true),
psps: []*policy.PodSecurityPolicy{noHostIPC, hostIPC},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedHostIPC: true,
expectedPSP: hostIPC.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.SecurityContext.HostIPC != v.expectedHostIPC {
t.Errorf("%s expected hostIPC to be %t", k, v.expectedHostIPC)
}
}
}
}
func createPodWithSecurityContexts(podSC *kapi.PodSecurityContext, containerSC *kapi.SecurityContext) *kapi.Pod {
pod := goodPod()
pod.Spec.SecurityContext = podSC
pod.Spec.Containers[0].SecurityContext = containerSC
return pod
}
func TestAdmitSELinux(t *testing.T) {
runAsAny := permissivePSP()
runAsAny.Name = "runAsAny"
runAsAny.Spec.SELinux.Rule = policy.SELinuxStrategyRunAsAny
runAsAny.Spec.SELinux.SELinuxOptions = nil
mustRunAs := permissivePSP()
mustRunAs.Name = "mustRunAs"
mustRunAs.Spec.SELinux.Rule = policy.SELinuxStrategyMustRunAs
mustRunAs.Spec.SELinux.SELinuxOptions = &v1.SELinuxOptions{}
mustRunAs.Spec.SELinux.SELinuxOptions.Level = "level"
mustRunAs.Spec.SELinux.SELinuxOptions.Role = "role"
mustRunAs.Spec.SELinux.SELinuxOptions.Type = "type"
mustRunAs.Spec.SELinux.SELinuxOptions.User = "user"
getInternalSEOptions := func(policy *policy.PodSecurityPolicy) *kapi.SELinuxOptions {
opt := kapi.SELinuxOptions{}
k8s_api_v1.Convert_v1_SELinuxOptions_To_core_SELinuxOptions(policy.Spec.SELinux.SELinuxOptions, &opt, nil)
return &opt
}
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPodSC *kapi.PodSecurityContext
expectedContainerSC *kapi.SecurityContext
expectedPSP string
}{
"runAsAny with no request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny with empty pod request": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{}, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{},
expectedContainerSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny with empty container request": {
pod: createPodWithSecurityContexts(nil, &kapi.SecurityContext{}),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: &kapi.SecurityContext{},
expectedPSP: runAsAny.Name,
},
"runAsAny with pod request": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}}, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}},
expectedContainerSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny with container request": {
pod: createPodWithSecurityContexts(nil, &kapi.SecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}}),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: &kapi.SecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}},
expectedPSP: runAsAny.Name,
},
"runAsAny with pod and container request": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "bar"}}, &kapi.SecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}}),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "bar"}},
expectedContainerSC: &kapi.SecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}},
expectedPSP: runAsAny.Name,
},
"mustRunAs with bad pod request": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}}, nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs with bad container request": {
pod: createPodWithSecurityContexts(nil, &kapi.SecurityContext{SELinuxOptions: &kapi.SELinuxOptions{User: "foo"}}),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs with no request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SELinuxOptions: getInternalSEOptions(mustRunAs)},
expectedContainerSC: nil,
expectedPSP: mustRunAs.Name,
},
"mustRunAs with good pod request": {
pod: createPodWithSecurityContexts(
&kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{Level: "level", Role: "role", Type: "type", User: "user"}},
nil,
),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SELinuxOptions: getInternalSEOptions(mustRunAs)},
expectedContainerSC: nil,
expectedPSP: mustRunAs.Name,
},
"mustRunAs with good container request": {
pod: createPodWithSecurityContexts(
&kapi.PodSecurityContext{SELinuxOptions: &kapi.SELinuxOptions{Level: "level", Role: "role", Type: "type", User: "user"}},
nil,
),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SELinuxOptions: getInternalSEOptions(mustRunAs)},
expectedContainerSC: nil,
expectedPSP: mustRunAs.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if !reflect.DeepEqual(v.expectedPodSC, v.pod.Spec.SecurityContext) {
t.Errorf("%s unexpected diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedPodSC, v.pod.Spec.SecurityContext))
}
if !reflect.DeepEqual(v.expectedContainerSC, v.pod.Spec.Containers[0].SecurityContext) {
t.Errorf("%s unexpected diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedContainerSC, v.pod.Spec.Containers[0].SecurityContext))
}
}
}
}
func TestAdmitAppArmor(t *testing.T) {
createPodWithAppArmor := func(profile string) *kapi.Pod {
pod := goodPod()
apparmor.SetProfileNameFromPodAnnotations(pod.Annotations, defaultContainerName, profile)
return pod
}
unconstrainedPSP := restrictivePSP()
defaultedPSP := restrictivePSP()
defaultedPSP.Annotations = map[string]string{
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
}
appArmorPSP := restrictivePSP()
appArmorPSP.Annotations = map[string]string{
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault,
}
appArmorDefaultPSP := restrictivePSP()
appArmorDefaultPSP.Annotations = map[string]string{
apparmor.DefaultProfileAnnotationKey: apparmor.ProfileRuntimeDefault,
apparmor.AllowedProfilesAnnotationKey: apparmor.ProfileRuntimeDefault + "," + apparmor.ProfileNamePrefix + "foo",
}
tests := map[string]struct {
pod *kapi.Pod
psp *policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedProfile string
}{
"unconstrained with no profile": {
pod: goodPod(),
psp: unconstrainedPSP,
shouldPassAdmit: true,
shouldPassValidate: true,
expectedProfile: "",
},
"unconstrained with profile": {
pod: createPodWithAppArmor(apparmor.ProfileRuntimeDefault),
psp: unconstrainedPSP,
shouldPassAdmit: true,
shouldPassValidate: true,
expectedProfile: apparmor.ProfileRuntimeDefault,
},
"unconstrained with default profile": {
pod: goodPod(),
psp: defaultedPSP,
shouldPassAdmit: true,
shouldPassValidate: true,
expectedProfile: apparmor.ProfileRuntimeDefault,
},
"AppArmor enforced with no profile": {
pod: goodPod(),
psp: appArmorPSP,
shouldPassAdmit: false,
shouldPassValidate: false,
},
"AppArmor enforced with default profile": {
pod: goodPod(),
psp: appArmorDefaultPSP,
shouldPassAdmit: true,
shouldPassValidate: true,
expectedProfile: apparmor.ProfileRuntimeDefault,
},
"AppArmor enforced with good profile": {
pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "foo"),
psp: appArmorDefaultPSP,
shouldPassAdmit: true,
shouldPassValidate: true,
expectedProfile: apparmor.ProfileNamePrefix + "foo",
},
"AppArmor enforced with local profile": {
pod: createPodWithAppArmor(apparmor.ProfileNamePrefix + "bar"),
psp: appArmorPSP,
shouldPassAdmit: false,
shouldPassValidate: false,
},
}
for k, v := range tests {
testPSPAdmit(k, []*policy.PodSecurityPolicy{v.psp}, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.psp.Name, t)
if v.shouldPassAdmit {
assert.Equal(t, v.expectedProfile, apparmor.GetProfileNameFromPodAnnotations(v.pod.Annotations, defaultContainerName), k)
}
}
}
func TestAdmitRunAsUser(t *testing.T) {
podSC := func(user *int64) *kapi.PodSecurityContext {
return &kapi.PodSecurityContext{RunAsUser: user}
}
containerSC := func(user *int64) *kapi.SecurityContext {
return &kapi.SecurityContext{RunAsUser: user}
}
runAsAny := permissivePSP()
runAsAny.Name = "runAsAny"
runAsAny.Spec.RunAsUser.Rule = policy.RunAsUserStrategyRunAsAny
mustRunAs := permissivePSP()
mustRunAs.Name = "mustRunAs"
mustRunAs.Spec.RunAsUser.Rule = policy.RunAsUserStrategyMustRunAs
mustRunAs.Spec.RunAsUser.Ranges = []policy.IDRange{
{Min: int64(999), Max: int64(1000)},
}
runAsNonRoot := permissivePSP()
runAsNonRoot.Name = "runAsNonRoot"
runAsNonRoot.Spec.RunAsUser.Rule = policy.RunAsUserStrategyMustRunAsNonRoot
trueValue := true
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPodSC *kapi.PodSecurityContext
expectedContainerSC *kapi.SecurityContext
expectedPSP string
}{
"runAsAny no pod request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny pod request": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(1)), nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(utilpointer.Int64Ptr(1)),
expectedContainerSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny container request": {
pod: createPodWithSecurityContexts(nil, containerSC(utilpointer.Int64Ptr(1))),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: containerSC(utilpointer.Int64Ptr(1)),
expectedPSP: runAsAny.Name,
},
"mustRunAs pod request out of range": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(1)), nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs container request out of range": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(999)), containerSC(utilpointer.Int64Ptr(1))),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs pod request in range": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(999)), nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(&mustRunAs.Spec.RunAsUser.Ranges[0].Min),
expectedContainerSC: nil,
expectedPSP: mustRunAs.Name,
},
"mustRunAs container request in range": {
pod: createPodWithSecurityContexts(nil, containerSC(utilpointer.Int64Ptr(999))),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: containerSC(&mustRunAs.Spec.RunAsUser.Ranges[0].Min),
expectedPSP: mustRunAs.Name,
},
"mustRunAs pod and container request in range": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(999)), containerSC(utilpointer.Int64Ptr(1000))),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(utilpointer.Int64Ptr(999)),
expectedContainerSC: containerSC(utilpointer.Int64Ptr(1000)),
expectedPSP: mustRunAs.Name,
},
"mustRunAs no request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: containerSC(&mustRunAs.Spec.RunAsUser.Ranges[0].Min),
expectedPSP: mustRunAs.Name,
},
"runAsNonRoot no request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{runAsNonRoot},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedContainerSC: &kapi.SecurityContext{RunAsNonRoot: &trueValue},
expectedPSP: runAsNonRoot.Name,
},
"runAsNonRoot pod request root": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(0)), nil),
psps: []*policy.PodSecurityPolicy{runAsNonRoot},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"runAsNonRoot pod request non-root": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(1)), nil),
psps: []*policy.PodSecurityPolicy{runAsNonRoot},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(utilpointer.Int64Ptr(1)),
expectedPSP: runAsNonRoot.Name,
},
"runAsNonRoot container request root": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(1)), containerSC(utilpointer.Int64Ptr(0))),
psps: []*policy.PodSecurityPolicy{runAsNonRoot},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"runAsNonRoot container request non-root": {
pod: createPodWithSecurityContexts(podSC(utilpointer.Int64Ptr(1)), containerSC(utilpointer.Int64Ptr(2))),
psps: []*policy.PodSecurityPolicy{runAsNonRoot},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(utilpointer.Int64Ptr(1)),
expectedContainerSC: containerSC(utilpointer.Int64Ptr(2)),
expectedPSP: runAsNonRoot.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if !reflect.DeepEqual(v.expectedPodSC, v.pod.Spec.SecurityContext) {
t.Errorf("%s unexpected pod sc diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedPodSC, v.pod.Spec.SecurityContext))
}
if !reflect.DeepEqual(v.expectedContainerSC, v.pod.Spec.Containers[0].SecurityContext) {
t.Errorf("%s unexpected container sc diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedContainerSC, v.pod.Spec.Containers[0].SecurityContext))
}
}
}
}
func TestAdmitSupplementalGroups(t *testing.T) {
podSC := func(group int64) *kapi.PodSecurityContext {
return &kapi.PodSecurityContext{SupplementalGroups: []int64{group}}
}
runAsAny := permissivePSP()
runAsAny.Name = "runAsAny"
runAsAny.Spec.SupplementalGroups.Rule = policy.SupplementalGroupsStrategyRunAsAny
mustRunAs := permissivePSP()
mustRunAs.Name = "mustRunAs"
mustRunAs.Spec.SupplementalGroups.Rule = policy.SupplementalGroupsStrategyMustRunAs
mustRunAs.Spec.SupplementalGroups.Ranges = []policy.IDRange{{Min: int64(999), Max: int64(1000)}}
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPodSC *kapi.PodSecurityContext
expectedPSP string
}{
"runAsAny no pod request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny empty pod request": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{}, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{},
expectedPSP: runAsAny.Name,
},
"runAsAny empty pod request empty supplemental groups": {
pod: createPodWithSecurityContexts(&kapi.PodSecurityContext{SupplementalGroups: []int64{}}, nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SupplementalGroups: []int64{}},
expectedPSP: runAsAny.Name,
},
"runAsAny pod request": {
pod: createPodWithSecurityContexts(podSC(1), nil),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: &kapi.PodSecurityContext{SupplementalGroups: []int64{1}},
expectedPSP: runAsAny.Name,
},
"mustRunAs no pod request": {
pod: createPodWithSecurityContexts(nil, nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(mustRunAs.Spec.SupplementalGroups.Ranges[0].Min),
expectedPSP: mustRunAs.Name,
},
"mustRunAs bad pod request": {
pod: createPodWithSecurityContexts(podSC(1), nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs good pod request": {
pod: createPodWithSecurityContexts(podSC(999), nil),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPodSC: podSC(999),
expectedPSP: mustRunAs.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if !reflect.DeepEqual(v.expectedPodSC, v.pod.Spec.SecurityContext) {
t.Errorf("%s unexpected pod sc diff:\n%s", k, diff.ObjectGoPrintSideBySide(v.expectedPodSC, v.pod.Spec.SecurityContext))
}
}
}
}
func TestAdmitFSGroup(t *testing.T) {
createPodWithFSGroup := func(group int64) *kapi.Pod {
pod := goodPod()
// doesn't matter if we set it here or on the container, the
// admission controller uses DetermineEffectiveSC to get the defaulting
// behavior so it can validate what will be applied at runtime
pod.Spec.SecurityContext.FSGroup = utilpointer.Int64Ptr(group)
return pod
}
runAsAny := restrictivePSP()
runAsAny.Name = "runAsAny"
runAsAny.Spec.FSGroup.Rule = policy.FSGroupStrategyRunAsAny
mustRunAs := restrictivePSP()
mustRunAs.Name = "mustRunAs"
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedFSGroup *int64
expectedPSP string
}{
"runAsAny no pod request": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedFSGroup: nil,
expectedPSP: runAsAny.Name,
},
"runAsAny pod request": {
pod: createPodWithFSGroup(1),
psps: []*policy.PodSecurityPolicy{runAsAny},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedFSGroup: utilpointer.Int64Ptr(1),
expectedPSP: runAsAny.Name,
},
"mustRunAs no pod request": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedFSGroup: &mustRunAs.Spec.SupplementalGroups.Ranges[0].Min,
expectedPSP: mustRunAs.Name,
},
"mustRunAs bad pod request": {
pod: createPodWithFSGroup(1),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"mustRunAs good pod request": {
pod: createPodWithFSGroup(999),
psps: []*policy.PodSecurityPolicy{mustRunAs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedFSGroup: utilpointer.Int64Ptr(999),
expectedPSP: mustRunAs.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.SecurityContext.FSGroup == nil && v.expectedFSGroup == nil {
// ok, don't need to worry about identifying specific diffs
continue
}
if v.pod.Spec.SecurityContext.FSGroup == nil && v.expectedFSGroup != nil {
t.Errorf("%s expected FSGroup to be: %v but found nil", k, *v.expectedFSGroup)
continue
}
if v.pod.Spec.SecurityContext.FSGroup != nil && v.expectedFSGroup == nil {
t.Errorf("%s expected FSGroup to be nil but found: %v", k, *v.pod.Spec.SecurityContext.FSGroup)
continue
}
if *v.expectedFSGroup != *v.pod.Spec.SecurityContext.FSGroup {
t.Errorf("%s expected FSGroup to be: %v but found %v", k, *v.expectedFSGroup, *v.pod.Spec.SecurityContext.FSGroup)
}
}
}
}
func TestAdmitReadOnlyRootFilesystem(t *testing.T) {
createPodWithRORFS := func(rorfs bool) *kapi.Pod {
pod := goodPod()
pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &rorfs
return pod
}
noRORFS := restrictivePSP()
noRORFS.Name = "no-rorfs"
noRORFS.Spec.ReadOnlyRootFilesystem = false
rorfs := restrictivePSP()
rorfs.Name = "rorfs"
rorfs.Spec.ReadOnlyRootFilesystem = true
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedRORFS bool
expectedPSP string
}{
"no-rorfs allows pod request with rorfs": {
pod: createPodWithRORFS(true),
psps: []*policy.PodSecurityPolicy{noRORFS},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedRORFS: true,
expectedPSP: noRORFS.Name,
},
"no-rorfs allows pod request without rorfs": {
pod: createPodWithRORFS(false),
psps: []*policy.PodSecurityPolicy{noRORFS},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedRORFS: false,
expectedPSP: noRORFS.Name,
},
"rorfs rejects pod request without rorfs": {
pod: createPodWithRORFS(false),
psps: []*policy.PodSecurityPolicy{rorfs},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"rorfs defaults nil pod request": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{rorfs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedRORFS: true,
expectedPSP: rorfs.Name,
},
"rorfs accepts pod request with rorfs": {
pod: createPodWithRORFS(true),
psps: []*policy.PodSecurityPolicy{rorfs},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedRORFS: true,
expectedPSP: rorfs.Name,
},
}
for k, v := range tests {
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem == nil ||
*v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem != v.expectedRORFS {
t.Errorf("%s expected ReadOnlyRootFilesystem to be %t but found %#v", k, v.expectedRORFS, v.pod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem)
}
}
}
}
func TestAdmitSysctls(t *testing.T) {
podWithSysctls := func(safeSysctls []string, unsafeSysctls []string) *kapi.Pod {
pod := goodPod()
dummySysctls := func(names []string) []kapi.Sysctl {
sysctls := make([]kapi.Sysctl, len(names))
for i, n := range names {
sysctls[i].Name = n
sysctls[i].Value = "dummy"
}
return sysctls
}
pod.Spec.SecurityContext = &kapi.PodSecurityContext{
Sysctls: dummySysctls(append(safeSysctls, unsafeSysctls...)),
}
return pod
}
safeSysctls := restrictivePSP()
safeSysctls.Name = "no sysctls"
noSysctls := restrictivePSP()
noSysctls.Name = "empty sysctls"
noSysctls.Spec.ForbiddenSysctls = []string{"*"}
mixedSysctls := restrictivePSP()
mixedSysctls.Name = "wildcard sysctls"
mixedSysctls.Spec.ForbiddenSysctls = []string{"net.*"}
mixedSysctls.Spec.AllowedUnsafeSysctls = []string{"a.*", "b.*"}
aUnsafeSysctl := restrictivePSP()
aUnsafeSysctl.Name = "a sysctl"
aUnsafeSysctl.Spec.AllowedUnsafeSysctls = []string{"a"}
bUnsafeSysctl := restrictivePSP()
bUnsafeSysctl.Name = "b sysctl"
bUnsafeSysctl.Spec.AllowedUnsafeSysctls = []string{"b"}
cUnsafeSysctl := restrictivePSP()
cUnsafeSysctl.Name = "c sysctl"
cUnsafeSysctl.Spec.AllowedUnsafeSysctls = []string{"c"}
catchallSysctls := restrictivePSP()
catchallSysctls.Name = "catchall sysctl"
catchallSysctls.Spec.AllowedUnsafeSysctls = []string{"*"}
tests := map[string]struct {
pod *kapi.Pod
psps []*policy.PodSecurityPolicy
shouldPassAdmit bool
shouldPassValidate bool
expectedPSP string
}{
"pod without any sysctls request allowed under safeSysctls PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{safeSysctls},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: safeSysctls.Name,
},
"pod without any sysctls request allowed under noSysctls PSP": {
pod: goodPod(),
psps: []*policy.PodSecurityPolicy{noSysctls},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: noSysctls.Name,
},
"pod with safe sysctls request allowed under safeSysctls PSP": {
pod: podWithSysctls([]string{"kernel.shm_rmid_forced", "net.ipv4.tcp_syncookies"}, []string{}),
psps: []*policy.PodSecurityPolicy{safeSysctls},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: safeSysctls.Name,
},
"pod with unsafe sysctls request disallowed under noSysctls PSP": {
pod: podWithSysctls([]string{}, []string{"a", "b"}),
psps: []*policy.PodSecurityPolicy{noSysctls},
shouldPassAdmit: false,
shouldPassValidate: false,
expectedPSP: noSysctls.Name,
},
"pod with unsafe sysctls a, b request disallowed under aUnsafeSysctl SCC": {
pod: podWithSysctls([]string{}, []string{"a", "b"}),
psps: []*policy.PodSecurityPolicy{aUnsafeSysctl},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with unsafe sysctls b request disallowed under aUnsafeSysctl SCC": {
pod: podWithSysctls([]string{}, []string{"b"}),
psps: []*policy.PodSecurityPolicy{aUnsafeSysctl},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with unsafe sysctls a request allowed under aUnsafeSysctl SCC": {
pod: podWithSysctls([]string{}, []string{"a"}),
psps: []*policy.PodSecurityPolicy{aUnsafeSysctl},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: aUnsafeSysctl.Name,
},
"pod with safe net sysctl request allowed under aUnsafeSysctl SCC": {
pod: podWithSysctls([]string{"net.ipv4.ip_local_port_range"}, []string{}),
psps: []*policy.PodSecurityPolicy{aUnsafeSysctl},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: aUnsafeSysctl.Name,
},
"pod with safe sysctls request disallowed under noSysctls PSP": {
pod: podWithSysctls([]string{"net.ipv4.ip_local_port_range"}, []string{}),
psps: []*policy.PodSecurityPolicy{noSysctls},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with matching sysctls request allowed under mixedSysctls PSP": {
pod: podWithSysctls([]string{"kernel.shm_rmid_forced"}, []string{"a.b", "b.a"}),
psps: []*policy.PodSecurityPolicy{mixedSysctls},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: mixedSysctls.Name,
},
"pod with not-matching unsafe sysctls request disallowed under mixedSysctls PSP": {
pod: podWithSysctls([]string{}, []string{"e"}),
psps: []*policy.PodSecurityPolicy{mixedSysctls},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with not-matching safe sysctls request disallowed under mixedSysctls PSP": {
pod: podWithSysctls([]string{"net.ipv4.ip_local_port_range"}, []string{}),
psps: []*policy.PodSecurityPolicy{mixedSysctls},
shouldPassAdmit: false,
shouldPassValidate: false,
},
"pod with sysctls request allowed under catchallSysctls PSP": {
pod: podWithSysctls([]string{"net.ipv4.ip_local_port_range"}, []string{"f"}),
psps: []*policy.PodSecurityPolicy{catchallSysctls},
shouldPassAdmit: true,
shouldPassValidate: true,
expectedPSP: catchallSysctls.Name,
},
}
for k, v := range tests {
origSysctl := v.pod.Spec.SecurityContext.Sysctls
testPSPAdmit(k, v.psps, v.pod, v.shouldPassAdmit, v.shouldPassValidate, v.expectedPSP, t)
if v.shouldPassAdmit {
if !reflect.DeepEqual(v.pod.Spec.SecurityContext.Sysctls, origSysctl) {
t.Errorf("%s: wrong sysctls: expected=%v, got=%v", k, origSysctl, v.pod.Spec.SecurityContext.Sysctls)
}
}
}
}
func testPSPAdmit(testCaseName string, psps []*policy.PodSecurityPolicy, pod *kapi.Pod, shouldPassAdmit, shouldPassValidate bool, expectedPSP string, t *testing.T) {
testPSPAdmitAdvanced(testCaseName, kadmission.Create, psps, nil, &user.DefaultInfo{}, pod, nil, shouldPassAdmit, shouldPassValidate, true, expectedPSP, t)
}
// fakeAttributes decorate kadmission.Attributes. It's used to trace the added annotations.
type fakeAttributes struct {
kadmission.Attributes
annotations map[string]string
}
func (f fakeAttributes) AddAnnotation(k, v string) error {
f.annotations[k] = v
return f.Attributes.AddAnnotation(k, v)
}
func testPSPAdmitAdvanced(testCaseName string, op kadmission.Operation, psps []*policy.PodSecurityPolicy, authz authorizer.Authorizer, userInfo user.Info, pod, oldPod *kapi.Pod, shouldPassAdmit, shouldPassValidate bool, canMutate bool, expectedPSP string, t *testing.T) {
originalPod := pod.DeepCopy()
plugin := NewTestAdmission(psps, authz)
attrs := kadmission.NewAttributesRecord(pod, oldPod, kapi.Kind("Pod").WithVersion("version"), pod.Namespace, "", kapi.Resource("pods").WithVersion("version"), "", op, false, userInfo)
annotations := make(map[string]string)
attrs = &fakeAttributes{attrs, annotations}
err := plugin.Admit(attrs, nil)
if shouldPassAdmit && err != nil {
t.Errorf("%s: expected no errors on Admit but received %v", testCaseName, err)
}
if shouldPassAdmit && err == nil {
if pod.Annotations[psputil.ValidatedPSPAnnotation] != expectedPSP {
t.Errorf("%s: expected to be admitted under %q PSP but found %q", testCaseName, expectedPSP, pod.Annotations[psputil.ValidatedPSPAnnotation])
}
if !canMutate {
podWithoutPSPAnnotation := pod.DeepCopy()
delete(podWithoutPSPAnnotation.Annotations, psputil.ValidatedPSPAnnotation)
originalPodWithoutPSPAnnotation := originalPod.DeepCopy()
delete(originalPodWithoutPSPAnnotation.Annotations, psputil.ValidatedPSPAnnotation)
if !apiequality.Semantic.DeepEqual(originalPodWithoutPSPAnnotation.Spec, podWithoutPSPAnnotation.Spec) {
t.Errorf("%s: expected no mutation on Admit, got %s", testCaseName, diff.ObjectGoPrintSideBySide(originalPodWithoutPSPAnnotation.Spec, podWithoutPSPAnnotation.Spec))
}
}
}
if !shouldPassAdmit && err == nil {
t.Errorf("%s: expected errors on Admit but received none", testCaseName)
}
err = plugin.Validate(attrs, nil)
psp := ""
if shouldPassAdmit && op == kadmission.Create {
psp = expectedPSP
}
validateAuditAnnotation(t, testCaseName, annotations, "podsecuritypolicy.policy.k8s.io/admit-policy", psp)
if shouldPassValidate && err != nil {
t.Errorf("%s: expected no errors on Validate but received %v", testCaseName, err)
} else if !shouldPassValidate && err == nil {
t.Errorf("%s: expected errors on Validate but received none", testCaseName)
}
if shouldPassValidate {
validateAuditAnnotation(t, testCaseName, annotations, "podsecuritypolicy.policy.k8s.io/validate-policy", expectedPSP)
} else {
validateAuditAnnotation(t, testCaseName, annotations, "podsecuritypolicy.policy.k8s.io/validate-policy", "")
}
}
func validateAuditAnnotation(t *testing.T, testCaseName string, annotations map[string]string, key, value string) {
if annotations[key] != value {
t.Errorf("%s: expected to have annotations[%s] set to %q, got %q", testCaseName, key, value, annotations[key])
}
}
func TestAssignSecurityContext(t *testing.T) {
// psp that will deny privileged container requests and has a default value for a field (uid)
psp := restrictivePSP()
provider, err := kpsp.NewSimpleProvider(psp, "namespace", kpsp.NewSimpleStrategyFactory())
if err != nil {
t.Fatalf("failed to create provider: %v", err)
}
createContainer := func(priv bool) kapi.Container {
return kapi.Container{
SecurityContext: &kapi.SecurityContext{
Privileged: &priv,
},
}
}
testCases := map[string]struct {
pod *kapi.Pod
shouldValidate bool
expectedUID *int64
}{
"pod and container SC is not changed when invalid": {
pod: &kapi.Pod{
Spec: kapi.PodSpec{
SecurityContext: &kapi.PodSecurityContext{},
Containers: []kapi.Container{createContainer(true)},
},
},
shouldValidate: false,
},
"must validate all containers": {
pod: &kapi.Pod{
Spec: kapi.PodSpec{
// good container and bad container
SecurityContext: &kapi.PodSecurityContext{},
Containers: []kapi.Container{createContainer(false), createContainer(true)},
},
},
shouldValidate: false,
},
"pod validates": {
pod: &kapi.Pod{
Spec: kapi.PodSpec{
SecurityContext: &kapi.PodSecurityContext{},
Containers: []kapi.Container{createContainer(false)},
},
},
shouldValidate: true,
},
}
for k, v := range testCases {
errs := assignSecurityContext(provider, v.pod)
if v.shouldValidate && len(errs) > 0 {
t.Errorf("%s expected to validate but received errors %v", k, errs)
continue
}
if !v.shouldValidate && len(errs) == 0 {
t.Errorf("%s expected validation errors but received none", k)
continue
}
}
}
func TestCreateProvidersFromConstraints(t *testing.T) {
testCases := map[string]struct {
// use a generating function so we can test for non-mutation
psp func() *policy.PodSecurityPolicy
expectedErr string
}{
"valid psp": {
psp: func() *policy.PodSecurityPolicy {
return &policy.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "valid psp",
},
Spec: policy.PodSecurityPolicySpec{
SELinux: policy.SELinuxStrategyOptions{
Rule: policy.SELinuxStrategyRunAsAny,
},
RunAsUser: policy.RunAsUserStrategyOptions{
Rule: policy.RunAsUserStrategyRunAsAny,
},
RunAsGroup: &policy.RunAsGroupStrategyOptions{
Rule: policy.RunAsGroupStrategyRunAsAny,
},
FSGroup: policy.FSGroupStrategyOptions{
Rule: policy.FSGroupStrategyRunAsAny,
},
SupplementalGroups: policy.SupplementalGroupsStrategyOptions{
Rule: policy.SupplementalGroupsStrategyRunAsAny,
},
},
}
},
},
"bad psp strategy options": {
psp: func() *policy.PodSecurityPolicy {
return &policy.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "bad psp user options",
},
Spec: policy.PodSecurityPolicySpec{
SELinux: policy.SELinuxStrategyOptions{
Rule: policy.SELinuxStrategyRunAsAny,
},
RunAsUser: policy.RunAsUserStrategyOptions{
Rule: policy.RunAsUserStrategyMustRunAs,
},
RunAsGroup: &policy.RunAsGroupStrategyOptions{
Rule: policy.RunAsGroupStrategyRunAsAny,
},
FSGroup: policy.FSGroupStrategyOptions{
Rule: policy.FSGroupStrategyRunAsAny,
},
SupplementalGroups: policy.SupplementalGroupsStrategyOptions{
Rule: policy.SupplementalGroupsStrategyRunAsAny,
},
},
}
},
expectedErr: "MustRunAs requires at least one range",
},
}
for k, v := range testCases {
admit := &PodSecurityPolicyPlugin{
Handler: kadmission.NewHandler(kadmission.Create, kadmission.Update),
strategyFactory: kpsp.NewSimpleStrategyFactory(),
}
psp := v.psp()
_, errs := admit.createProvidersFromPolicies([]*policy.PodSecurityPolicy{psp}, "namespace")
if !reflect.DeepEqual(psp, v.psp()) {
diff := diff.ObjectDiff(psp, v.psp())
t.Errorf("%s createProvidersFromPolicies mutated policy. diff:\n%s", k, diff)
}
if len(v.expectedErr) > 0 && len(errs) != 1 {
t.Errorf("%s expected a single error '%s' but received %v", k, v.expectedErr, errs)
continue
}
if len(v.expectedErr) == 0 && len(errs) != 0 {
t.Errorf("%s did not expect an error but received %v", k, errs)
continue
}
// check that we got the error we expected
if len(v.expectedErr) > 0 {
if !strings.Contains(errs[0].Error(), v.expectedErr) {
t.Errorf("%s expected error '%s' but received %v", k, v.expectedErr, errs[0])
}
}
}
}
func TestPolicyAuthorization(t *testing.T) {
policyWithName := func(name string) *policy.PodSecurityPolicy {
p := permissivePSP()
p.Name = name
return p
}
tests := map[string]struct {
user user.Info
sa string
ns string
expectedPolicy string
inPolicies []*policy.PodSecurityPolicy
allowed map[string]map[string]map[string]bool
allowedGroup string
}{
"policy allowed by user (extensions API Group)": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
"user": {
"test": {"policy": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{policyWithName("policy")},
expectedPolicy: "policy",
},
"policy allowed by sa (extensions API Group)": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{policyWithName("policy")},
expectedPolicy: "policy",
},
"policy allowed by user (policy API Group)": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
"user": {
"test": {"policy": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{policyWithName("policy")},
expectedPolicy: "policy",
allowedGroup: policy.GroupName,
},
"policy allowed by sa (policy API Group)": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{policyWithName("policy")},
expectedPolicy: "policy",
allowedGroup: policy.GroupName,
},
"no policies allowed": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{},
inPolicies: []*policy.PodSecurityPolicy{policyWithName("policy")},
expectedPolicy: "",
},
"multiple policies allowed": {
user: &user.DefaultInfo{Name: "user"},
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy1": true},
"": {"policy4": true},
"other": {"policy6": true},
},
"user": {
"test": {"policy2": true},
"": {"policy5": true},
"other": {"policy7": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
// Prefix to force checking these policies first.
policyWithName("a_policy1"), // not allowed in this namespace
policyWithName("a_policy2"), // not allowed in this namespace
policyWithName("policy2"), // allowed by sa
policyWithName("policy3"), // allowed by user
policyWithName("policy4"), // not allowed
policyWithName("policy5"), // allowed by sa at cluster level
policyWithName("policy6"), // allowed by user at cluster level
},
expectedPolicy: "policy2",
},
"policies are not allowed for nil user info": {
user: nil,
sa: "sa",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy1": true},
},
"user": {
"test": {"policy2": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
policyWithName("policy3"),
},
// only the policies for the sa are allowed when user info is nil
expectedPolicy: "policy1",
},
"policies are not allowed for nil sa info": {
user: &user.DefaultInfo{Name: "user"},
sa: "",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy1": true},
},
"user": {
"test": {"policy2": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
policyWithName("policy3"),
},
// only the policies for the user are allowed when sa info is nil
expectedPolicy: "policy2",
},
"policies are not allowed for nil sa and user info": {
user: nil,
sa: "",
ns: "test",
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy1": true},
},
"user": {
"test": {"policy2": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
policyWithName("policy3"),
},
// no policies are allowed if sa and user are both nil
expectedPolicy: "",
},
}
for k, v := range tests {
var (
oldPod *kapi.Pod
shouldPass = v.expectedPolicy != ""
authz = &TestAuthorizer{usernameToNamespaceToAllowedPSPs: v.allowed, allowedAPIGroupName: v.allowedGroup}
canMutate = true
)
pod := goodPod()
pod.Namespace = v.ns
pod.Spec.ServiceAccountName = v.sa
testPSPAdmitAdvanced(k, kadmission.Create, v.inPolicies, authz, v.user,
pod, oldPod, shouldPass, shouldPass, canMutate, v.expectedPolicy, t)
}
}
func TestPolicyAuthorizationErrors(t *testing.T) {
policyWithName := func(name string) *policy.PodSecurityPolicy {
p := restrictivePSP()
p.Name = name
return p
}
const (
sa = "sa"
ns = "test"
userName = "user"
)
tests := map[string]struct {
inPolicies []*policy.PodSecurityPolicy
allowed map[string]map[string]map[string]bool
expectValidationErrs int
}{
"policies not allowed": {
allowed: map[string]map[string]map[string]bool{},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
},
expectValidationErrs: 0,
},
"policy allowed by user": {
allowed: map[string]map[string]map[string]bool{
"user": {
"test": {"policy1": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
},
expectValidationErrs: 1,
},
"policy allowed by service account": {
allowed: map[string]map[string]map[string]bool{
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy2": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
},
expectValidationErrs: 1,
},
"multiple policies allowed": {
allowed: map[string]map[string]map[string]bool{
"user": {
"test": {"policy1": true},
},
serviceaccount.MakeUsername("test", "sa"): {
"test": {"policy2": true},
},
},
inPolicies: []*policy.PodSecurityPolicy{
policyWithName("policy1"),
policyWithName("policy2"),
},
expectValidationErrs: 2,
},
}
for desc, tc := range tests {
t.Run(desc, func(t *testing.T) {
authz := &TestAuthorizer{usernameToNamespaceToAllowedPSPs: tc.allowed}
pod := goodPod()
pod.Namespace = ns
pod.Spec.ServiceAccountName = sa
pod.Spec.SecurityContext.HostPID = true
plugin := NewTestAdmission(tc.inPolicies, authz)
attrs := kadmission.NewAttributesRecord(pod, nil, kapi.Kind("Pod").WithVersion("version"), ns, "", kapi.Resource("pods").WithVersion("version"), "", kadmission.Create, false, &user.DefaultInfo{Name: userName})
allowedPod, _, validationErrs, err := plugin.computeSecurityContext(attrs, pod, true, "")
assert.Nil(t, allowedPod)
assert.NoError(t, err)
assert.Len(t, validationErrs, tc.expectValidationErrs)
})
}
}
func TestPreferValidatedPSP(t *testing.T) {
restrictivePSPWithName := func(name string) *policy.PodSecurityPolicy {
p := restrictivePSP()
p.Name = name
return p
}
permissivePSPWithName := func(name string) *policy.PodSecurityPolicy {
p := permissivePSP()
p.Name = name
return p
}
tests := map[string]struct {
inPolicies []*policy.PodSecurityPolicy
expectValidationErrs int
validatedPSPHint string
expectedPSP string
}{
"no policy saved in annotations, PSPs are ordered lexicographically": {
inPolicies: []*policy.PodSecurityPolicy{
restrictivePSPWithName("001restrictive"),
restrictivePSPWithName("002restrictive"),
permissivePSPWithName("002permissive"),
permissivePSPWithName("001permissive"),
permissivePSPWithName("003permissive"),
},
expectValidationErrs: 0,
validatedPSPHint: "",
expectedPSP: "001permissive",
},
"policy saved in annotations is preferred": {
inPolicies: []*policy.PodSecurityPolicy{
restrictivePSPWithName("001restrictive"),
restrictivePSPWithName("002restrictive"),
permissivePSPWithName("001permissive"),
permissivePSPWithName("002permissive"),
permissivePSPWithName("003permissive"),
},
expectValidationErrs: 0,
validatedPSPHint: "002permissive",
expectedPSP: "002permissive",
},
"policy saved in annotations is invalid": {
inPolicies: []*policy.PodSecurityPolicy{
restrictivePSPWithName("001restrictive"),
restrictivePSPWithName("002restrictive"),
},
expectValidationErrs: 2,
validatedPSPHint: "foo",
expectedPSP: "",
},
"policy saved in annotations is disallowed anymore": {
inPolicies: []*policy.PodSecurityPolicy{
restrictivePSPWithName("001restrictive"),
restrictivePSPWithName("002restrictive"),
},
expectValidationErrs: 2,
validatedPSPHint: "001restrictive",
expectedPSP: "",
},
"policy saved in annotations is disallowed anymore, but find another one": {
inPolicies: []*policy.PodSecurityPolicy{
restrictivePSPWithName("001restrictive"),
restrictivePSPWithName("002restrictive"),
permissivePSPWithName("002permissive"),
permissivePSPWithName("001permissive"),
},
expectValidationErrs: 0,
validatedPSPHint: "001restrictive",
expectedPSP: "001permissive",
},
}
for desc, tc := range tests {
t.Run(desc, func(t *testing.T) {
authz := authorizerfactory.NewAlwaysAllowAuthorizer()
allowPrivilegeEscalation := true
pod := goodPod()
pod.Namespace = "ns"
pod.Spec.ServiceAccountName = "sa"
pod.Spec.Containers[0].SecurityContext.AllowPrivilegeEscalation = &allowPrivilegeEscalation
plugin := NewTestAdmission(tc.inPolicies, authz)
attrs := kadmission.NewAttributesRecord(pod, nil, kapi.Kind("Pod").WithVersion("version"), "ns", "", kapi.Resource("pods").WithVersion("version"), "", kadmission.Update, false, &user.DefaultInfo{Name: "test"})
_, pspName, validationErrs, err := plugin.computeSecurityContext(attrs, pod, false, tc.validatedPSPHint)
assert.NoError(t, err)
assert.Len(t, validationErrs, tc.expectValidationErrs)
assert.Equal(t, tc.expectedPSP, pspName)
})
}
}
func restrictivePSP() *policy.PodSecurityPolicy {
allowPrivilegeEscalation := false
return &policy.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "restrictive",
Annotations: map[string]string{},
},
Spec: policy.PodSecurityPolicySpec{
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
RunAsUser: policy.RunAsUserStrategyOptions{
Rule: policy.RunAsUserStrategyMustRunAs,
Ranges: []policy.IDRange{
{Min: int64(999), Max: int64(999)},
},
},
RunAsGroup: &policy.RunAsGroupStrategyOptions{
Rule: policy.RunAsGroupStrategyMustRunAs,
Ranges: []policy.IDRange{
{Min: int64(999), Max: int64(999)},
},
},
SELinux: policy.SELinuxStrategyOptions{
Rule: policy.SELinuxStrategyMustRunAs,
SELinuxOptions: &v1.SELinuxOptions{
Level: "s9:z0,z1",
},
},
FSGroup: policy.FSGroupStrategyOptions{
Rule: policy.FSGroupStrategyMustRunAs,
Ranges: []policy.IDRange{
{Min: int64(999), Max: int64(999)},
},
},
SupplementalGroups: policy.SupplementalGroupsStrategyOptions{
Rule: policy.SupplementalGroupsStrategyMustRunAs,
Ranges: []policy.IDRange{
{Min: int64(999), Max: int64(999)},
},
},
},
}
}
func permissivePSP() *policy.PodSecurityPolicy {
allowPrivilegeEscalation := true
return &policy.PodSecurityPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "privileged",
Annotations: map[string]string{},
},
Spec: policy.PodSecurityPolicySpec{
AllowPrivilegeEscalation: &allowPrivilegeEscalation,
HostIPC: true,
HostNetwork: true,
HostPID: true,
HostPorts: []policy.HostPortRange{{Min: 0, Max: 65536}},
Volumes: []policy.FSType{policy.All},
AllowedCapabilities: []v1.Capability{policy.AllowAllCapabilities},
RunAsUser: policy.RunAsUserStrategyOptions{
Rule: policy.RunAsUserStrategyRunAsAny,
},
RunAsGroup: &policy.RunAsGroupStrategyOptions{
Rule: policy.RunAsGroupStrategyRunAsAny,
},
SELinux: policy.SELinuxStrategyOptions{
Rule: policy.SELinuxStrategyRunAsAny,
},
FSGroup: policy.FSGroupStrategyOptions{
Rule: policy.FSGroupStrategyRunAsAny,
},
SupplementalGroups: policy.SupplementalGroupsStrategyOptions{
Rule: policy.SupplementalGroupsStrategyRunAsAny,
},
},
}
}
// goodPod is empty and should not be used directly for testing since we're providing
// two different PSPs. Since no values are specified it would be allowed to match any
// psp when defaults are filled in.
func goodPod() *kapi.Pod {
return &kapi.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod",
Namespace: "namespace",
Annotations: map[string]string{},
},
Spec: kapi.PodSpec{
ServiceAccountName: "default",
SecurityContext: &kapi.PodSecurityContext{},
Containers: []kapi.Container{
{
Name: defaultContainerName,
SecurityContext: &kapi.SecurityContext{},
},
},
},
}
}
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"],
)
/*
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, o admission.ObjectInterfaces) (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
}
/*
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), 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), 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), 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{
{},
},
},
}
}
# See the OWNERS docs at https://go.k8s.io/owners
reviewers:
- andrewsykim
- dims
- msau42
approvers:
- andrewsykim
- dims
- msau42
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"],
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"],
)
/*
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, o admission.ObjectInterfaces) 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
}
/*
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, nil)
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))
}
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment