Merge pull request #101992 from zshihang/bound

BoundServiceAccountTokenVolume ga
This commit is contained in:
Kubernetes Prow Robot 2021-05-25 07:52:22 -07:00 committed by GitHub
commit 548f16dbbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 797 deletions

View File

@ -179,6 +179,7 @@ const (
// owner: @mikedanese // owner: @mikedanese
// alpha: v1.13 // alpha: v1.13
// beta: v1.21 // beta: v1.21
// ga: v1.22
// //
// Migrate ServiceAccount volumes to use a projected volume consisting of a // Migrate ServiceAccount volumes to use a projected volume consisting of a
// ServiceAccountTokenVolumeProjection. This feature adds new required flags // ServiceAccountTokenVolumeProjection. This feature adds new required flags
@ -764,7 +765,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
StorageObjectInUseProtection: {Default: true, PreRelease: featuregate.GA}, StorageObjectInUseProtection: {Default: true, PreRelease: featuregate.GA},
SupportPodPidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.23 SupportPodPidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.23
SupportNodePidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.23 SupportNodePidsLimit: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.23
BoundServiceAccountTokenVolume: {Default: true, PreRelease: featuregate.Beta}, BoundServiceAccountTokenVolume: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.23
ServiceAccountIssuerDiscovery: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22 ServiceAccountIssuerDiscovery: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22
CSIMigration: {Default: true, PreRelease: featuregate.Beta}, CSIMigration: {Default: true, PreRelease: featuregate.Beta},
CSIMigrationGCE: {Default: false, PreRelease: featuregate.Beta}, // Off by default (requires GCE PD CSI Driver) CSIMigrationGCE: {Default: false, PreRelease: featuregate.Beta}, // Off by default (requires GCE PD CSI Driver)

View File

@ -1481,7 +1481,7 @@ func TestValidateProjectedVolume(t *testing.T) {
psp.Spec.Volumes = test.allowedFSTypes psp.Spec.Volumes = test.allowedFSTypes
errs := provider.ValidatePod(pod) errs := provider.ValidatePod(pod)
if test.wantAllow { if test.wantAllow {
assert.Empty(t, errs, "projected volumes are allowed if secret volumes is allowed and BoundServiceAccountTokenVolume is enabled") assert.Empty(t, errs, "projected volumes are allowed")
} else { } else {
assert.Contains(t, errs.ToAggregate().Error(), fmt.Sprintf("projected volumes are not allowed to be used"), "did not find the expected error") assert.Contains(t, errs.ToAggregate().Error(), fmt.Sprintf("projected volumes are not allowed to be used"), "did not find the expected error")
} }

View File

@ -28,20 +28,15 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer" genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1" corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/component-base/featuregate"
podutil "k8s.io/kubernetes/pkg/api/pod" podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/serviceaccount" "k8s.io/kubernetes/pkg/serviceaccount"
) )
@ -80,24 +75,18 @@ type Plugin struct {
// LimitSecretReferences rejects pods that reference secrets their service accounts do not reference // LimitSecretReferences rejects pods that reference secrets their service accounts do not reference
LimitSecretReferences bool LimitSecretReferences bool
// RequireAPIToken determines whether pod creation attempts are rejected if no API token exists for the pod's service account
RequireAPIToken bool
// MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account // MountServiceAccountToken creates Volume and VolumeMounts for the first referenced ServiceAccountToken for the pod's service account
MountServiceAccountToken bool MountServiceAccountToken bool
client kubernetes.Interface client kubernetes.Interface
serviceAccountLister corev1listers.ServiceAccountLister serviceAccountLister corev1listers.ServiceAccountLister
secretLister corev1listers.SecretLister
generateName func(string) string generateName func(string) string
boundServiceAccountTokenVolume bool
} }
var _ admission.MutationInterface = &Plugin{} var _ admission.MutationInterface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{} var _ admission.ValidationInterface = &Plugin{}
var _ genericadmissioninitializer.WantsFeatures = &Plugin{}
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Plugin{}) var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Plugin{})
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Plugin{}) var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Plugin{})
@ -114,18 +103,11 @@ func NewServiceAccount() *Plugin {
LimitSecretReferences: false, LimitSecretReferences: false,
// Auto mount service account API token secrets // Auto mount service account API token secrets
MountServiceAccountToken: true, MountServiceAccountToken: true,
// Reject pod creation until a service account token is available
RequireAPIToken: true,
generateName: names.SimpleNameGenerator.GenerateName, generateName: names.SimpleNameGenerator.GenerateName,
} }
} }
// InspectFeatureGates allows setting bools without taking a dep on a global variable
func (s *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
s.boundServiceAccountTokenVolume = featureGates.Enabled(features.BoundServiceAccountTokenVolume)
}
// SetExternalKubeClientSet sets the client for the plugin // SetExternalKubeClientSet sets the client for the plugin
func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) { func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) {
s.client = cl s.client = cl
@ -135,12 +117,8 @@ func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) {
func (s *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) { func (s *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
serviceAccountInformer := f.Core().V1().ServiceAccounts() serviceAccountInformer := f.Core().V1().ServiceAccounts()
s.serviceAccountLister = serviceAccountInformer.Lister() s.serviceAccountLister = serviceAccountInformer.Lister()
secretInformer := f.Core().V1().Secrets()
s.secretLister = secretInformer.Lister()
s.SetReadyFunc(func() bool { s.SetReadyFunc(func() bool {
return serviceAccountInformer.Informer().HasSynced() && secretInformer.Informer().HasSynced() return serviceAccountInformer.Informer().HasSynced()
}) })
} }
@ -149,9 +127,6 @@ func (s *Plugin) ValidateInitialization() error {
if s.client == nil { if s.client == nil {
return fmt.Errorf("missing client") return fmt.Errorf("missing client")
} }
if s.secretLister == nil {
return fmt.Errorf("missing secretLister")
}
if s.serviceAccountLister == nil { if s.serviceAccountLister == nil {
return fmt.Errorf("missing serviceAccountLister") return fmt.Errorf("missing serviceAccountLister")
} }
@ -183,12 +158,7 @@ func (s *Plugin) Admit(ctx context.Context, a admission.Attributes, o admission.
return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err)) return admission.NewForbidden(a, fmt.Errorf("error looking up service account %s/%s: %v", a.GetNamespace(), pod.Spec.ServiceAccountName, err))
} }
if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) { if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) {
if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil { s.mountServiceAccountToken(serviceAccount, pod)
if _, ok := err.(errors.APIStatus); ok {
return err
}
return admission.NewForbidden(a, err)
}
} }
if len(pod.Spec.ImagePullSecrets) == 0 { if len(pod.Spec.ImagePullSecrets) == 0 {
pod.Spec.ImagePullSecrets = make([]api.LocalObjectReference, len(serviceAccount.ImagePullSecrets)) pod.Spec.ImagePullSecrets = make([]api.LocalObjectReference, len(serviceAccount.ImagePullSecrets))
@ -328,52 +298,6 @@ func (s *Plugin) getServiceAccount(namespace string, name string) (*corev1.Servi
return nil, errors.NewNotFound(api.Resource("serviceaccount"), name) return nil, errors.NewNotFound(api.Resource("serviceaccount"), name)
} }
// getReferencedServiceAccountToken returns the name of the first referenced secret which is a ServiceAccountToken for the service account
func (s *Plugin) getReferencedServiceAccountToken(serviceAccount *corev1.ServiceAccount) (string, error) {
if len(serviceAccount.Secrets) == 0 {
return "", nil
}
tokens, err := s.getServiceAccountTokens(serviceAccount)
if err != nil {
return "", err
}
accountTokens := sets.NewString()
for _, token := range tokens {
accountTokens.Insert(token.Name)
}
// Prefer secrets in the order they're referenced.
for _, secret := range serviceAccount.Secrets {
if accountTokens.Has(secret.Name) {
return secret.Name, nil
}
}
return "", nil
}
// getServiceAccountTokens returns all ServiceAccountToken secrets for the given ServiceAccount
func (s *Plugin) getServiceAccountTokens(serviceAccount *corev1.ServiceAccount) ([]*corev1.Secret, error) {
secrets, err := s.secretLister.Secrets(serviceAccount.Namespace).List(labels.Everything())
if err != nil {
return nil, err
}
tokens := []*corev1.Secret{}
for _, secret := range secrets {
if secret.Type != corev1.SecretTypeServiceAccountToken {
continue
}
if apiserverserviceaccount.IsServiceAccountToken(secret, serviceAccount) {
tokens = append(tokens, secret)
}
}
return tokens, nil
}
func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error { func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
// Ensure all secrets the pod references are allowed by the service account // Ensure all secrets the pod references are allowed by the service account
mountableSecrets := sets.NewString() mountableSecrets := sets.NewString()
@ -424,38 +348,14 @@ func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, po
return nil return nil
} }
func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error { func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) {
var (
// serviceAccountToken holds the name of a secret containing a legacy service account token
serviceAccountToken string
err error
)
if !s.boundServiceAccountTokenVolume {
// Find the name of a referenced ServiceAccountToken secret we can mount
serviceAccountToken, err = s.getReferencedServiceAccountToken(serviceAccount)
if err != nil {
return fmt.Errorf("Error looking up service account token for %s/%s: %v", serviceAccount.Namespace, serviceAccount.Name, err)
}
}
if len(serviceAccountToken) == 0 && !s.boundServiceAccountTokenVolume {
// We don't have an API token to mount, so return
if s.RequireAPIToken {
// If a token is required, this is considered an error
err := errors.NewServerTimeout(schema.GroupResource{Resource: "serviceaccounts"}, "create pod", 1)
err.ErrStatus.Message = fmt.Sprintf("No API token found for service account %q, retry after the token is automatically created and added to the service account", serviceAccount.Name)
return err
}
return nil
}
// Find the volume and volume name for the ServiceAccountTokenSecret if it already exists // Find the volume and volume name for the ServiceAccountTokenSecret if it already exists
tokenVolumeName := "" tokenVolumeName := ""
hasTokenVolume := false hasTokenVolume := false
allVolumeNames := sets.NewString() allVolumeNames := sets.NewString()
for _, volume := range pod.Spec.Volumes { for _, volume := range pod.Spec.Volumes {
allVolumeNames.Insert(volume.Name) allVolumeNames.Insert(volume.Name)
if (!s.boundServiceAccountTokenVolume && volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken) || if strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-") {
(s.boundServiceAccountTokenVolume && strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-")) {
tokenVolumeName = volume.Name tokenVolumeName = volume.Name
hasTokenVolume = true hasTokenVolume = true
break break
@ -464,16 +364,7 @@ func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount,
// Determine a volume name for the ServiceAccountTokenSecret in case we need it // Determine a volume name for the ServiceAccountTokenSecret in case we need it
if len(tokenVolumeName) == 0 { if len(tokenVolumeName) == 0 {
if s.boundServiceAccountTokenVolume { tokenVolumeName = s.generateName(ServiceAccountVolumeName + "-")
tokenVolumeName = s.generateName(ServiceAccountVolumeName + "-")
} else {
// Try naming the volume the same as the serviceAccountToken, and uniquify if needed
// Replace dots because volumeMountName can't contain it
tokenVolumeName = strings.Replace(serviceAccountToken, ".", "-", -1)
if allVolumeNames.Has(tokenVolumeName) {
tokenVolumeName = s.generateName(fmt.Sprintf("%s-", tokenVolumeName))
}
}
} }
// Create the prototypical VolumeMount // Create the prototypical VolumeMount
@ -516,27 +407,12 @@ func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount,
// Add the volume if a container needs it // Add the volume if a container needs it
if !hasTokenVolume && needsTokenVolume { if !hasTokenVolume && needsTokenVolume {
pod.Spec.Volumes = append(pod.Spec.Volumes, s.createVolume(tokenVolumeName, serviceAccountToken)) pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{
}
return nil
}
func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
if s.boundServiceAccountTokenVolume {
return api.Volume{
Name: tokenVolumeName, Name: tokenVolumeName,
VolumeSource: api.VolumeSource{ VolumeSource: api.VolumeSource{
Projected: TokenVolumeSource(), Projected: TokenVolumeSource(),
}, },
} })
}
return api.Volume{
Name: tokenVolumeName,
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: secretName,
},
},
} }
} }

