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
// alpha: v1.13
// beta: v1.21
// ga: v1.22
//
// Migrate ServiceAccount volumes to use a projected volume consisting of a
// ServiceAccountTokenVolumeProjection. This feature adds new required flags
@ -764,7 +765,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
StorageObjectInUseProtection: {Default: true, PreRelease: featuregate.GA},
SupportPodPidsLimit: {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
CSIMigration: {Default: true, PreRelease: featuregate.Beta},
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
errs := provider.ValidatePod(pod)
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 {
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"
"k8s.io/apimachinery/pkg/api/errors"
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/apiserver/pkg/admission"
genericadmissioninitializer "k8s.io/apiserver/pkg/admission/initializer"
apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/component-base/featuregate"
podutil "k8s.io/kubernetes/pkg/api/pod"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
"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 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 bool
client kubernetes.Interface
serviceAccountLister corev1listers.ServiceAccountLister
secretLister corev1listers.SecretLister
generateName func(string) string
boundServiceAccountTokenVolume bool
}
var _ admission.MutationInterface = &Plugin{}
var _ admission.ValidationInterface = &Plugin{}
var _ genericadmissioninitializer.WantsFeatures = &Plugin{}
var _ = genericadmissioninitializer.WantsExternalKubeClientSet(&Plugin{})
var _ = genericadmissioninitializer.WantsExternalKubeInformerFactory(&Plugin{})
@ -114,18 +103,11 @@ func NewServiceAccount() *Plugin {
LimitSecretReferences: false,
// Auto mount service account API token secrets
MountServiceAccountToken: true,
// Reject pod creation until a service account token is available
RequireAPIToken: true,
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
func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) {
s.client = cl
@ -135,12 +117,8 @@ func (s *Plugin) SetExternalKubeClientSet(cl kubernetes.Interface) {
func (s *Plugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
serviceAccountInformer := f.Core().V1().ServiceAccounts()
s.serviceAccountLister = serviceAccountInformer.Lister()
secretInformer := f.Core().V1().Secrets()
s.secretLister = secretInformer.Lister()
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 {
return fmt.Errorf("missing client")
}
if s.secretLister == nil {
return fmt.Errorf("missing secretLister")
}
if s.serviceAccountLister == nil {
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))
}
if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) {
if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil {
if _, ok := err.(errors.APIStatus); ok {
return err
}
return admission.NewForbidden(a, err)
}
s.mountServiceAccountToken(serviceAccount, pod)
}
if len(pod.Spec.ImagePullSecrets) == 0 {
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)
}
// 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 {
// Ensure all secrets the pod references are allowed by the service account
mountableSecrets := sets.NewString()
@ -424,38 +348,14 @@ func (s *Plugin) limitSecretReferences(serviceAccount *corev1.ServiceAccount, po
return nil
}
func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
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
}
func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) {
// Find the volume and volume name for the ServiceAccountTokenSecret if it already exists
tokenVolumeName := ""
hasTokenVolume := false
allVolumeNames := sets.NewString()
for _, volume := range pod.Spec.Volumes {
allVolumeNames.Insert(volume.Name)
if (!s.boundServiceAccountTokenVolume && volume.Secret != nil && volume.Secret.SecretName == serviceAccountToken) ||
(s.boundServiceAccountTokenVolume && strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-")) {
if strings.HasPrefix(volume.Name, ServiceAccountVolumeName+"-") {
tokenVolumeName = volume.Name
hasTokenVolume = true
break
@ -464,16 +364,7 @@ func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount,
// Determine a volume name for the ServiceAccountTokenSecret in case we need it
if len(tokenVolumeName) == 0 {
if s.boundServiceAccountTokenVolume {
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
@ -516,27 +407,12 @@ func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount,
// Add the volume if a container needs it
if !hasTokenVolume && needsTokenVolume {
pod.Spec.Volumes = append(pod.Spec.Volumes, s.createVolume(tokenVolumeName, serviceAccountToken))
}
return nil
}
func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
if s.boundServiceAccountTokenVolume {
return api.Volume{
pod.Spec.Volumes = append(pod.Spec.Volumes, api.Volume{
Name: tokenVolumeName,
VolumeSource: api.VolumeSource{
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"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff"
@ -32,18 +31,11 @@ import (
admissiontesting "k8s.io/apiserver/pkg/admission/testing"
"k8s.io/client-go/informers"
"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"
"k8s.io/kubernetes/pkg/controller"
kubelet "k8s.io/kubernetes/pkg/kubelet/types"
)
var (
deprecationDisabledBoundTokenVolume = false
deprecationEnabledBoundTokenVolume = true
)
func TestIgnoresNonCreate(t *testing.T) {
for _, op := range []admission.Operation{admission.Delete, admission.Connect} {
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) {
ns := "myns"
@ -225,8 +164,6 @@ func TestAssignsDefaultServiceAccountAndBoundTokenWithNoSecretTokens(t *testing.
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
admit.boundServiceAccountTokenVolume = true
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -300,7 +237,6 @@ func TestFetchesUncachedServiceAccount(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.client = client
admit.RequireAPIToken = false
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)
@ -333,25 +269,25 @@ func TestDeniesInvalidServiceAccount(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())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.generateName = testGenerateName
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
ns := "myns"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
tokenName := "token-name"
if admit.boundServiceAccountTokenVolume {
tokenName = generatedVolumeName
}
tokenName := generatedVolumeName
expectedVolume := admit.createVolume(tokenName, tokenName)
expectedVolume := api.Volume{
Name: tokenName,
VolumeSource: api.VolumeSource{
Projected: TokenVolumeSource(),
},
}
expectedVolumeMount := api.VolumeMount{
Name: tokenName,
ReadOnly: true,
@ -364,24 +300,6 @@ func TestAutomountsAPIToken(t *testing.T) {
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{
@ -439,13 +357,10 @@ func TestAutomountsAPIToken(t *testing.T) {
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) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) {
ns := "myns"
tokenName := "token-name"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
@ -455,11 +370,10 @@ func TestRespectsExistingMount(t *testing.T) {
MountPath: DefaultAPITokenMountPath,
}
admit := applyFeatures(NewServiceAccount())
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 with a token into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -468,24 +382,6 @@ func TestRespectsExistingMount(t *testing.T) {
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{
corev1.ServiceAccountTokenKey: []byte("token-data"),
},
})
// Define a pod with a container that already mounts a volume at the API token path
@ -548,7 +444,6 @@ func TestRespectsExistingMount(t *testing.T) {
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) {
@ -558,7 +453,6 @@ func TestAllowsReferencedSecret(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -639,7 +533,6 @@ func TestRejectsUnreferencedSecretVolumes(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -717,7 +610,6 @@ func TestAllowUnreferencedSecretVolumesForPermissiveSAs(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = false
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -749,7 +641,6 @@ func TestAllowsReferencedImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -781,7 +672,6 @@ func TestRejectsUnreferencedImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -810,7 +700,6 @@ func TestDoNotAddImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
// Add the default service account for the ns with a secret reference into the cache
informerFactory.Core().V1().ServiceAccounts().Informer().GetStore().Add(&corev1.ServiceAccount{
@ -847,7 +736,6 @@ func TestAddImagePullSecrets(t *testing.T) {
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.LimitSecretReferences = true
admit.RequireAPIToken = false
sa := &corev1.ServiceAccount{
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 {
return n + "abc123"
}
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)
extraEnvs := []string{"KUBE_FEATURE_GATES=BoundServiceAccountTokenVolume=true"}
upgradeFunc := common.ControlPlaneUpgradeFunc(f, upgCtx, serviceaccountAdmissionControllerMigrationTest, extraEnvs)
upgradeFunc := common.ControlPlaneUpgradeFunc(f, upgCtx, serviceaccountAdmissionControllerMigrationTest, nil)
upgrades.RunUpgradeSuite(upgCtx, upgradeTests, testFrameworks, testSuite, upgrades.MasterUpgrade, upgradeFunc)
})
})

View File

@ -31,7 +31,6 @@ import (
"time"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -211,70 +210,30 @@ func TestServiceAccountTokenAutoMount(t *testing.T) {
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
protoPod := v1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "protopod"},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "container-1",
Image: "container-1-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{}},
Name: "container",
Image: "container-image",
},
},
},
}
// 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{})
if err != nil {
t.Fatal(err)
}
expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName
if createdPod.Spec.ServiceAccountName != expectedServiceAccount {
t.Fatalf("Expected %s, got %s", expectedServiceAccount, createdPod.Spec.ServiceAccountName)
}
if !apiequality.Semantic.DeepEqual(&expectedVolumes, &createdPod.Spec.Volumes) {
t.Fatalf("Expected\n\t%#v\n\tgot\n\t%#v", expectedVolumes, createdPod.Spec.Volumes)
}
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)
if len(createdPod.Spec.Volumes) == 0 || createdPod.Spec.Volumes[0].Projected == nil {
t.Fatal("Expected projected volume for service account token inserted")
}
}