mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	ServiceAccount admission plugin
This commit is contained in:
		@@ -72,7 +72,7 @@ DNS_DOMAIN="kubernetes.local"
 | 
				
			|||||||
DNS_REPLICAS=1
 | 
					DNS_REPLICAS=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Admission Controllers to invoke prior to persisting objects in cluster
 | 
					# Admission Controllers to invoke prior to persisting objects in cluster
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Optional: Enable/disable public IP assignment for minions.
 | 
					# Optional: Enable/disable public IP assignment for minions.
 | 
				
			||||||
# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
 | 
					# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1
 | 
				
			|||||||
ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}"
 | 
					ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Admission Controllers to invoke prior to persisting objects in cluster
 | 
					# Admission Controllers to invoke prior to persisting objects in cluster
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,4 +76,4 @@ DNS_DOMAIN="kubernetes.local"
 | 
				
			|||||||
DNS_REPLICAS=1
 | 
					DNS_REPLICAS=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Admission Controllers to invoke prior to persisting objects in cluster
 | 
					# Admission Controllers to invoke prior to persisting objects in cluster
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,4 +74,4 @@ DNS_SERVER_IP="10.0.0.10"
 | 
				
			|||||||
DNS_DOMAIN="kubernetes.local"
 | 
					DNS_DOMAIN="kubernetes.local"
 | 
				
			||||||
DNS_REPLICAS=1
 | 
					DNS_REPLICAS=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,7 +50,7 @@ MASTER_USER=vagrant
 | 
				
			|||||||
MASTER_PASSWD=vagrant
 | 
					MASTER_PASSWD=vagrant
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Admission Controllers to invoke prior to persisting objects in cluster
 | 
					# Admission Controllers to invoke prior to persisting objects in cluster
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Optional: Install node monitoring.
 | 
					# Optional: Install node monitoring.
 | 
				
			||||||
