Merge pull request #45929 from liggitt/node-admission

Automatic merge from submit-queue (batch tested with PRs 41535, 45985, 45929, 45948, 46056)

NodeRestriction admission plugin

Adds an optional `NodeRestriction` admission plugin that limits identifiable kubelets to mutating their own Node object, and Pod objects bound to their node.

This is the admission portion of https://github.com/kubernetes/community/blob/master/contributors/design-proposals/kubelet-authorizer.md and kubernetes/features#279

```release-note
The `NodeRestriction` admission plugin limits the `Node` and `Pod` objects a kubelet can modify. In order to be limited by this admission plugin, kubelets must use credentials in the `system:nodes` group, with a username in the form `system:node:<nodeName>`. Such kubelets will only be allowed to modify their own `Node` API object, and only modify `Pod` API objects that are bound to their node.
```
This commit is contained in:
Kubernetes Submit Queue 2017-05-18 19:58:13 -07:00 committed by GitHub
commit a9fbeef694
16 changed files with 1015 additions and 4 deletions

View File

@ -54,6 +54,7 @@ go_library(
"//plugin/pkg/admission/namespace/autoprovision:go_default_library",
"//plugin/pkg/admission/namespace/exists:go_default_library",
"//plugin/pkg/admission/namespace/lifecycle:go_default_library",
"//plugin/pkg/admission/noderestriction:go_default_library",
"//plugin/pkg/admission/persistentvolume/label:go_default_library",
"//plugin/pkg/admission/podnodeselector:go_default_library",
"//plugin/pkg/admission/podpreset:go_default_library",

View File

@ -37,6 +37,7 @@ import (
_ "k8s.io/kubernetes/plugin/pkg/admission/namespace/autoprovision"
_ "k8s.io/kubernetes/plugin/pkg/admission/namespace/exists"
_ "k8s.io/kubernetes/plugin/pkg/admission/namespace/lifecycle"
_ "k8s.io/kubernetes/plugin/pkg/admission/noderestriction"
_ "k8s.io/kubernetes/plugin/pkg/admission/persistentvolume/label"
_ "k8s.io/kubernetes/plugin/pkg/admission/podnodeselector"
_ "k8s.io/kubernetes/plugin/pkg/admission/podpreset"

View File

@ -87,6 +87,7 @@ pkg/apis/settings/install
pkg/apis/settings/validation
pkg/apis/storage/install
pkg/apis/storage/validation
pkg/auth/nodeidentifier
pkg/bootstrap/api
pkg/client/conditions
pkg/client/informers/informers_generated/externalversions

View File

@ -388,6 +388,9 @@ function start_apiserver {
if [[ -n "${PSP_ADMISSION}" ]]; then
security_admission=",PodSecurityPolicy"
fi
if [[ -n "${NODE_ADMISSION}" ]]; then
security_admission=",NodeRestriction"
fi
# Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,LimitRanger,ServiceAccount${security_admission},ResourceQuota,DefaultStorageClass,DefaultTolerationSeconds

View File

@ -31,6 +31,7 @@ filegroup(
"//pkg/apis/settings:all-srcs",
"//pkg/apis/storage:all-srcs",
"//pkg/auth/authorizer/abac:all-srcs",
"//pkg/auth/nodeidentifier:all-srcs",
"//pkg/auth/user:all-srcs",
"//pkg/bootstrap/api:all-srcs",
"//pkg/capabilities:all-srcs",

View File

@ -21,11 +21,14 @@ import (
"k8s.io/kubernetes/pkg/api"
)
// Visitor is called with each object name, and returns true if visiting should continue
type Visitor func(name string) (shouldContinue bool)
// VisitPodSecretNames invokes the visitor function with the name of every secret
// referenced by the pod spec. If visitor returns false, visiting is short-circuited.
// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited.
// Returns true if visiting completed, false if visiting was short-circuited.
func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool {
func VisitPodSecretNames(pod *api.Pod, visitor Visitor) bool {
for _, reference := range pod.Spec.ImagePullSecrets {
if !visitor(reference.Name) {
return false
@ -86,7 +89,7 @@ func VisitPodSecretNames(pod *api.Pod, visitor func(string) bool) bool {
return true
}
func visitContainerSecretNames(container *api.Container, visitor func(string) bool) bool {
func visitContainerSecretNames(container *api.Container, visitor Visitor) bool {
for _, env := range container.EnvFrom {
if env.SecretRef != nil {
if !visitor(env.SecretRef.Name) {
@ -104,6 +107,60 @@ func visitContainerSecretNames(container *api.Container, visitor func(string) bo
return true
}
// VisitPodConfigmapNames invokes the visitor function with the name of every configmap
// referenced by the pod spec. If visitor returns false, visiting is short-circuited.
// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited.
// Returns true if visiting completed, false if visiting was short-circuited.
func VisitPodConfigmapNames(pod *api.Pod, visitor Visitor) bool {
for i := range pod.Spec.InitContainers {
if !visitContainerConfigmapNames(&pod.Spec.InitContainers[i], visitor) {
return false
}
}
for i := range pod.Spec.Containers {
if !visitContainerConfigmapNames(&pod.Spec.Containers[i], visitor) {
return false
}
}
var source *api.VolumeSource
for i := range pod.Spec.Volumes {
source = &pod.Spec.Volumes[i].VolumeSource
switch {
case source.Projected != nil:
for j := range source.Projected.Sources {
if source.Projected.Sources[j].ConfigMap != nil {
if !visitor(source.Projected.Sources[j].ConfigMap.Name) {
return false
}
}
}
case source.ConfigMap != nil:
if !visitor(source.ConfigMap.Name) {
return false
}
}
}
return true
}
func visitContainerConfigmapNames(container *api.Container, visitor Visitor) bool {
for _, env := range container.EnvFrom {
if env.ConfigMapRef != nil {
if !visitor(env.ConfigMapRef.Name) {
return false
}
}
}
for _, envVar := range container.Env {
if envVar.ValueFrom != nil && envVar.ValueFrom.ConfigMapKeyRef != nil {
if !visitor(envVar.ValueFrom.ConfigMapKeyRef.Name) {
return false
}
}
}
return true
}
// IsPodReady returns true if a pod is ready; false otherwise.
func IsPodReady(pod *api.Pod) bool {
return IsPodReadyConditionTrue(pod.Status)

View File

@ -107,11 +107,14 @@ func SetInitContainersStatusesAnnotations(pod *v1.Pod) error {
return nil
}
// Visitor is called with each object name, and returns true if visiting should continue
type Visitor func(name string) (shouldContinue bool)
// VisitPodSecretNames invokes the visitor function with the name of every secret
// referenced by the pod spec. If visitor returns false, visiting is short-circuited.
// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited.
// Returns true if visiting completed, false if visiting was short-circuited.
func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool {
func VisitPodSecretNames(pod *v1.Pod, visitor Visitor) bool {
for _, reference := range pod.Spec.ImagePullSecrets {
if !visitor(reference.Name) {
return false
@ -173,7 +176,7 @@ func VisitPodSecretNames(pod *v1.Pod, visitor func(string) bool) bool {
return true
}
func visitContainerSecretNames(container *v1.Container, visitor func(string) bool) bool {
func visitContainerSecretNames(container *v1.Container, visitor Visitor) bool {
for _, env := range container.EnvFrom {
if env.SecretRef != nil {
if !visitor(env.SecretRef.Name) {

View File

@ -0,0 +1,40 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"default.go",
"interfaces.go",
],
tags = ["automanaged"],
deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"],
)
go_test(
name = "go_default_test",
srcs = ["default_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,64 @@
/*
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 nodeidentifier
import (
"strings"
"k8s.io/apiserver/pkg/authentication/user"
)
// NewDefaultNodeIdentifier returns a default NodeIdentifier implementation,
// which returns isNode=true if the user groups contain the system:nodes group,
// and populates nodeName if isNode is true, and the user name is in the format system:node:<nodeName>
func NewDefaultNodeIdentifier() NodeIdentifier {
return defaultNodeIdentifier{}
}
// defaultNodeIdentifier implements NodeIdentifier
type defaultNodeIdentifier struct{}
// nodeUserNamePrefix is the prefix for usernames in the form `system:node:<nodeName>`
const nodeUserNamePrefix = "system:node:"
// NodeIdentity returns isNode=true if the user groups contain the system:nodes group,
// and populates nodeName if isNode is true, and the user name is in the format system:node:<nodeName>
func (defaultNodeIdentifier) NodeIdentity(u user.Info) (string, bool) {
// Make sure we're a node, and can parse the node name
if u == nil {
return "", false
}
isNode := false
for _, g := range u.GetGroups() {
if g == user.NodesGroup {
isNode = true
break
}
}
if !isNode {
return "", false
}
userName := u.GetName()
nodeName := ""
if strings.HasPrefix(userName, nodeUserNamePrefix) {
nodeName = strings.TrimPrefix(userName, nodeUserNamePrefix)
}
return nodeName, isNode
}

View File

@ -0,0 +1,68 @@
/*
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 nodeidentifier
import (
"testing"
"k8s.io/apiserver/pkg/authentication/user"
)
func TestDefaultNodeIdentifier_NodeIdentity(t *testing.T) {
tests := []struct {
name string
user user.Info
expectNodeName string
expectIsNode bool
}{
{
name: "nil user",
user: nil,
expectNodeName: "",
expectIsNode: false,
},
{
name: "node username without group",
user: &user.DefaultInfo{Name: "system:node:foo"},
expectNodeName: "",
expectIsNode: false,
},
{
name: "node group without username",
user: &user.DefaultInfo{Name: "foo", Groups: []string{"system:nodes"}},
expectNodeName: "",
expectIsNode: true,
},
{
name: "node group and username",
user: &user.DefaultInfo{Name: "system:node:foo", Groups: []string{"system:nodes"}},
expectNodeName: "foo",
expectIsNode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nodeName, isNode := NewDefaultNodeIdentifier().NodeIdentity(tt.user)
if nodeName != tt.expectNodeName {
t.Errorf("DefaultNodeIdentifier.NodeIdentity() got = %v, want %v", nodeName, tt.expectNodeName)
}
if isNode != tt.expectIsNode {
t.Errorf("DefaultNodeIdentifier.NodeIdentity() got1 = %v, want %v", isNode, tt.expectIsNode)
}
})
}
}

View File

@ -0,0 +1,30 @@
/*
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 nodeidentifier
import (
"k8s.io/apiserver/pkg/authentication/user"
)
// NodeIdentifier determines node information from a given user
type NodeIdentifier interface {
// IdentifyNode determines node information from the given user.Info.
// nodeName is the name of the Node API object associated with the user.Info,
// and may be empty if a specific node cannot be determined.
// isNode is true if the user.Info represents an identity issued to a node.
NodeIdentity(user.Info) (nodeName string, isNode bool)
}

View File

@ -27,6 +27,7 @@ filegroup(
"//plugin/pkg/admission/namespace/autoprovision:all-srcs",
"//plugin/pkg/admission/namespace/exists:all-srcs",
"//plugin/pkg/admission/namespace/lifecycle:all-srcs",
"//plugin/pkg/admission/noderestriction:all-srcs",
"//plugin/pkg/admission/persistentvolume/label:all-srcs",
"//plugin/pkg/admission/podnodeselector:all-srcs",
"//plugin/pkg/admission/podpreset:all-srcs",

View File

@ -0,0 +1,54 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["admission.go"],
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/api/pod:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//pkg/kubeapiserver/admission:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//pkg/api:go_default_library",
"//pkg/auth/nodeidentifier:go_default_library",
"//pkg/client/clientset_generated/internalclientset/fake:go_default_library",
"//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,8 @@
approvers:
- deads2k
- liggitt
- timstclair
reviewers:
- deads2k
- liggitt
- timstclair

View File

@ -0,0 +1,203 @@
/*
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 node
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/kubernetes/pkg/api"
podutil "k8s.io/kubernetes/pkg/api/pod"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
kubeapiserveradmission "k8s.io/kubernetes/pkg/kubeapiserver/admission"
)
const (
PluginName = "NodeRestriction"
)
func init() {
kubeapiserveradmission.Plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
return NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), false), nil
})
}
// NewPlugin creates a new NodeRestriction admission plugin.
// This plugin identifies requests from nodes
func NewPlugin(nodeIdentifier nodeidentifier.NodeIdentifier, strict bool) *nodePlugin {
return &nodePlugin{
Handler: admission.NewHandler(admission.Create, admission.Update, admission.Delete),
nodeIdentifier: nodeIdentifier,
strict: strict,
}
}
// nodePlugin holds state for and implements the admission plugin.
type nodePlugin struct {
*admission.Handler
strict bool
nodeIdentifier nodeidentifier.NodeIdentifier
podsGetter coreinternalversion.PodsGetter
}
var (
_ = admission.Interface(&nodePlugin{})
_ = kubeapiserveradmission.WantsInternalKubeClientSet(&nodePlugin{})
)
func (p *nodePlugin) SetInternalKubeClientSet(f internalclientset.Interface) {
p.podsGetter = f.Core()
}
func (p *nodePlugin) Validate() error {
if p.nodeIdentifier == nil {
return fmt.Errorf("%s requires a node identifier", PluginName)
}
if p.podsGetter == nil {
return fmt.Errorf("%s requires a pod getter", PluginName)
}
return nil
}
var (
podResource = api.Resource("pods")
nodeResource = api.Resource("nodes")
)
func (c *nodePlugin) Admit(a admission.Attributes) error {
nodeName, isNode := c.nodeIdentifier.NodeIdentity(a.GetUserInfo())
// Our job is just to restrict nodes
if !isNode {
return nil
}
if len(nodeName) == 0 {
if c.strict {
// In strict mode, disallow requests from nodes we cannot match to a particular node
return admission.NewForbidden(a, fmt.Errorf("could not determine node identity from user"))
}
// Our job is just to restrict identifiable nodes
return nil
}
switch a.GetResource().GroupResource() {
case podResource:
switch a.GetSubresource() {
case "":
return c.admitPod(nodeName, a)
case "status":
return c.admitPodStatus(nodeName, a)
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected pod subresource %s", a.GetSubresource()))
}
case nodeResource:
return c.admitNode(nodeName, a)
default:
return nil
}
}
func (c *nodePlugin) admitPod(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Create:
// require a pod object
pod, ok := a.GetObject().(*api.Pod)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// only allow nodes to create mirror pods
if _, isMirrorPod := pod.Annotations[api.MirrorPodAnnotationKey]; !isMirrorPod {
return admission.NewForbidden(a, fmt.Errorf("pod does not have %q annotation, node %s can only create mirror pods", api.MirrorPodAnnotationKey, nodeName))
}
// only allow nodes to create a pod bound to itself
if pod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %s can only create pods with spec.nodeName set to itself", nodeName))
}
// don't allow a node to create a pod that references any other API objects
if pod.Spec.ServiceAccountName != "" {
return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference a service account", nodeName))
}
hasSecrets := false
podutil.VisitPodSecretNames(pod, func(name string) (shouldContinue bool) { hasSecrets = true; return false })
if hasSecrets {
return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference secrets", nodeName))
}
hasConfigMaps := false
podutil.VisitPodConfigmapNames(pod, func(name string) (shouldContinue bool) { hasConfigMaps = true; return false })
if hasConfigMaps {
return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference configmaps", nodeName))
}
for _, v := range pod.Spec.Volumes {
if v.PersistentVolumeClaim != nil {
return admission.NewForbidden(a, fmt.Errorf("node %s can not create pods that reference persistentvolumeclaims", nodeName))
}
}
return nil
case admission.Delete:
// get the existing pod
existingPod, err := c.podsGetter.Pods(a.GetNamespace()).Get(a.GetName(), v1.GetOptions{ResourceVersion: "0"})
if err != nil {
return admission.NewForbidden(a, err)
}
// only allow a node to delete a pod bound to itself
if existingPod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %s can only delete pods with spec.nodeName set to itself", nodeName))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation()))
}
}
func (c *nodePlugin) admitPodStatus(nodeName string, a admission.Attributes) error {
switch a.GetOperation() {
case admission.Update:
// require an existing pod
pod, ok := a.GetOldObject().(*api.Pod)
if !ok {
return admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
}
// only allow a node to update status of a pod bound to itself
if pod.Spec.NodeName != nodeName {
return admission.NewForbidden(a, fmt.Errorf("node %s can only update pod status for pods with spec.nodeName set to itself", nodeName))
}
return nil
default:
return admission.NewForbidden(a, fmt.Errorf("unexpected operation %s", a.GetOperation()))
}
}
func (c *nodePlugin) admitNode(nodeName string, a admission.Attributes) error {
if a.GetName() != nodeName {
return admission.NewForbidden(a, fmt.Errorf("cannot modify other nodes"))
}
return nil
}

View File

@ -0,0 +1,476 @@
/*
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 node
import (
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/auth/nodeidentifier"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
coreinternalversion "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
)
func makeTestPod(namespace, name, node string, mirror bool) *api.Pod {
pod := &api.Pod{}
pod.Namespace = namespace
pod.Name = name
pod.Spec.NodeName = node
if mirror {
pod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"}
}
return pod
}
func Test_nodePlugin_Admit(t *testing.T) {
var (
mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}}
bob = &user.DefaultInfo{Name: "bob"}
mynodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode"}}
othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}}
mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true)
othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true)
unboundmirrorpod = makeTestPod("ns", "unboundmirrorpod", "", true)
mypod = makeTestPod("ns", "mypod", "mynode", false)
otherpod = makeTestPod("ns", "otherpod", "othernode", false)
unboundpod = makeTestPod("ns", "unboundpod", "", false)
configmapResource = api.Resource("configmap").WithVersion("v1")
configmapKind = api.Kind("ConfigMap").WithVersion("v1")
podResource = api.Resource("pods").WithVersion("v1")
podKind = api.Kind("Pod").WithVersion("v1")
nodeResource = api.Resource("nodes").WithVersion("v1")
nodeKind = api.Kind("Node").WithVersion("v1")
noExistingPods = fake.NewSimpleClientset().Core()
existingPods = fake.NewSimpleClientset(mymirrorpod, othermirrorpod, unboundmirrorpod, mypod, otherpod, unboundpod).Core()
)
sapod := makeTestPod("ns", "mysapod", "mynode", true)
sapod.Spec.ServiceAccountName = "foo"
secretpod := makeTestPod("ns", "mysecretpod", "mynode", true)
secretpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}}}
configmappod := makeTestPod("ns", "myconfigmappod", "mynode", true)
configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}}
pvcpod := makeTestPod("ns", "mypvcpod", "mynode", true)
pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}}
tests := []struct {
name string
strict bool
podsGetter coreinternalversion.PodsGetter
attributes admission.Attributes
err string
}{
// Mirror pods bound to us
{
name: "allow creating a mirror pod bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Create, mynode),
err: "",
},
{
name: "forbid update of mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow delete of mirror pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "",
},
{
name: "forbid create of mirror pod status bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow update of mirror pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mymirrorpod, mymirrorpod, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "",
},
{
name: "forbid delete of mirror pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mymirrorpod.Namespace, mymirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Mirror pods bound to another node
{
name: "forbid creating a mirror pod bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of mirror pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of mirror pod status bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of mirror pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othermirrorpod, othermirrorpod, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of mirror pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, othermirrorpod.Namespace, othermirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Mirror pods not bound to any node
{
name: "forbid creating a mirror pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Create, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid update of mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of mirror pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of mirror pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of mirror pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundmirrorpod, unboundmirrorpod, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of mirror pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundmirrorpod.Namespace, unboundmirrorpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Normal pods bound to us
{
name: "forbid creating a normal pod bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow delete of normal pod bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "", admission.Delete, mynode),
err: "",
},
{
name: "forbid create of normal pod status bound to self",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mypod, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "allow update of normal pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mypod, mypod, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Update, mynode),
err: "",
},
{
name: "forbid delete of normal pod status bound to self",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, mypod.Namespace, mypod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Normal pods bound to another
{
name: "forbid creating a normal pod bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of normal pod bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of normal pod status bound to another",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(otherpod, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of normal pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(otherpod, otherpod, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of normal pod status bound to another",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, otherpod.Namespace, otherpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Normal pods not bound to any node
{
name: "forbid creating a normal pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, mynode),
err: "can only create mirror pods",
},
{
name: "forbid update of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid delete of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid create of normal pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, mynode),
err: "forbidden: unexpected operation",
},
{
name: "forbid update of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, mynode),
err: "spec.nodeName set to itself",
},
{
name: "forbid delete of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, mynode),
err: "forbidden: unexpected operation",
},
// Missing pod
{
name: "forbid delete of unknown pod",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, mynode),
err: "not found",
},
// Resource pods
{
name: "forbid create of pod referencing service account",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(sapod, nil, podKind, sapod.Namespace, sapod.Name, podResource, "", admission.Create, mynode),
err: "reference a service account",
},
{
name: "forbid create of pod referencing secret",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(secretpod, nil, podKind, secretpod.Namespace, secretpod.Name, podResource, "", admission.Create, mynode),
err: "reference secrets",
},
{
name: "forbid create of pod referencing configmap",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, mynode),
err: "reference configmaps",
},
{
name: "forbid create of pod referencing persistentvolumeclaim",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(pvcpod, nil, podKind, pvcpod.Namespace, pvcpod.Name, podResource, "", admission.Create, mynode),
err: "reference persistentvolumeclaims",
},
// My node object
{
name: "allow create of my node",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow update of my node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow delete of my node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Delete, mynode),
err: "",
},
{
name: "allow update of my node status",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "status", admission.Update, mynode),
err: "",
},
// Other node object
{
name: "forbid create of other node",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Create, mynode),
err: "cannot modify other nodes",
},
{
name: "forbid update of other node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Update, mynode),
err: "cannot modify other nodes",
},
{
name: "forbid delete of other node",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Delete, mynode),
err: "cannot modify other nodes",
},
{
name: "forbid update of other node status",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "status", admission.Update, mynode),
err: "cannot modify other nodes",
},
// Unrelated objects
{
name: "allow create of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(&api.ConfigMap{}, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Create, mynode),
err: "",
},
{
name: "allow update of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(&api.ConfigMap{}, &api.ConfigMap{}, configmapKind, "myns", "mycm", configmapResource, "", admission.Update, mynode),
err: "",
},
{
name: "allow delete of unrelated object",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Delete, mynode),
err: "",
},
// Unrelated user
{
name: "allow unrelated user creating a normal pod unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Create, bob),
err: "",
},
{
name: "allow unrelated user update of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Update, bob),
err: "",
},
{
name: "allow unrelated user delete of normal pod unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "", admission.Delete, bob),
err: "",
},
{
name: "allow unrelated user create of normal pod status unbound",
podsGetter: noExistingPods,
attributes: admission.NewAttributesRecord(unboundpod, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Create, bob),
err: "",
},
{
name: "allow unrelated user update of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(unboundpod, unboundpod, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Update, bob),
err: "",
},
{
name: "allow unrelated user delete of normal pod status unbound",
podsGetter: existingPods,
attributes: admission.NewAttributesRecord(nil, nil, podKind, unboundpod.Namespace, unboundpod.Name, podResource, "status", admission.Delete, bob),
err: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier(), tt.strict)
c.podsGetter = tt.podsGetter
err := c.Admit(tt.attributes)
if (err == nil) != (len(tt.err) == 0) {
t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err)
return
}
if len(tt.err) > 0 && !strings.Contains(err.Error(), tt.err) {
t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, tt.err)
}
})
}
}