mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-14 06:15:45 +00:00
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:
commit
a9fbeef694
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
40
pkg/auth/nodeidentifier/BUILD
Normal file
40
pkg/auth/nodeidentifier/BUILD
Normal 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"],
|
||||
)
|
64
pkg/auth/nodeidentifier/default.go
Normal file
64
pkg/auth/nodeidentifier/default.go
Normal 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
|
||||
}
|
68
pkg/auth/nodeidentifier/default_test.go
Normal file
68
pkg/auth/nodeidentifier/default_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
30
pkg/auth/nodeidentifier/interfaces.go
Normal file
30
pkg/auth/nodeidentifier/interfaces.go
Normal 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)
|
||||
}
|
@ -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",
|
||||
|
54
plugin/pkg/admission/noderestriction/BUILD
Normal file
54
plugin/pkg/admission/noderestriction/BUILD
Normal 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"],
|
||||
)
|
8
plugin/pkg/admission/noderestriction/OWNERS
Normal file
8
plugin/pkg/admission/noderestriction/OWNERS
Normal file
@ -0,0 +1,8 @@
|
||||
approvers:
|
||||
- deads2k
|
||||
- liggitt
|
||||
- timstclair
|
||||
reviewers:
|
||||
- deads2k
|
||||
- liggitt
|
||||
- timstclair
|
203
plugin/pkg/admission/noderestriction/admission.go
Normal file
203
plugin/pkg/admission/noderestriction/admission.go
Normal 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
|
||||
}
|
476
plugin/pkg/admission/noderestriction/admission_test.go
Normal file
476
plugin/pkg/admission/noderestriction/admission_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user