ENABLE_NODE_MONITORING=true
 | 
					ENABLE_NODE_MONITORING=true
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,4 +37,5 @@ import (
 | 
				
			|||||||
	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle"
 | 
						_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle"
 | 
				
			||||||
	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota"
 | 
						_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota"
 | 
				
			||||||
	_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
 | 
						_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
 | 
				
			||||||
 | 
						_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ KUBE_SERVICE_ADDRESSES="--portal_net={{ kube_service_addresses }}"
 | 
				
			|||||||
KUBE_ETCD_SERVERS="--etcd_servers=http://{{ groups['etcd'][0] }}:2379"
 | 
					KUBE_ETCD_SERVERS="--etcd_servers=http://{{ groups['etcd'][0] }}:2379"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# default admission control policies
 | 
					# default admission control policies
 | 
				
			||||||
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota"
 | 
					KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add your own!
 | 
					# Add your own!
 | 
				
			||||||
KUBE_API_ARGS=""
 | 
					KUBE_API_ARGS=""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -140,7 +140,7 @@ if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
 | 
				
			|||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Admission Controllers to invoke prior to persisting objects in cluster
 | 
					# Admission Controllers to invoke prior to persisting objects in cluster
 | 
				
			||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
 | 
					ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
 | 
				
			||||||
 | 
					
 | 
				
			||||||
APISERVER_LOG=/tmp/kube-apiserver.log
 | 
					APISERVER_LOG=/tmp/kube-apiserver.log
 | 
				
			||||||
sudo -E "${GO_OUT}/kube-apiserver" \
 | 
					sudo -E "${GO_OUT}/kube-apiserver" \
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										373
									
								
								plugin/pkg/admission/serviceaccount/admission.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								plugin/pkg/admission/serviceaccount/admission.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,373 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2014 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 serviceaccount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"math/rand"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DefaultServiceAccountName is the name of the default service account to set on pods which do not specify a service account
 | 
				
			||||||
 | 
					const DefaultServiceAccountName = "default"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DefaultAPITokenMountPath is the path that ServiceAccountToken secrets are automounted to.
 | 
				
			||||||
 | 
					// The token file would then be accessible at /var/run/secrets/kubernetes.io/serviceaccount
 | 
				
			||||||
 | 
					const DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						admission.RegisterPlugin("ServiceAccount", func(client client.Interface, config io.Reader) (admission.Interface, error) {
 | 
				
			||||||
 | 
							serviceAccountAdmission := NewServiceAccount(client)
 | 
				
			||||||
 | 
							serviceAccountAdmission.Run()
 | 
				
			||||||
 | 
							return serviceAccountAdmission, nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var _ = admission.Interface(&serviceAccount{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type serviceAccount struct {
 | 
				
			||||||
 | 
						// LimitSecretReferences rejects pods that reference secrets their service accounts do not reference
 | 
				
			||||||
 | 
						LimitSecretReferences bool
 | 
				
			||||||
 | 
						// MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account
 | 
				
			||||||
 | 
						MountServiceAccountToken bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client client.Interface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						serviceAccounts cache.Indexer
 | 
				
			||||||
 | 
						secrets         cache.Indexer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stopChan                 chan struct{}
 | 
				
			||||||
 | 
						serviceAccountsReflector *cache.Reflector
 | 
				
			||||||
 | 
						secretsReflector         *cache.Reflector
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewServiceAccount returns an admission.Interface implementation which limits admission of Pod CREATE requests based on the pod's ServiceAccount:
 | 
				
			||||||
 | 
					// 1. If the pod does not specify a ServiceAccount, it sets the pod's ServiceAccount to "default"
 | 
				
			||||||
 | 
					// 2. It ensures the ServiceAccount referenced by the pod exists
 | 
				
			||||||
 | 
					// 3. If LimitSecretReferences is true, it rejects the pod if the pod references Secret objects which the pod's ServiceAccount does not reference
 | 
				
			||||||
 | 
					// 4. If MountServiceAccountToken is true, it adds a VolumeMount with the pod's ServiceAccount's api token secret to containers
 | 
				
			||||||
 | 
					func NewServiceAccount(cl client.Interface) *serviceAccount {
 | 
				
			||||||
 | 
						serviceAccountsIndexer, serviceAccountsReflector := cache.NewNamespaceKeyedIndexerAndReflector(
 | 
				
			||||||
 | 
							&cache.ListWatch{
 | 
				
			||||||
 | 
								ListFunc: func() (runtime.Object, error) {
 | 
				
			||||||
 | 
									return cl.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything())
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								WatchFunc: func(resourceVersion string) (watch.Interface, error) {
 | 
				
			||||||
 | 
									return cl.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), resourceVersion)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&api.ServiceAccount{},
 | 
				
			||||||
 | 
							0,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
 | 
				
			||||||
 | 
						secretsIndexer, secretsReflector := cache.NewNamespaceKeyedIndexerAndReflector(
 | 
				
			||||||
 | 
							&cache.ListWatch{
 | 
				
			||||||
 | 
								ListFunc: func() (runtime.Object, error) {
 | 
				
			||||||
 | 
									return cl.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								WatchFunc: func(resourceVersion string) (watch.Interface, error) {
 | 
				
			||||||
 | 
									return cl.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, resourceVersion)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&api.Secret{},
 | 
				
			||||||
 | 
							0,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &serviceAccount{
 | 
				
			||||||
 | 
							// TODO: enable this once we've swept secret usage to account for adding secret references to service accounts
 | 
				
			||||||
 | 
							LimitSecretReferences: false,
 | 
				
			||||||
 | 
							// Auto mount service account API token secrets
 | 
				
			||||||
 | 
							MountServiceAccountToken: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							client:                   cl,
 | 
				
			||||||
 | 
							serviceAccounts:          serviceAccountsIndexer,
 | 
				
			||||||
 | 
							serviceAccountsReflector: serviceAccountsReflector,
 | 
				
			||||||
 | 
							secrets:                  secretsIndexer,
 | 
				
			||||||
 | 
							secretsReflector:         secretsReflector,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *serviceAccount) Run() {
 | 
				
			||||||
 | 
						if s.stopChan == nil {
 | 
				
			||||||
 | 
							s.stopChan = make(chan struct{})
 | 
				
			||||||
 | 
							s.serviceAccountsReflector.RunUntil(s.stopChan)
 | 
				
			||||||
 | 
							s.secretsReflector.RunUntil(s.stopChan)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (s *serviceAccount) Stop() {
 | 
				
			||||||
 | 
						if s.stopChan != nil {
 | 
				
			||||||
 | 
							close(s.stopChan)
 | 
				
			||||||
 | 
							s.stopChan = nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *serviceAccount) Admit(a admission.Attributes) (err error) {
 | 
				
			||||||
 | 
						// We only care about Pod CREATE operations
 | 
				
			||||||
 | 
						if a.GetOperation() != "CREATE" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if a.GetResource() != string(api.ResourcePods) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						obj := a.GetObject()
 | 
				
			||||||
 | 
						if obj == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pod, ok := obj.(*api.Pod)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't modify the spec of mirror pods.
 | 
				
			||||||
 | 
						// That makes the kubelet very angry and confused, and it immediately deletes the pod (because the spec doesn't match)
 | 
				
			||||||
 | 
						// That said, don't allow mirror pods to reference ServiceAccounts or SecretVolumeSources either
 | 
				
			||||||
 | 
						if _, isMirrorPod := pod.Annotations[kubelet.ConfigMirrorAnnotationKey]; isMirrorPod {
 | 
				
			||||||
 | 
							if len(pod.Spec.ServiceAccount) != 0 {
 | 
				
			||||||
 | 
								return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference service accounts"))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, volume := range pod.Spec.Volumes {
 | 
				
			||||||
 | 
								if volume.VolumeSource.Secret != nil {
 | 
				
			||||||
 | 
									return admission.NewForbidden(a, fmt.Errorf("A mirror pod may not reference secrets"))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set the default service account if needed
 | 
				
			||||||
 | 
						if len(pod.Spec.ServiceAccount) == 0 {
 | 
				
			||||||
 | 
							pod.Spec.ServiceAccount = DefaultServiceAccountName
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure the referenced service account exists
 | 
				
			||||||
 | 
						serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return admission.NewForbidden(a, fmt.Errorf("Error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if serviceAccount == nil {
 | 
				
			||||||
 | 
							return admission.NewForbidden(a, fmt.Errorf("Missing service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccount, err))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if s.LimitSecretReferences {
 | 
				
			||||||
 | 
							if err := s.limitSecretReferences(serviceAccount, pod); err != nil {
 | 
				
			||||||
 | 
								return admission.NewForbidden(a, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if s.MountServiceAccountToken {
 | 
				
			||||||
 | 
							if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil {
 | 
				
			||||||
 | 
								return admission.NewForbidden(a, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getServiceAccount returns the ServiceAccount for the given namespace and name if it exists
 | 
				
			||||||
 | 
					func (s *serviceAccount) getServiceAccount(namespace string, name string) (*api.ServiceAccount, error) {
 | 
				
			||||||
 | 
						key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}}
 | 
				
			||||||
 | 
						index, err := s.serviceAccounts.Index("namespace", key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, obj := range index {
 | 
				
			||||||
 | 
							serviceAccount := obj.(*api.ServiceAccount)
 | 
				
			||||||
 | 
							if serviceAccount.Name == name {
 | 
				
			||||||
 | 
								return serviceAccount, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Could not find in cache, attempt to look up directly
 | 
				
			||||||
 | 
						numAttempts := 1
 | 
				
			||||||
 | 
						if name == DefaultServiceAccountName {
 | 
				
			||||||
 | 
							// If this is the default serviceaccount, attempt more times, since it should be auto-created by the controller
 | 
				
			||||||
 | 
							numAttempts = 10
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						retryInterval := time.Duration(rand.Int63n(100)+int64(100)) * time.Millisecond
 | 
				
			||||||
 | 
						for i := 0; i < numAttempts; i++ {
 | 
				
			||||||
 | 
							if i != 0 {
 | 
				
			||||||
 | 
								time.Sleep(retryInterval)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							serviceAccount, err := s.client.ServiceAccounts(namespace).Get(name)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								return serviceAccount, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !errors.IsNotFound(err) {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getReferencedServiceAccountToken returns the name of the first referenced secret which is a ServiceAccountToken for the service account
 | 
				
			||||||
 | 
					func (s *serviceAccount) getReferencedServiceAccountToken(serviceAccount *api.ServiceAccount) (string, error) {
 | 
				
			||||||
 | 
						if len(serviceAccount.Secrets) == 0 {
 | 
				
			||||||
 | 
							return "", nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokens, err := s.getServiceAccountTokens(serviceAccount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						references := util.NewStringSet()
 | 
				
			||||||
 | 
						for _, secret := range serviceAccount.Secrets {
 | 
				
			||||||
 | 
							references.Insert(secret.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, token := range tokens {
 | 
				
			||||||
 | 
							if references.Has(token.Name) {
 | 
				
			||||||
 | 
								return token.Name, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getServiceAccountTokens returns all ServiceAccountToken secrets for the given ServiceAccount
 | 
				
			||||||
 | 
					func (s *serviceAccount) getServiceAccountTokens(serviceAccount *api.ServiceAccount) ([]*api.Secret, error) {
 | 
				
			||||||
 | 
						key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}}
 | 
				
			||||||
 | 
						index, err := s.secrets.Index("namespace", key)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokens := []*api.Secret{}
 | 
				
			||||||
 | 
						for _, obj := range index {
 | 
				
			||||||
 | 
							token := obj.(*api.Secret)
 | 
				
			||||||
 | 
							if token.Type != api.SecretTypeServiceAccountToken {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							name := token.Annotations[api.ServiceAccountNameKey]
 | 
				
			||||||
 | 
							uid := token.Annotations[api.ServiceAccountUIDKey]
 | 
				
			||||||
 | 
							if name != serviceAccount.Name {
 | 
				
			||||||
 | 
								// Name must match
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(uid) > 0 && uid != string(serviceAccount.UID) {
 | 
				
			||||||
 | 
								// If UID is set, it must match
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tokens = append(tokens, token)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tokens, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *serviceAccount) limitSecretReferences(serviceAccount *api.ServiceAccount, pod *api.Pod) error {
 | 
				
			||||||
 | 
						// Ensure all secrets the pod references are allowed by the service account
 | 
				
			||||||
 | 
						referencedSecrets := util.NewStringSet()
 | 
				
			||||||
 | 
						for _, s := range serviceAccount.Secrets {
 | 
				
			||||||
 | 
							referencedSecrets.Insert(s.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, volume := range pod.Spec.Volumes {
 | 
				
			||||||
 | 
							source := volume.VolumeSource
 | 
				
			||||||
 | 
							if source.Secret == nil {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							secretName := source.Secret.SecretName
 | 
				
			||||||
 | 
							if !referencedSecrets.Has(secretName) {
 | 
				
			||||||
 | 
								return fmt.Errorf("Volume with secret.secretName=\"%s\" is not allowed because service account %s does not reference that secret", secretName, serviceAccount.Name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *serviceAccount) mountServiceAccountToken(serviceAccount *api.ServiceAccount, pod *api.Pod) error {
 | 
				
			||||||
 | 
						// Find the name of a referenced ServiceAccountToken secret we can mount
 | 
				
			||||||
 | 
						serviceAccountToken, err := s.getReferencedServiceAccountToken(serviceAccount)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Errorf("Error looking up service account token for %s/%s: %v", serviceAccount.Namespace, serviceAccount.Name, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(serviceAccountToken) == 0 {
 | 
				
			||||||
 | 
							// We don't have an API token to mount, so return
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Find the volume and volume name for the ServiceAccountTokenSecret if it already exists
 | 
				
			||||||
 | 
						tokenVolumeName := ""
 | 
				
			||||||
 | 
						hasTokenVolume := false
 | 
				
			||||||
 | 
						allVolumeNames := util.NewStringSet()
 | 
				
			||||||
 | 
						for _, volume := range pod.Spec.Volumes {
 | 
				
			||||||
 | 
							allVolumeNames.Insert(volume.Name)
 | 
				
			||||||
 | 
							if volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken {
 | 
				
			||||||
 | 
								tokenVolumeName = volume.Name
 | 
				
			||||||
 | 
								hasTokenVolume = true
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Determine a volume name for the ServiceAccountTokenSecret in case we need it
 | 
				
			||||||
 | 
						if len(tokenVolumeName) == 0 {
 | 
				
			||||||
 | 
							// Try naming the volume the same as the serviceAccountToken, and uniquify if needed
 | 
				
			||||||
 | 
							tokenVolumeName = serviceAccountToken
 | 
				
			||||||
 | 
							if allVolumeNames.Has(tokenVolumeName) {
 | 
				
			||||||
 | 
								tokenVolumeName = api.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-", serviceAccountToken))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the prototypical VolumeMount
 | 
				
			||||||
 | 
						volumeMount := api.VolumeMount{
 | 
				
			||||||
 | 
							Name:      tokenVolumeName,
 | 
				
			||||||
 | 
							ReadOnly:  true,
 | 
				
			||||||
 | 
							MountPath: DefaultAPITokenMountPath,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure every container mounts the APISecret volume
 | 
				
			||||||
 | 
						needsTokenVolume := false
 | 
				
			||||||
 | 
						for i, container := range pod.Spec.Containers {
 | 
				
			||||||
 | 
							existingContainerMount := false
 | 
				
			||||||
 | 
							for _, volumeMount := range container.VolumeMounts {
 | 
				
			||||||
 | 
								// Existing mounts at the default mount path prevent mounting of the API token
 | 
				
			||||||
 | 
								if volumeMount.MountPath == DefaultAPITokenMountPath {
 | 
				
			||||||
 | 
									existingContainerMount = true
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !existingContainerMount {
 | 
				
			||||||
 | 
								pod.Spec.Containers[i].VolumeMounts = append(pod.Spec.Containers[i].VolumeMounts, volumeMount)
 | 
				
			||||||
 | 
								needsTokenVolume = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the volume if a container needs it
 | 
				
			||||||
 | 
						if !hasTokenVolume && needsTokenVolume {
 | 
				
			||||||
 | 
							volume := api.Volume{
 | 
				
			||||||
 | 
								Name: tokenVolumeName,
 | 
				
			||||||
 | 
								VolumeSource: api.VolumeSource{
 | 
				
			||||||
 | 
									Secret: &api.SecretVolumeSource{
 | 
				
			||||||
 | 
										SecretName: serviceAccountToken,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										399
									
								
								plugin/pkg/admission/serviceaccount/admission_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										399
									
								
								plugin/pkg/admission/serviceaccount/admission_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,399 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2014 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 serviceaccount
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/admission"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet"
 | 
				
			||||||
 | 
						"github.com/GoogleCloudPlatform/kubernetes/pkg/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIgnoresNonCreate(t *testing.T) {
 | 
				
			||||||
 | 
						pod := &api.Pod{}
 | 
				
			||||||
 | 
						for _, op := range []string{"UPDATE", "DELETE", "CUSTOM"} {
 | 
				
			||||||
 | 
							attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), op)
 | 
				
			||||||
 | 
							err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Errorf("Expected %s operation allowed, got err: %v", op, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIgnoresNonPodResource(t *testing.T) {
 | 
				
			||||||
 | 
						pod := &api.Pod{}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", "myns", "CustomResource", "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected non-pod resource allowed, got err: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIgnoresNilObject(t *testing.T) {
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(nil, "Pod", "myns", string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected nil object allowed allowed, got err: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIgnoresNonPodObject(t *testing.T) {
 | 
				
			||||||
 | 
						obj := &api.Namespace{}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(obj, "Pod", "myns", string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected non pod object allowed, got err: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestIgnoresMirrorPod(t *testing.T) {
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Annotations: map[string]string{
 | 
				
			||||||
 | 
									kubelet.ConfigMirrorAnnotationKey: "true",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Volumes: []api.Volume{
 | 
				
			||||||
 | 
									{VolumeSource: api.VolumeSource{}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected mirror pod without service account or secrets allowed, got err: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRejectsMirrorPodWithServiceAccount(t *testing.T) {
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Annotations: map[string]string{
 | 
				
			||||||
 | 
									kubelet.ConfigMirrorAnnotationKey: "true",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								ServiceAccount: "default",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected a mirror pod to be prevented from referencing a service account")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRejectsMirrorPodWithSecretVolumes(t *testing.T) {
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Annotations: map[string]string{
 | 
				
			||||||
 | 
									kubelet.ConfigMirrorAnnotationKey: "true",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Volumes: []api.Volume{
 | 
				
			||||||
 | 
									{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{}}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", "myns", string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := NewServiceAccount(nil).Admit(attrs)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected a mirror pod to be prevented from referencing a secret volume")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(nil)
 | 
				
			||||||
 | 
						admit.MountServiceAccountToken = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the default service account for the ns into the cache
 | 
				
			||||||
 | 
						admit.serviceAccounts.Add(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      DefaultServiceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pod.Spec.ServiceAccount != DefaultServiceAccountName {
 | 
				
			||||||
 | 
							t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestFetchesUncachedServiceAccount(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Build a test client that the admission plugin can use to look up the service account missing from its cache
 | 
				
			||||||
 | 
						client := testclient.NewSimpleFake(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      DefaultServiceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pod.Spec.ServiceAccount != DefaultServiceAccountName {
 | 
				
			||||||
 | 
							t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDeniesInvalidServiceAccount(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Build a test client that the admission plugin can use to look up the service account missing from its cache
 | 
				
			||||||
 | 
						client := testclient.NewSimpleFake()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error for missing service account, got none")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAutomountsAPIToken(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
						tokenName := "token-name"
 | 
				
			||||||
 | 
						serviceAccountName := DefaultServiceAccountName
 | 
				
			||||||
 | 
						serviceAccountUID := "12345"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expectedVolume := api.Volume{
 | 
				
			||||||
 | 
							Name: tokenName,
 | 
				
			||||||
 | 
							VolumeSource: api.VolumeSource{
 | 
				
			||||||
 | 
								Secret: &api.SecretVolumeSource{SecretName: tokenName},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						expectedVolumeMount := api.VolumeMount{
 | 
				
			||||||
 | 
							Name:      tokenName,
 | 
				
			||||||
 | 
							ReadOnly:  true,
 | 
				
			||||||
 | 
							MountPath: DefaultAPITokenMountPath,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(nil)
 | 
				
			||||||
 | 
						admit.MountServiceAccountToken = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the default service account for the ns with a token into the cache
 | 
				
			||||||
 | 
						admit.serviceAccounts.Add(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      serviceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
								UID:       types.UID(serviceAccountUID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Secrets: []api.ObjectReference{
 | 
				
			||||||
 | 
								{Name: tokenName},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						// Add a token for the service account into the cache
 | 
				
			||||||
 | 
						admit.secrets.Add(&api.Secret{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      tokenName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
								Annotations: map[string]string{
 | 
				
			||||||
 | 
									api.ServiceAccountNameKey: serviceAccountName,
 | 
				
			||||||
 | 
									api.ServiceAccountUIDKey:  serviceAccountUID,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Type: api.SecretTypeServiceAccountToken,
 | 
				
			||||||
 | 
							Data: map[string][]byte{
 | 
				
			||||||
 | 
								api.ServiceAccountTokenKey: []byte("token-data"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Containers: []api.Container{
 | 
				
			||||||
 | 
									{},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pod.Spec.ServiceAccount != DefaultServiceAccountName {
 | 
				
			||||||
 | 
							t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(pod.Spec.Volumes) != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 1 volume, got %d", len(pod.Spec.Volumes))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !reflect.DeepEqual(expectedVolume, pod.Spec.Volumes[0]) {
 | 
				
			||||||
 | 
							t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolume, pod.Spec.Volumes[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(pod.Spec.Containers[0].VolumeMounts) != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) {
 | 
				
			||||||
 | 
							t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRespectsExistingMount(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
						tokenName := "token-name"
 | 
				
			||||||
 | 
						serviceAccountName := DefaultServiceAccountName
 | 
				
			||||||
 | 
						serviceAccountUID := "12345"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expectedVolumeMount := api.VolumeMount{
 | 
				
			||||||
 | 
							Name:      "my-custom-mount",
 | 
				
			||||||
 | 
							ReadOnly:  false,
 | 
				
			||||||
 | 
							MountPath: DefaultAPITokenMountPath,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(nil)
 | 
				
			||||||
 | 
						admit.MountServiceAccountToken = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the default service account for the ns with a token into the cache
 | 
				
			||||||
 | 
						admit.serviceAccounts.Add(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      serviceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
								UID:       types.UID(serviceAccountUID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Secrets: []api.ObjectReference{
 | 
				
			||||||
 | 
								{Name: tokenName},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						// Add a token for the service account into the cache
 | 
				
			||||||
 | 
						admit.secrets.Add(&api.Secret{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      tokenName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
								Annotations: map[string]string{
 | 
				
			||||||
 | 
									api.ServiceAccountNameKey: serviceAccountName,
 | 
				
			||||||
 | 
									api.ServiceAccountUIDKey:  serviceAccountUID,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Type: api.SecretTypeServiceAccountToken,
 | 
				
			||||||
 | 
							Data: map[string][]byte{
 | 
				
			||||||
 | 
								api.ServiceAccountTokenKey: []byte("token-data"),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Define a pod with a container that already mounts a volume at the API token path
 | 
				
			||||||
 | 
						// Admission should respect that
 | 
				
			||||||
 | 
						// Additionally, no volume should be created if no container is going to use it
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Containers: []api.Container{
 | 
				
			||||||
 | 
									{
 | 
				
			||||||
 | 
										VolumeMounts: []api.VolumeMount{
 | 
				
			||||||
 | 
											expectedVolumeMount,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if pod.Spec.ServiceAccount != DefaultServiceAccountName {
 | 
				
			||||||
 | 
							t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccount)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(pod.Spec.Volumes) != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 0 volumes (shouldn't create a volume for a secret we don't need), got %d", len(pod.Spec.Volumes))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(pod.Spec.Containers[0].VolumeMounts) != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.Containers[0].VolumeMounts))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0]) {
 | 
				
			||||||
 | 
							t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.Containers[0].VolumeMounts[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAllowsReferencedSecretVolumes(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(nil)
 | 
				
			||||||
 | 
						admit.LimitSecretReferences = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the default service account for the ns with a secret reference into the cache
 | 
				
			||||||
 | 
						admit.serviceAccounts.Add(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      DefaultServiceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Secrets: []api.ObjectReference{
 | 
				
			||||||
 | 
								{Name: "foo"},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Volumes: []api.Volume{
 | 
				
			||||||
 | 
									{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Unexpected error: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestRejectsUnreferencedSecretVolumes(t *testing.T) {
 | 
				
			||||||
 | 
						ns := "myns"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						admit := NewServiceAccount(nil)
 | 
				
			||||||
 | 
						admit.LimitSecretReferences = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add the default service account for the ns into the cache
 | 
				
			||||||
 | 
						admit.serviceAccounts.Add(&api.ServiceAccount{
 | 
				
			||||||
 | 
							ObjectMeta: api.ObjectMeta{
 | 
				
			||||||
 | 
								Name:      DefaultServiceAccountName,
 | 
				
			||||||
 | 
								Namespace: ns,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pod := &api.Pod{
 | 
				
			||||||
 | 
							Spec: api.PodSpec{
 | 
				
			||||||
 | 
								Volumes: []api.Volume{
 | 
				
			||||||
 | 
									{VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "foo"}}},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						attrs := admission.NewAttributesRecord(pod, "Pod", ns, string(api.ResourcePods), "CREATE")
 | 
				
			||||||
 | 
						err := admit.Admit(attrs)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected rejection for using a secret the service account does not reference")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										19
									
								
								plugin/pkg/admission/serviceaccount/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								plugin/pkg/admission/serviceaccount/doc.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2014 The Kubernetes Authors All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// serviceaccount enforces all pods having an associated serviceaccount,
 | 
				
			||||||
 | 
					// and all containers mounting the API token for that serviceaccount at a known location
 | 
				
			||||||
 | 
					package serviceaccount
 | 
				
			||||||
		Reference in New Issue
	
	Block a user