/* 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 noderestriction import ( "context" "reflect" "strings" "testing" "time" "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/features" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" 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/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authentication/user" corev1lister "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/component-base/featuregate" kubeletapis "k8s.io/kubelet/pkg/apis" authenticationapi "k8s.io/kubernetes/pkg/apis/authentication" "k8s.io/kubernetes/pkg/apis/coordination" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/policy" resourceapi "k8s.io/kubernetes/pkg/apis/resource" storage "k8s.io/kubernetes/pkg/apis/storage" "k8s.io/kubernetes/pkg/auth/nodeidentifier" "k8s.io/utils/pointer" ) func makeTestPod(namespace, name, node string, mirror bool) (*api.Pod, *corev1.Pod) { corePod := &api.Pod{} corePod.Namespace = namespace corePod.UID = types.UID("pod-uid") corePod.Name = name corePod.Spec.NodeName = node v1Pod := &corev1.Pod{} v1Pod.Namespace = namespace v1Pod.UID = types.UID("pod-uid") v1Pod.Name = name v1Pod.Spec.NodeName = node if mirror { corePod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"} v1Pod.Annotations = map[string]string{api.MirrorPodAnnotationKey: "true"} // Insert a valid owner reference by default. controller := true owner := metav1.OwnerReference{ APIVersion: "v1", Kind: "Node", Name: node, UID: types.UID(node + "-uid"), Controller: &controller, } corePod.OwnerReferences = []metav1.OwnerReference{owner} v1Pod.OwnerReferences = []metav1.OwnerReference{owner} } return corePod, v1Pod } func withLabels(pod *api.Pod, labels map[string]string) *api.Pod { labeledPod := pod.DeepCopy() if labels == nil { labeledPod.Labels = nil return labeledPod } // Clone. labeledPod.Labels = map[string]string{} for key, value := range labels { labeledPod.Labels[key] = value } return labeledPod } func makeTestPodEviction(name string) *policy.Eviction { eviction := &policy.Eviction{} eviction.Name = name eviction.Namespace = "ns" return eviction } func makeTokenRequest(podname string, poduid types.UID) *authenticationapi.TokenRequest { tr := &authenticationapi.TokenRequest{ Spec: authenticationapi.TokenRequestSpec{ Audiences: []string{"foo"}, }, } if podname != "" { tr.Spec.BoundObjectRef = &authenticationapi.BoundObjectReference{ Kind: "Pod", APIVersion: "v1", Name: podname, UID: poduid, } } return tr } func setAllLabels(node *api.Node, value string) *api.Node { node = setAllowedCreateLabels(node, value) node = setAllowedUpdateLabels(node, value) node = setForbiddenCreateLabels(node, value) node = setForbiddenUpdateLabels(node, value) return node } func setAllowedCreateLabels(node *api.Node, value string) *api.Node { node = setAllowedUpdateLabels(node, value) return node } func setAllowedUpdateLabels(node *api.Node, value string) *api.Node { node = node.DeepCopy() if node.Labels == nil { node.Labels = map[string]string{} } if value == "" { value = "value" } // non-kube labels node.Labels["foo"] = value node.Labels["example.com/foo"] = value // kubelet labels node.Labels["kubernetes.io/hostname"] = value node.Labels["failure-domain.beta.kubernetes.io/zone"] = value node.Labels["failure-domain.beta.kubernetes.io/region"] = value node.Labels["topology.kubernetes.io/zone"] = value node.Labels["topology.kubernetes.io/region"] = value node.Labels["beta.kubernetes.io/instance-type"] = value node.Labels["node.kubernetes.io/instance-type"] = value node.Labels["beta.kubernetes.io/os"] = value node.Labels["beta.kubernetes.io/arch"] = value node.Labels["kubernetes.io/os"] = value node.Labels["kubernetes.io/arch"] = value // kubelet label prefixes node.Labels["kubelet.kubernetes.io/foo"] = value node.Labels["foo.kubelet.kubernetes.io/foo"] = value node.Labels["node.kubernetes.io/foo"] = value node.Labels["foo.node.kubernetes.io/foo"] = value // test all explicitly allowed labels and prefixes for _, key := range kubeletapis.KubeletLabels() { node.Labels[key] = value } for _, namespace := range kubeletapis.KubeletLabelNamespaces() { node.Labels[namespace+"/foo"] = value node.Labels["foo."+namespace+"/foo"] = value } return node } func setForbiddenCreateLabels(node *api.Node, value string) *api.Node { node = node.DeepCopy() if node.Labels == nil { node.Labels = map[string]string{} } if value == "" { value = "value" } // node restriction labels are forbidden node.Labels["node-restriction.kubernetes.io/foo"] = value node.Labels["foo.node-restriction.kubernetes.io/foo"] = value node.Labels["other.kubernetes.io/foo"] = value node.Labels["other.k8s.io/foo"] = value return node } func setForbiddenUpdateLabels(node *api.Node, value string) *api.Node { node = node.DeepCopy() if node.Labels == nil { node.Labels = map[string]string{} } if value == "" { value = "value" } // node restriction labels are forbidden node.Labels["node-restriction.kubernetes.io/foo"] = value node.Labels["foo.node-restriction.kubernetes.io/foo"] = value // arbitrary kubernetes labels are forbidden on update node.Labels["other.kubernetes.io/foo"] = value node.Labels["other.k8s.io/foo"] = value return node } type admitTestCase struct { name string podsGetter corev1lister.PodLister nodesGetter corev1lister.NodeLister attributes admission.Attributes features featuregate.FeatureGate err string } func (a *admitTestCase) run(t *testing.T) { t.Run(a.name, func(t *testing.T) { c := NewPlugin(nodeidentifier.NewDefaultNodeIdentifier()) if a.features != nil { c.InspectFeatureGates(a.features) } c.podsGetter = a.podsGetter c.nodesGetter = a.nodesGetter err := c.Admit(context.TODO(), a.attributes, nil) if (err == nil) != (len(a.err) == 0) { t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, a.err) return } if len(a.err) > 0 && !strings.Contains(err.Error(), a.err) { t.Errorf("nodePlugin.Admit() error = %v, expected %v", err, a.err) } }) } func Test_nodePlugin_Admit(t *testing.T) { var ( mynode = &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} bob = &user.DefaultInfo{Name: "bob"} mynodeObjMeta = metav1.ObjectMeta{Name: "mynode", UID: "mynode-uid"} mynodeObj = &api.Node{ObjectMeta: mynodeObjMeta} mynodeObjConfigA = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{ConfigSource: &api.NodeConfigSource{ ConfigMap: &api.ConfigMapNodeConfigSource{ Name: "foo", Namespace: "bar", UID: "fooUID", KubeletConfigKey: "kubelet", }}}} mynodeObjConfigB = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{ConfigSource: &api.NodeConfigSource{ ConfigMap: &api.ConfigMapNodeConfigSource{ Name: "qux", Namespace: "bar", UID: "quxUID", KubeletConfigKey: "kubelet", }}}} mynodeObjTaintA = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{Taints: []api.Taint{{Key: "mykey", Value: "A"}}}} mynodeObjTaintB = &api.Node{ObjectMeta: mynodeObjMeta, Spec: api.NodeSpec{Taints: []api.Taint{{Key: "mykey", Value: "B"}}}} othernodeObj = &api.Node{ObjectMeta: metav1.ObjectMeta{Name: "othernode"}} coremymirrorpod, v1mymirrorpod = makeTestPod("ns", "mymirrorpod", "mynode", true) coreothermirrorpod, v1othermirrorpod = makeTestPod("ns", "othermirrorpod", "othernode", true) coreunboundmirrorpod, v1unboundmirrorpod = makeTestPod("ns", "unboundmirrorpod", "", true) coremypod, v1mypod = makeTestPod("ns", "mypod", "mynode", false) coreotherpod, v1otherpod = makeTestPod("ns", "otherpod", "othernode", false) coreunboundpod, v1unboundpod = makeTestPod("ns", "unboundpod", "", false) coreunnamedpod, _ = makeTestPod("ns", "", "mynode", false) mymirrorpodEviction = makeTestPodEviction("mymirrorpod") othermirrorpodEviction = makeTestPodEviction("othermirrorpod") unboundmirrorpodEviction = makeTestPodEviction("unboundmirrorpod") mypodEviction = makeTestPodEviction("mypod") otherpodEviction = makeTestPodEviction("otherpod") unboundpodEviction = makeTestPodEviction("unboundpod") unnamedEviction = makeTestPodEviction("") configmapResource = api.Resource("configmap").WithVersion("v1") configmapKind = api.Kind("ConfigMap").WithVersion("v1") podResource = api.Resource("pods").WithVersion("v1") podKind = api.Kind("Pod").WithVersion("v1") evictionKind = policy.Kind("Eviction").WithVersion("v1beta1") nodeResource = api.Resource("nodes").WithVersion("v1") nodeKind = api.Kind("Node").WithVersion("v1") svcacctResource = api.Resource("serviceaccounts").WithVersion("v1") tokenrequestKind = api.Kind("TokenRequest").WithVersion("v1") leaseResource = coordination.Resource("leases").WithVersion("v1beta1") leaseKind = coordination.Kind("Lease").WithVersion("v1beta1") lease = &coordination.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "mynode", Namespace: api.NamespaceNodeLease, }, Spec: coordination.LeaseSpec{ HolderIdentity: pointer.String("mynode"), LeaseDurationSeconds: pointer.Int32(40), RenewTime: &metav1.MicroTime{Time: time.Now()}, }, } leaseWrongNS = &coordination.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "mynode", Namespace: "foo", }, Spec: coordination.LeaseSpec{ HolderIdentity: pointer.String("mynode"), LeaseDurationSeconds: pointer.Int32(40), RenewTime: &metav1.MicroTime{Time: time.Now()}, }, } leaseWrongName = &coordination.Lease{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", Namespace: api.NamespaceNodeLease, }, Spec: coordination.LeaseSpec{ HolderIdentity: pointer.String("mynode"), LeaseDurationSeconds: pointer.Int32(40), RenewTime: &metav1.MicroTime{Time: time.Now()}, }, } csiNodeResource = storage.Resource("csinodes").WithVersion("v1") csiNodeKind = schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "CSINode"} nodeInfo = &storage.CSINode{ ObjectMeta: metav1.ObjectMeta{ Name: "mynode", }, Spec: storage.CSINodeSpec{ Drivers: []storage.CSINodeDriver{ { Name: "com.example.csi/mydriver", NodeID: "com.example.csi/mynode", TopologyKeys: []string{"com.example.csi/zone"}, }, }, }, } nodeInfoWrongName = &storage.CSINode{ ObjectMeta: metav1.ObjectMeta{ Name: "foo", }, Spec: storage.CSINodeSpec{ Drivers: []storage.CSINodeDriver{ { Name: "com.example.csi/mydriver", NodeID: "com.example.csi/foo", TopologyKeys: []string{"com.example.csi/zone"}, }, }, }, } existingNodesIndex = cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) existingNodes = corev1lister.NewNodeLister(existingNodesIndex) noExistingPodsIndex = cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) noExistingPods = corev1lister.NewPodLister(noExistingPodsIndex) existingPodsIndex = cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) existingPods = corev1lister.NewPodLister(existingPodsIndex) labelsA = map[string]string{ "label-a": "value-a", } labelsAB = map[string]string{ "label-a": "value-a", "label-b": "value-b", } aLabeledPod = withLabels(coremypod, labelsA) abLabeledPod = withLabels(coremypod, labelsAB) ) existingPodsIndex.Add(v1mymirrorpod) existingPodsIndex.Add(v1othermirrorpod) existingPodsIndex.Add(v1unboundmirrorpod) existingPodsIndex.Add(v1mypod) existingPodsIndex.Add(v1otherpod) existingPodsIndex.Add(v1unboundpod) existingNodesIndex.Add(&corev1.Node{ObjectMeta: mynodeObjMeta}) 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"}}}}} ctbpod, _ := makeTestPod("ns", "myctbpod", "mynode", true) ctbpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ClusterTrustBundle: &api.ClusterTrustBundleProjection{Name: pointer.String("foo")}}}}}}} pvcpod, _ := makeTestPod("ns", "mypvcpod", "mynode", true) pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}} tests := []admitTestCase{ // Mirror pods bound to us { name: "allow creating a mirror pod bound to self", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coremymirrorpod, nil, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "forbid update of mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coremymirrorpod, coremymirrorpod, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow delete of mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "", }, { name: "forbid create of mirror pod status bound to self", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coremymirrorpod, nil, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow update of mirror pod status bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coremymirrorpod, coremymirrorpod, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "forbid delete of mirror pod status bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow create of eviction for mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "forbid update of eviction for mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mymirrorpodEviction, nil, evictionKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow create of unnamed eviction for mirror pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coremymirrorpod.Namespace, coremymirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, // Mirror pods bound to another node { name: "forbid creating a mirror pod bound to another", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreothermirrorpod, nil, podKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of mirror pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreothermirrorpod, coreothermirrorpod, podKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of mirror pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid create of mirror pod status bound to another", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreothermirrorpod, nil, podKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid update of mirror pod status bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreothermirrorpod, coreothermirrorpod, podKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, 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, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of eviction for mirror pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of eviction for mirror pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for mirror pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(othermirrorpodEviction, nil, evictionKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for mirror pod to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coreothermirrorpod.Namespace, coreothermirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, // Mirror pods not bound to any node { name: "forbid creating a mirror pod unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundmirrorpod, nil, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundmirrorpod, coreunboundmirrorpod, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid create of mirror pod status unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundmirrorpod, nil, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid update of mirror pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundmirrorpod, coreunboundmirrorpod, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid delete of mirror pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of eviction for mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of eviction for mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundmirrorpodEviction, nil, evictionKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for mirror pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coreunboundmirrorpod.Namespace, coreunboundmirrorpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, // Normal pods bound to us { name: "forbid creating a normal pod bound to self", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coremypod, nil, podKind, coremypod.Namespace, coremypod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "can only create mirror pods", }, { name: "forbid update of normal pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coremypod, coremypod, podKind, coremypod.Namespace, coremypod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow delete of normal pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coremypod.Namespace, coremypod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "", }, { name: "forbid create of normal pod status bound to self", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coremypod, nil, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow update of normal pod status bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coremypod, coremypod, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "forbid delete of normal pod status bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid addition of pod status preexisting labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(abLabeledPod, aLabeledPod, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "cannot update labels through pod status", }, { name: "forbid deletion of pod status preexisting labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(aLabeledPod, abLabeledPod, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "cannot update labels through pod status", }, { name: "forbid deletion of all pod status preexisting labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(aLabeledPod, coremypod, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "cannot update labels through pod status", }, { name: "forbid addition of pod status labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coremypod, aLabeledPod, podKind, coremypod.Namespace, coremypod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "cannot update labels through pod status", }, { name: "forbid update of eviction for normal pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for normal pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "allow create of unnamed eviction for normal pod bound to self", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, // Normal pods bound to another { name: "forbid creating a normal pod bound to another", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreotherpod, nil, podKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "can only create mirror pods", }, { name: "forbid update of normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreotherpod, coreotherpod, podKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "", admission.Delete, &metav1.UpdateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid create of normal pod status bound to another", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreotherpod, nil, podKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid update of normal pod status bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreotherpod, coreotherpod, podKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, 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, coreotherpod.Namespace, coreotherpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of eviction for normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of eviction for normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(otherpodEviction, nil, evictionKind, otherpodEviction.Namespace, otherpodEviction.Name, podResource, "eviction", admission.Delete, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for normal pod bound to another", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coreotherpod.Namespace, coreotherpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, // Normal pods not bound to any node { name: "forbid creating a normal pod unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundpod, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "can only create mirror pods", }, { name: "forbid update of normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundpod, coreunboundpod, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid create of normal pod status unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundpod, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid update of normal pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundpod, coreunboundpod, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid delete of normal pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of eviction for normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, { name: "forbid update of eviction for normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unboundpodEviction, nil, evictionKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for normal unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "spec.nodeName set to itself", }, // Missing pod { name: "forbid delete of unknown pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "not found", }, { name: "forbid create of eviction for unknown pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "not found", }, { name: "forbid update of eviction for unknown pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for unknown pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for unknown pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coremypod.Namespace, coremypod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "not found", }, // Eviction for unnamed pod { name: "allow create of eviction for unnamed pod", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coreunnamedpod.Namespace, coreunnamedpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), // use the submitted eviction resource name as the pod name err: "", }, { name: "forbid update of eviction for unnamed pod", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coreunnamedpod.Namespace, coreunnamedpod.Name, podResource, "eviction", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid delete of eviction for unnamed pod", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mypodEviction, nil, evictionKind, coreunnamedpod.Namespace, coreunnamedpod.Name, podResource, "eviction", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: unexpected operation", }, { name: "forbid create of unnamed eviction for unnamed pod", podsGetter: existingPods, attributes: admission.NewAttributesRecord(unnamedEviction, nil, evictionKind, coreunnamedpod.Namespace, coreunnamedpod.Name, podResource, "eviction", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "could not determine pod from request data", }, // 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, &metav1.CreateOptions{}, false, 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, &metav1.CreateOptions{}, false, 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, &metav1.CreateOptions{}, false, mynode), err: "reference configmaps", }, { name: "forbid create of pod referencing clustertrustbundle", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(ctbpod, nil, podKind, ctbpod.Namespace, ctbpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "reference clustertrustbundles", }, { name: "forbid create of pod referencing persistentvolumeclaim", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(pvcpod, nil, podKind, pvcpod.Namespace, pvcpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, 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, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allow create of my node pulling name from object", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mynodeObj, nil, nodeKind, mynodeObj.Namespace, "mynode", nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allow create of my node with taints", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mynodeObjTaintA, nil, nodeKind, mynodeObj.Namespace, "mynode", nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allow create of my node with labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(setAllowedCreateLabels(mynodeObj, ""), nil, nodeKind, mynodeObj.Namespace, "mynode", nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "forbid create of my node with forbidden labels", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(setForbiddenCreateLabels(mynodeObj, ""), nil, nodeKind, mynodeObj.Namespace, "mynode", nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: `is not allowed to set the following labels: foo.node-restriction.kubernetes.io/foo, node-restriction.kubernetes.io/foo, other.k8s.io/foo, other.kubernetes.io/foo`, }, { name: "allow update of my node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObj, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow delete of my node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Delete, &metav1.DeleteOptions{}, false, 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, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "forbid create of my node with non-nil configSource", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(mynodeObjConfigA, nil, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "is not allowed to create pods with a non-nil configSource", }, { name: "forbid update of my node: nil configSource to new non-nil configSource", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjConfigA, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "update configSource to a new non-nil configSource", }, { name: "forbid update of my node: non-nil configSource to new non-nil configSource", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjConfigB, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "update configSource to a new non-nil configSource", }, { name: "allow update of my node: non-nil configSource unchanged", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjConfigA, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: non-nil configSource to nil configSource", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObj, mynodeObjConfigA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: no change to taints", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObjTaintA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: add allowed labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setAllowedUpdateLabels(mynodeObj, ""), mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: remove allowed labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObj, setAllowedUpdateLabels(mynodeObj, ""), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: modify allowed labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setAllowedUpdateLabels(mynodeObj, "b"), setAllowedUpdateLabels(mynodeObj, "a"), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: no change to labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setAllLabels(mynodeObj, ""), setAllLabels(mynodeObj, ""), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: add allowed labels while forbidden labels exist unmodified", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setAllLabels(mynodeObj, ""), setForbiddenUpdateLabels(mynodeObj, ""), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow update of my node: remove allowed labels while forbidden labels exist unmodified", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setForbiddenUpdateLabels(mynodeObj, ""), setAllLabels(mynodeObj, ""), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "forbid update of my node: add taints", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "is not allowed to modify taints", }, { name: "forbid update of my node: remove taints", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObj, mynodeObjTaintA, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "is not allowed to modify taints", }, { name: "forbid update of my node: change taints", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObjTaintA, mynodeObjTaintB, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "is not allowed to modify taints", }, { name: "forbid update of my node: add labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setForbiddenUpdateLabels(mynodeObj, ""), mynodeObj, nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: `is not allowed to modify labels: foo.node-restriction.kubernetes.io/foo, node-restriction.kubernetes.io/foo, other.k8s.io/foo, other.kubernetes.io/foo`, }, { name: "forbid update of my node: remove labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(mynodeObj, setForbiddenUpdateLabels(mynodeObj, ""), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: `is not allowed to modify labels: foo.node-restriction.kubernetes.io/foo, node-restriction.kubernetes.io/foo, other.k8s.io/foo, other.kubernetes.io/foo`, }, { name: "forbid update of my node: change labels", podsGetter: existingPods, attributes: admission.NewAttributesRecord(setForbiddenUpdateLabels(mynodeObj, "new"), setForbiddenUpdateLabels(mynodeObj, "old"), nodeKind, mynodeObj.Namespace, mynodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: `is not allowed to modify labels: foo.node-restriction.kubernetes.io/foo, node-restriction.kubernetes.io/foo, other.k8s.io/foo, other.kubernetes.io/foo`, }, // Other node object { name: "forbid create of other node", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "is not allowed to modify node", }, { name: "forbid create of other node pulling name from object", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(othernodeObj, nil, nodeKind, othernodeObj.Namespace, "", nodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "is not allowed to modify node", }, { name: "forbid update of other node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "is not allowed to modify node", }, { name: "forbid delete of other node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "is not allowed to modify node", }, { name: "forbid update of other node status", podsGetter: existingPods, attributes: admission.NewAttributesRecord(othernodeObj, othernodeObj, nodeKind, othernodeObj.Namespace, othernodeObj.Name, nodeResource, "status", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "is not allowed to modify node", }, // Service accounts { name: "forbid create of unbound token", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(makeTokenRequest("", ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "not bound to a pod", }, { name: "forbid create of token bound to nonexistant pod", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(makeTokenRequest("nopod", "someuid"), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "not found", }, { name: "forbid create of token bound to pod without uid", podsGetter: existingPods, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, ""), nil, tokenrequestKind, "ns", "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "pod binding without a uid", }, { name: "forbid create of token bound to pod scheduled on another node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(makeTokenRequest(coreotherpod.Name, coreotherpod.UID), nil, tokenrequestKind, coreotherpod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "pod scheduled on a different node", }, { name: "allow create of token bound to pod scheduled this node", podsGetter: existingPods, attributes: admission.NewAttributesRecord(makeTokenRequest(coremypod.Name, coremypod.UID), nil, tokenrequestKind, coremypod.Namespace, "mysa", svcacctResource, "token", admission.Create, &metav1.CreateOptions{}, false, mynode), }, // Unrelated objects { name: "allow create of unrelated object", podsGetter: existingPods, attributes: admission.NewAttributesRecord(&api.ConfigMap{}, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allow update of unrelated object", podsGetter: existingPods, attributes: admission.NewAttributesRecord(&api.ConfigMap{}, &api.ConfigMap{}, configmapKind, "myns", "mycm", configmapResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allow delete of unrelated object", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, configmapKind, "myns", "mycm", configmapResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "", }, // Unrelated user { name: "allow unrelated user creating a normal pod unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundpod, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, bob), err: "", }, { name: "allow unrelated user update of normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundpod, coreunboundpod, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Update, &metav1.UpdateOptions{}, false, bob), err: "", }, { name: "allow unrelated user delete of normal pod unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "", admission.Delete, &metav1.DeleteOptions{}, false, bob), err: "", }, { name: "allow unrelated user create of normal pod status unbound", podsGetter: noExistingPods, attributes: admission.NewAttributesRecord(coreunboundpod, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Create, &metav1.CreateOptions{}, false, bob), err: "", }, { name: "allow unrelated user update of normal pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(coreunboundpod, coreunboundpod, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Update, &metav1.UpdateOptions{}, false, bob), err: "", }, { name: "allow unrelated user delete of normal pod status unbound", podsGetter: existingPods, attributes: admission.NewAttributesRecord(nil, nil, podKind, coreunboundpod.Namespace, coreunboundpod.Name, podResource, "status", admission.Delete, &metav1.DeleteOptions{}, false, bob), err: "", }, // Node leases { name: "disallowed create lease in namespace other than kube-node-lease - feature enabled", attributes: admission.NewAttributesRecord(leaseWrongNS, nil, leaseKind, leaseWrongNS.Namespace, leaseWrongNS.Name, leaseResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed update lease in namespace other than kube-node-lease - feature enabled", attributes: admission.NewAttributesRecord(leaseWrongNS, leaseWrongNS, leaseKind, leaseWrongNS.Namespace, leaseWrongNS.Name, leaseResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed delete lease in namespace other than kube-node-lease - feature enabled", attributes: admission.NewAttributesRecord(nil, nil, leaseKind, leaseWrongNS.Namespace, leaseWrongNS.Name, leaseResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed create another node's lease - feature enabled", attributes: admission.NewAttributesRecord(leaseWrongName, nil, leaseKind, leaseWrongName.Namespace, leaseWrongName.Name, leaseResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed update another node's lease - feature enabled", attributes: admission.NewAttributesRecord(leaseWrongName, leaseWrongName, leaseKind, leaseWrongName.Namespace, leaseWrongName.Name, leaseResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed delete another node's lease - feature enabled", attributes: admission.NewAttributesRecord(nil, nil, leaseKind, leaseWrongName.Namespace, leaseWrongName.Name, leaseResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: ", }, { name: "allowed create node lease - feature enabled", attributes: admission.NewAttributesRecord(lease, nil, leaseKind, lease.Namespace, lease.Name, leaseResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allowed update node lease - feature enabled", attributes: admission.NewAttributesRecord(lease, lease, leaseKind, lease.Namespace, lease.Name, leaseResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allowed delete node lease - feature enabled", attributes: admission.NewAttributesRecord(nil, nil, leaseKind, lease.Namespace, lease.Name, leaseResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "", }, // CSINode { name: "disallowed create another node's CSINode", attributes: admission.NewAttributesRecord(nodeInfoWrongName, nil, csiNodeKind, nodeInfoWrongName.Namespace, nodeInfoWrongName.Name, csiNodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed update another node's CSINode", attributes: admission.NewAttributesRecord(nodeInfoWrongName, nodeInfoWrongName, csiNodeKind, nodeInfoWrongName.Namespace, nodeInfoWrongName.Name, csiNodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "forbidden: ", }, { name: "disallowed delete another node's CSINode", attributes: admission.NewAttributesRecord(nil, nil, csiNodeKind, nodeInfoWrongName.Namespace, nodeInfoWrongName.Name, csiNodeResource, "", admission.Delete, &metav1.DeleteOptions{}, false, mynode), err: "forbidden: ", }, { name: "allowed create node CSINode", attributes: admission.NewAttributesRecord(nodeInfo, nil, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), err: "", }, { name: "allowed update node CSINode", attributes: admission.NewAttributesRecord(nodeInfo, nodeInfo, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Update, &metav1.UpdateOptions{}, false, mynode), err: "", }, { name: "allowed delete node CSINode", attributes: admission.NewAttributesRecord(nil, nil, csiNodeKind, nodeInfo.Namespace, nodeInfo.Name, csiNodeResource, "", admission.Delete, &metav1.UpdateOptions{}, false, mynode), err: "", }, } for _, tt := range tests { tt.nodesGetter = existingNodes tt.run(t) } } func Test_nodePlugin_Admit_OwnerReference(t *testing.T) { expectedNodeIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) expectedNodeIndex.Add(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode", UID: "mynode-uid"}}) expectedNode := corev1lister.NewNodeLister(expectedNodeIndex) unexpectedNodeIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) unexpectedNodeIndex.Add(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode", UID: "mynode-unexpected-uid"}}) unexpectedNode := corev1lister.NewNodeLister(unexpectedNodeIndex) noNodesIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) noNodes := corev1lister.NewNodeLister(noNodesIndex) noExistingPodsIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) noExistingPods := corev1lister.NewPodLister(noExistingPodsIndex) mynode := &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} validOwner := metav1.OwnerReference{ APIVersion: "v1", Kind: "Node", Name: "mynode", UID: "mynode-uid", Controller: pointer.BoolPtr(true), } invalidName := validOwner invalidName.Name = "other" invalidKind := validOwner invalidKind.Kind = "Pod" invalidAPI := validOwner invalidAPI.APIVersion = "v2" invalidControllerNil := validOwner invalidControllerNil.Controller = nil invalidControllerFalse := validOwner invalidControllerFalse.Controller = pointer.BoolPtr(false) invalidBlockDeletion := validOwner invalidBlockDeletion.BlockOwnerDeletion = pointer.BoolPtr(true) tests := []struct { name string owners []metav1.OwnerReference nodesGetter corev1lister.NodeLister expectErr string }{ { name: "no owner", owners: nil, expectErr: "pods \"test\" is forbidden: node \"mynode\" can only create pods with an owner reference set to itself", }, { name: "valid owner", owners: []metav1.OwnerReference{validOwner}, }, { name: "duplicate owner", owners: []metav1.OwnerReference{validOwner, validOwner}, expectErr: "can only create pods with a single owner reference set to itself", }, { name: "invalid name", owners: []metav1.OwnerReference{invalidName}, expectErr: "can only create pods with an owner reference set to itself", }, { name: "invalid UID", owners: []metav1.OwnerReference{validOwner}, nodesGetter: unexpectedNode, expectErr: "UID mismatch", }, { name: "node not found", owners: []metav1.OwnerReference{validOwner}, nodesGetter: noNodes, expectErr: "not found", }, { name: "invalid API version", owners: []metav1.OwnerReference{invalidAPI}, expectErr: "can only create pods with an owner reference set to itself", }, { name: "invalid kind", owners: []metav1.OwnerReference{invalidKind}, expectErr: "can only create pods with an owner reference set to itself", }, { name: "nil controller", owners: []metav1.OwnerReference{invalidControllerNil}, expectErr: "can only create pods with a controller owner reference set to itself", }, { name: "false controller", owners: []metav1.OwnerReference{invalidControllerFalse}, expectErr: "can only create pods with a controller owner reference set to itself", }, { name: "invalid blockOwnerDeletion", owners: []metav1.OwnerReference{invalidBlockDeletion}, expectErr: "must not set blockOwnerDeletion on an owner reference", }, } for _, test := range tests { if test.nodesGetter == nil { test.nodesGetter = expectedNode } pod, _ := makeTestPod("ns", "test", "mynode", true) pod.OwnerReferences = test.owners a := &admitTestCase{ name: test.name, podsGetter: noExistingPods, nodesGetter: test.nodesGetter, attributes: createPodAttributes(pod, mynode), err: test.expectErr, } a.run(t) } } func Test_getModifiedLabels(t *testing.T) { tests := []struct { name string a map[string]string b map[string]string want sets.String }{ { name: "empty", a: nil, b: nil, want: sets.NewString(), }, { name: "no change", a: map[string]string{"x": "1", "y": "2", "z": "3"}, b: map[string]string{"x": "1", "y": "2", "z": "3"}, want: sets.NewString(), }, { name: "added", a: map[string]string{}, b: map[string]string{"a": "0"}, want: sets.NewString("a"), }, { name: "removed", a: map[string]string{"z": "3"}, b: map[string]string{}, want: sets.NewString("z"), }, { name: "changed", a: map[string]string{"z": "3"}, b: map[string]string{"z": "4"}, want: sets.NewString("z"), }, { name: "added empty", a: map[string]string{}, b: map[string]string{"a": ""}, want: sets.NewString("a"), }, { name: "removed empty", a: map[string]string{"z": ""}, b: map[string]string{}, want: sets.NewString("z"), }, { name: "changed to empty", a: map[string]string{"z": "3"}, b: map[string]string{"z": ""}, want: sets.NewString("z"), }, { name: "changed from empty", a: map[string]string{"z": ""}, b: map[string]string{"z": "3"}, want: sets.NewString("z"), }, { name: "added, removed, and changed", a: map[string]string{"a": "1", "b": "2"}, b: map[string]string{"a": "2", "c": "3"}, want: sets.NewString("a", "b", "c"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := getModifiedLabels(tt.a, tt.b); !reflect.DeepEqual(got, tt.want) { t.Errorf("getModifiedLabels() = %v, want %v", got, tt.want) } }) } } func TestAdmitPVCStatus(t *testing.T) { expectedNodeIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) expectedNodeIndex.Add(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "mynode", UID: "mynode-uid"}}) expectedNode := corev1lister.NewNodeLister(expectedNodeIndex) noExistingPodsIndex := cache.NewIndexer(cache.MetaNamespaceKeyFunc, nil) noExistingPods := corev1lister.NewPodLister(noExistingPodsIndex) mynode := &user.DefaultInfo{Name: "system:node:mynode", Groups: []string{"system:nodes"}} nodeExpansionFailed := api.PersistentVolumeClaimNodeResizeFailed tests := []struct { name string resource schema.GroupVersionResource subresource string newObj runtime.Object oldObj runtime.Object expansionFeatureEnabled bool recoveryFeatureEnabled bool expectError string }{ { name: "should not allow full pvc update from nodes", oldObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", nil, ), subresource: "", newObj: makeTestPVC( "", "10G", nil, ), expectError: "is forbidden: may only update PVC status", }, { name: "should allow capacity and condition updates, if expansion is enabled", oldObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", nil, ), expansionFeatureEnabled: true, subresource: "status", newObj: makeTestPVC( api.PersistentVolumeClaimFileSystemResizePending, "10G", nil, ), expectError: "", }, { name: "should not allow updates to allocatedResources with just expansion enabled", oldObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", nil, ), subresource: "status", expansionFeatureEnabled: true, newObj: makeTestPVC( api.PersistentVolumeClaimFileSystemResizePending, "15G", nil, ), expectError: "is not allowed to update fields other than", }, { name: "should allow updates to allocatedResources with expansion and recovery enabled", oldObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", nil, ), subresource: "status", expansionFeatureEnabled: true, recoveryFeatureEnabled: true, newObj: makeTestPVC( api.PersistentVolumeClaimFileSystemResizePending, "15G", nil, ), expectError: "", }, { name: "should allow updates to resizeStatus with expansion and recovery enabled", oldObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", nil, ), subresource: "status", expansionFeatureEnabled: true, recoveryFeatureEnabled: true, newObj: makeTestPVC( api.PersistentVolumeClaimResizing, "10G", &nodeExpansionFailed, ), expectError: "", }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { operation := admission.Update apiResource := api.SchemeGroupVersion.WithResource("persistentvolumeclaims") attributes := admission.NewAttributesRecord( test.newObj, test.oldObj, schema.GroupVersionKind{}, metav1.NamespaceDefault, "foo", apiResource, test.subresource, operation, &metav1.CreateOptions{}, false, mynode) defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.RecoverVolumeExpansionFailure, test.recoveryFeatureEnabled)() a := &admitTestCase{ name: test.name, podsGetter: noExistingPods, nodesGetter: expectedNode, attributes: attributes, features: feature.DefaultFeatureGate, err: test.expectError, } a.run(t) }) } } func makeTestPVC( condition api.PersistentVolumeClaimConditionType, allocatedResources string, resizeStatus *api.ClaimResourceStatus) *api.PersistentVolumeClaim { pvc := &api.PersistentVolumeClaim{ Spec: api.PersistentVolumeClaimSpec{ VolumeName: "volume1", Resources: api.VolumeResourceRequirements{ Requests: api.ResourceList{ api.ResourceStorage: resource.MustParse("10G"), }, }, }, Status: api.PersistentVolumeClaimStatus{ Capacity: api.ResourceList{ api.ResourceStorage: resource.MustParse(allocatedResources), }, Phase: api.ClaimBound, AllocatedResources: api.ResourceList{ api.ResourceStorage: resource.MustParse(allocatedResources), }, }, } if resizeStatus != nil { claimStatusMap := map[api.ResourceName]api.ClaimResourceStatus{ api.ResourceStorage: *resizeStatus, } pvc.Status.AllocatedResourceStatuses = claimStatusMap } if len(condition) > 0 { pvc.Status.Conditions = []api.PersistentVolumeClaimCondition{ { Type: condition, Status: api.ConditionTrue, }, } } return pvc } func createPodAttributes(pod *api.Pod, user user.Info) admission.Attributes { podResource := api.Resource("pods").WithVersion("v1") podKind := api.Kind("Pod").WithVersion("v1") return admission.NewAttributesRecord(pod, nil, podKind, pod.Namespace, pod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, user) } func TestAdmitResourceSlice(t *testing.T) { apiResource := resourceapi.SchemeGroupVersion.WithResource("resourceslices") nodename := "mynode" mynode := &user.DefaultInfo{Name: "system:node:" + nodename, Groups: []string{"system:nodes"}} err := "can only create ResourceSlice with the same NodeName as the requesting node" sliceNode := &resourceapi.ResourceSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "something", }, NodeName: nodename, } sliceOtherNode := &resourceapi.ResourceSlice{ ObjectMeta: metav1.ObjectMeta{ Name: "something", }, NodeName: nodename + "-other", } tests := map[string]struct { operation admission.Operation obj runtime.Object featureEnabled bool expectError string }{ "create allowed, enabled": { operation: admission.Create, obj: sliceNode, featureEnabled: true, expectError: "", }, "create disallowed, enabled": { operation: admission.Create, obj: sliceOtherNode, featureEnabled: true, expectError: err, }, "create allowed, disabled": { operation: admission.Create, obj: sliceNode, featureEnabled: false, expectError: "", }, "create disallowed, disabled": { operation: admission.Create, obj: sliceOtherNode, featureEnabled: false, expectError: err, }, "update allowed, same node": { operation: admission.Update, obj: sliceNode, featureEnabled: true, expectError: "", }, "update allowed, other node": { operation: admission.Update, obj: sliceOtherNode, featureEnabled: true, expectError: "", }, } for name, test := range tests { t.Run(name, func(t *testing.T) { attributes := admission.NewAttributesRecord( test.obj, nil, schema.GroupVersionKind{}, "", "foo", apiResource, "", test.operation, &metav1.CreateOptions{}, false, mynode) defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.DynamicResourceAllocation, test.featureEnabled)() a := &admitTestCase{ name: name, attributes: attributes, features: feature.DefaultFeatureGate, err: test.expectError, } a.run(t) }) } }