View File

@ -24,7 +24,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff" "k8s.io/apimachinery/pkg/util/diff"
@ -32,18 +31,11 @@ import (
admissiontesting "k8s.io/apiserver/pkg/admission/testing" admissiontesting "k8s.io/apiserver/pkg/admission/testing"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
api "k8s.io/kubernetes/pkg/apis/core" api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
kubelet "k8s.io/kubernetes/pkg/kubelet/types" kubelet "k8s.io/kubernetes/pkg/kubelet/types"
) )
var (
deprecationDisabledBoundTokenVolume = false
deprecationEnabledBoundTokenVolume = true
)
func TestIgnoresNonCreate(t *testing.T) { func TestIgnoresNonCreate(t *testing.T) {
for _, op := range []admission.Operation{admission.Delete, admission.Connect} { for _, op := range []admission.Operation{admission.Delete, admission.Connect} {
handler := NewServiceAccount() handler := NewServiceAccount()
@ -165,59 +157,6 @@ func TestRejectsMirrorPodWithServiceAccountTokenVolumeProjections(t *testing.T)
} }
} }
func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) {
ns := "myns"
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: DefaultServiceAccountName,
Namespace: ns,
},
})
pod := &api.Pod{}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
}
func TestAssignsDefaultServiceAccountAndRejectsMissingAPIToken(t *testing.T) {
ns := "myns"
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: DefaultServiceAccountName,
Namespace: ns,
},
})
pod := &api.Pod{}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err == nil || !errors.IsServerTimeout(err) {
t.Errorf("Expected server timeout error for missing API token: %v", err)
}
}
func TestAssignsDefaultServiceAccountAndBoundTokenWithNoSecretTokens(t *testing.T) { func TestAssignsDefaultServiceAccountAndBoundTokenWithNoSecretTokens(t *testing.T) {
ns := "myns" ns := "myns"
@ -225,8 +164,6 @@ func TestAssignsDefaultServiceAccountAndBoundTokenWithNoSecretTokens(t *testing.
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
admit.boundServiceAccountTokenVolume = true
// Add the default service account for the ns into the cache // Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -300,7 +237,6 @@ func TestFetchesUncachedServiceAccount(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.client = client admit.client = client
admit.RequireAPIToken = false
pod := &api.Pod{} pod := &api.Pod{}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil) attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
@ -333,222 +269,181 @@ func TestDeniesInvalidServiceAccount(t *testing.T) {
} }
func TestAutomountsAPIToken(t *testing.T) { func TestAutomountsAPIToken(t *testing.T) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) {
admit := applyFeatures(NewServiceAccount()) admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.generateName = testGenerateName admit.generateName = testGenerateName
admit.MountServiceAccountToken = true admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
ns := "myns" ns := "myns"
serviceAccountName := DefaultServiceAccountName serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345" serviceAccountUID := "12345"
tokenName := "token-name" tokenName := generatedVolumeName
if admit.boundServiceAccountTokenVolume {
tokenName = generatedVolumeName
}
expectedVolume := admit.createVolume(tokenName, tokenName) expectedVolume := api.Volume{
expectedVolumeMount := api.VolumeMount{ Name: tokenName,
Name: tokenName, VolumeSource: api.VolumeSource{
ReadOnly: true, Projected: TokenVolumeSource(),
MountPath: DefaultAPITokenMountPath, },
} }
// Add the default service account for the ns with a token into the cache expectedVolumeMount := api.VolumeMount{
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ Name: tokenName,
ObjectMeta: metav1.ObjectMeta{ ReadOnly: true,
Name: serviceAccountName, MountPath: DefaultAPITokenMountPath,
Namespace: ns, }
UID: types.UID(serviceAccountUID), // Add the default service account for the ns with a token into the cache
}, informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
Secrets: []corev1.ObjectReference{ ObjectMeta: metav1.ObjectMeta{
{Name: tokenName}, Name: serviceAccountName,
}, Namespace: ns,
}) UID: types.UID(serviceAccountUID),
// Add a token for the service account into the cache },
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: tokenName,
Namespace: ns,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
corev1.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
api.ServiceAccountTokenKey: []byte("token-data"),
},
})
pod := &api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{
{},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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])
}
// testing InitContainers
pod = &api.Pod{
Spec: api.PodSpec{
InitContainers: []api.Container{
{},
},
},
}
attrs = admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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.InitContainers[0].VolumeMounts) != 1 {
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.InitContainers[0].VolumeMounts))
}
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0]) {
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0])
}
}) })
pod := &api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{
{},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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])
}
// testing InitContainers
pod = &api.Pod{
Spec: api.PodSpec{
InitContainers: []api.Container{
{},
},
},
}
attrs = admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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.InitContainers[0].VolumeMounts) != 1 {
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.InitContainers[0].VolumeMounts))
}
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0]) {
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0])
}
} }
func TestRespectsExistingMount(t *testing.T) { func TestRespectsExistingMount(t *testing.T) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) { ns := "myns"
ns := "myns" serviceAccountName := DefaultServiceAccountName
tokenName := "token-name" serviceAccountUID := "12345"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
expectedVolumeMount := api.VolumeMount{ expectedVolumeMount := api.VolumeMount{
Name: "my-custom-mount", Name: "my-custom-mount",
ReadOnly: false, ReadOnly: false,
MountPath: DefaultAPITokenMountPath, MountPath: DefaultAPITokenMountPath,
} }
admit := applyFeatures(NewServiceAccount()) admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
// Add the default service account for the ns with a token into the cache // Add the default service account for the ns with a token into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName, Name: serviceAccountName,
Namespace: ns, Namespace: ns,
UID: types.UID(serviceAccountUID), UID: types.UID(serviceAccountUID),
}, },
Secrets: []corev1.ObjectReference{
{Name: tokenName},
},
})
// Add a token for the service account into the cache
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: tokenName,
Namespace: ns,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
corev1.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
corev1.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, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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])
}
// check init containers
pod = &api.Pod{
Spec: api.PodSpec{
InitContainers: []api.Container{
{
VolumeMounts: []api.VolumeMount{
expectedVolumeMount,
},
},
},
},
}
attrs = admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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.InitContainers[0].VolumeMounts) != 1 {
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.InitContainers[0].VolumeMounts))
}
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0]) {
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0])
}
}) })
// 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, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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])
}
// check init containers
pod = &api.Pod{
Spec: api.PodSpec{
InitContainers: []api.Container{
{
VolumeMounts: []api.VolumeMount{
expectedVolumeMount,
},
},
},
},
}
attrs = admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
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.InitContainers[0].VolumeMounts) != 1 {
t.Fatalf("Expected 1 volume mount, got %d", len(pod.Spec.InitContainers[0].VolumeMounts))
}
if !reflect.DeepEqual(expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0]) {
t.Fatalf("Expected\n\t%#v\ngot\n\t%#v", expectedVolumeMount, pod.Spec.InitContainers[0].VolumeMounts[0])
}
} }
func TestAllowsReferencedSecret(t *testing.T) { func TestAllowsReferencedSecret(t *testing.T) {
@ -558,7 +453,6 @@ func TestAllowsReferencedSecret(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache // Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -639,7 +533,6 @@ func TestRejectsUnreferencedSecretVolumes(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache // Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -717,7 +610,6 @@ func TestAllowUnreferencedSecretVolumesForPermissiveSAs(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = false admit.LimitSecretReferences = false
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache // Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -749,7 +641,6 @@ func TestAllowsReferencedImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache // Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -781,7 +672,6 @@ func TestRejectsUnreferencedImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache // Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -810,7 +700,6 @@ func TestDoNotAddImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache // Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{ informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -847,7 +736,6 @@ func TestAddImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc()) informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory) admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true admit.LimitSecretReferences = true
admit.RequireAPIToken = false
sa := &corev1.ServiceAccount{ sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -877,348 +765,8 @@ func TestAddImagePullSecrets(t *testing.T) {
} }
} }
func TestMultipleReferencedSecrets(t *testing.T) {
var (
ns = "myns"
serviceAccountName = "mysa"
serviceAccountUID = "mysauid"
token1 = "token1"
token2 = "token2"
)
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName,
UID: types.UID(serviceAccountUID),
Namespace: ns,
},
Secrets: []corev1.ObjectReference{
{Name: token1},
{Name: token2},
},
}
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(sa)
// Add two tokens for the service account into the cache.
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: token2,
Namespace: ns,
Annotations: map[string]string{
api.ServiceAccountNameKey: serviceAccountName,
api.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
api.ServiceAccountTokenKey: []byte("token-data"),
},
})
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: token1,
Namespace: ns,
Annotations: map[string]string{
api.ServiceAccountNameKey: serviceAccountName,
api.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
api.ServiceAccountTokenKey: []byte("token-data"),
},
})
pod := &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: serviceAccountName,
Containers: []api.Container{
{Name: "container-1"},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Fatal(err)
}
if n := len(pod.Spec.Volumes); n != 1 {
t.Fatalf("expected 1 volume mount, got %d", n)
}
if name := pod.Spec.Volumes[0].Name; name != token1 {
t.Errorf("expected first referenced secret to be mounted, got %q", name)
}
}
func newSecret(secretType corev1.SecretType, namespace, name, serviceAccountName, serviceAccountUID string) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
corev1.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: secretType,
}
}
func TestGetServiceAccountTokens(t *testing.T) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) {
admit := applyFeatures(NewServiceAccount())
indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
admit.secretLister = corev1listers.NewSecretLister(indexer)
ns := "namespace"
serviceAccountUID := "12345"
sa := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: DefaultServiceAccountName,
Namespace: ns,
UID: types.UID(serviceAccountUID),
},
}
nonSATokenSecret := newSecret(corev1.SecretTypeDockercfg, ns, "nonSATokenSecret", DefaultServiceAccountName, serviceAccountUID)
indexer.Add(nonSATokenSecret)
differentSAToken := newSecret(corev1.SecretTypeServiceAccountToken, ns, "differentSAToken", "someOtherSA", "someOtherUID")
indexer.Add(differentSAToken)
matchingSAToken := newSecret(corev1.SecretTypeServiceAccountToken, ns, "matchingSAToken", DefaultServiceAccountName, serviceAccountUID)
indexer.Add(matchingSAToken)
tokens, err := admit.getServiceAccountTokens(sa)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(tokens) != 1 {
names := make([]string, 0, len(tokens))
for _, token := range tokens {
names = append(names, token.Name)
}
t.Fatalf("expected only 1 token, got %v", names)
}
if e, a := matchingSAToken.Name, tokens[0].Name; e != a {
t.Errorf("expected token %s, got %s", e, a)
}
})
}
func TestAutomountIsBackwardsCompatible(t *testing.T) {
ns := "myns"
tokenName := "token-name"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
defaultTokenName := "default-token-abc123"
expectedVolume := api.Volume{
Name: defaultTokenName,
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: defaultTokenName,
},
},
}
expectedVolumeMount := api.VolumeMount{
Name: defaultTokenName,
ReadOnly: true,
MountPath: DefaultAPITokenMountPath,
}
admit := NewServiceAccount()
admit.generateName = testGenerateName
admit.boundServiceAccountTokenVolume = deprecationEnabledBoundTokenVolume
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
// Add the default service account for the ns with a token into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName,
Namespace: ns,
UID: types.UID(serviceAccountUID),
},
Secrets: []corev1.ObjectReference{
{Name: tokenName},
},
})
// Add a token for the service account into the cache
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: tokenName,
Namespace: ns,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
corev1.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
api.ServiceAccountTokenKey: []byte("token-data"),
},
})
pod := &api.Pod{
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "c-1",
VolumeMounts: []api.VolumeMount{
{
Name: defaultTokenName,
MountPath: DefaultAPITokenMountPath,
ReadOnly: true,
},
},
},
},
Volumes: []api.Volume{
{
Name: defaultTokenName,
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: defaultTokenName,
},
},
},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if pod.Spec.ServiceAccountName != DefaultServiceAccountName {
t.Errorf("Expected service account %s assigned, got %s", DefaultServiceAccountName, pod.Spec.ServiceAccountName)
}
_ = expectedVolume
_ = expectedVolumeMount
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 TestServiceAccountNameWithDotMount(t *testing.T) {
ns := "myns"
tokenName := "token.name-123"
serviceAccountName := "token.name"
serviceAccountUID := "12345"
expectedVolume := api.Volume{
Name: "token-name-123",
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: "token.name-123",
},
},
}
expectedVolumeMount := api.VolumeMount{
Name: "token-name-123",
ReadOnly: true,
MountPath: DefaultAPITokenMountPath,
}
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName,
Namespace: ns,
UID: types.UID(serviceAccountUID),
},
Secrets: []corev1.ObjectReference{
{Name: tokenName},
},
})
informerFactory.Core().V1().Secrets().Informer().GetStore().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: tokenName,
Namespace: ns,
Annotations: map[string]string{
corev1.ServiceAccountNameKey: serviceAccountName,
corev1.ServiceAccountUIDKey: serviceAccountUID,
},
},
Type: corev1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
api.ServiceAccountTokenKey: []byte("token-data"),
},
})
pod := &api.Pod{
Spec: api.PodSpec{
ServiceAccountName: serviceAccountName,
Containers: []api.Container{
{Name: "container-1"},
},
},
}
attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), ns, "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, &metav1.CreateOptions{}, false, nil)
if err := admissiontesting.WithReinvocationTesting(t, admit).Admit(context.TODO(), attrs, nil); err != nil {
t.Fatal(err)
}
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 testGenerateName(n string) string { func testGenerateName(n string) string {
return n + "abc123" return n + "abc123"
} }
var generatedVolumeName = testGenerateName(ServiceAccountVolumeName + "-") var generatedVolumeName = testGenerateName(ServiceAccountVolumeName + "-")
func testBoundServiceAccountTokenVolumePhases(t *testing.T, f func(*testing.T, func(*Plugin) *Plugin)) {
t.Run("BoundServiceAccountTokenVolume disabled", func(t *testing.T) {
f(t, func(s *Plugin) *Plugin {
s.boundServiceAccountTokenVolume = deprecationDisabledBoundTokenVolume
return s
})
})
t.Run("BoundServiceAccountTokenVolume enabled", func(t *testing.T) {
f(t, func(s *Plugin) *Plugin {
s.boundServiceAccountTokenVolume = deprecationEnabledBoundTokenVolume
return s
})
})
}

View File

@ -46,8 +46,7 @@ var _ = SIGDescribe("ServiceAccount admission controller migration [Feature:Boun
} }
testSuite.TestCases = append(testSuite.TestCases, serviceaccountAdmissionControllerMigrationTest) testSuite.TestCases = append(testSuite.TestCases, serviceaccountAdmissionControllerMigrationTest)
extraEnvs := []string{"KUBE_FEATURE_GATES=BoundServiceAccountTokenVolume=true"} upgradeFunc := common.ControlPlaneUpgradeFunc(f, upgCtx, serviceaccountAdmissionControllerMigrationTest, nil)
upgradeFunc := common.ControlPlaneUpgradeFunc(f, upgCtx, serviceaccountAdmissionControllerMigrationTest, extraEnvs)
upgrades.RunUpgradeSuite(upgCtx, upgradeTests, testFrameworks, testSuite, upgrades.MasterUpgrade, upgradeFunc) upgrades.RunUpgradeSuite(upgCtx, upgradeTests, testFrameworks, testSuite, upgrades.MasterUpgrade, upgradeFunc)
}) })
}) })

View File

@ -31,7 +31,6 @@ import (
"time" "time"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -211,70 +210,30 @@ func TestServiceAccountTokenAutoMount(t *testing.T) {
t.Fatalf("could not create namespace: %v", err) t.Fatalf("could not create namespace: %v", err)
} }
// Get default token
defaultTokenName, _, err := getReferencedServiceAccountToken(c, ns, serviceaccountadmission.DefaultServiceAccountName, true)
if err != nil {
t.Fatal(err)
}
// Pod to create // Pod to create
protoPod := v1.Pod{ protoPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "protopod"}, ObjectMeta: metav1.ObjectMeta{Name: "protopod"},
Spec: v1.PodSpec{ Spec: v1.PodSpec{
Containers: []v1.Container{ Containers: []v1.Container{
{ {
Name: "container-1", Name: "container",
Image: "container-1-image", Image: "container-image",
},
{
Name: "container-2",
Image: "container-2-image",
VolumeMounts: []v1.VolumeMount{
{Name: "empty-dir", MountPath: serviceaccountadmission.DefaultAPITokenMountPath},
},
},
},
Volumes: []v1.Volume{
{
Name: "empty-dir",
VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}},
}, },
}, },
}, },
} }
// Pod we expect to get created
defaultMode := int32(0644)
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
expectedVolumes := append(protoPod.Spec.Volumes, v1.Volume{
Name: defaultTokenName,
VolumeSource: v1.VolumeSource{
Secret: &v1.SecretVolumeSource{
SecretName: defaultTokenName,
DefaultMode: &defaultMode,
},
},
})
expectedContainer1VolumeMounts := []v1.VolumeMount{
{Name: defaultTokenName, MountPath: serviceaccountadmission.DefaultAPITokenMountPath, ReadOnly: true},
}
expectedContainer2VolumeMounts := protoPod.Spec.Containers[1].VolumeMounts
createdPod, err := c.CoreV1().Pods(ns).Create(context.TODO(), &protoPod, metav1.CreateOptions{}) createdPod, err := c.CoreV1().Pods(ns).Create(context.TODO(), &protoPod, metav1.CreateOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
if createdPod.Spec.ServiceAccountName != expectedServiceAccount { if createdPod.Spec.ServiceAccountName != expectedServiceAccount {
t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccountName) t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccountName)
} }
if !apiequality.Semantic.DeepEqual(&expectedVolumes, &createdPod.Spec.Volumes) { if len(createdPod.Spec.Volumes) == 0 || createdPod.Spec.Volumes[0].Projected == nil {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedVolumes, createdPod.Spec.Volumes) t.Fatal("Expected projected volume for service account token inserted")
}
if !apiequality.Semantic.DeepEqual(&expectedContainer1VolumeMounts, &createdPod.Spec.Containers[0].VolumeMounts) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer1VolumeMounts, createdPod.Spec.Containers[0].VolumeMounts)
}
if !apiequality.Semantic.DeepEqual(&expectedContainer2VolumeMounts, &createdPod.Spec.Containers[1].VolumeMounts) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedContainer2VolumeMounts, createdPod.Spec.Containers[1].VolumeMounts)
} }
} }