BoundServiceAccountTokenVolume ga

This commit is contained in:
Shihang Zhang
2021-05-13 15:03:03 -07:00
parent 9f0f14952c
commit 88b31814f4
6 changed files with 180 additions and 797 deletions

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))
}
}
tokenVolumeName = s.generateName(ServiceAccountVolumeName + "-")
}
// 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,222 +269,181 @@ func TestDeniesInvalidServiceAccount(t *testing.T) {
}
func TestAutomountsAPIToken(t *testing.T) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) {
admit := applyFeatures(NewServiceAccount())
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.generateName = testGenerateName
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.generateName = testGenerateName
admit.MountServiceAccountToken = true
ns := "myns"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
ns := "myns"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
tokenName := "token-name"
if admit.boundServiceAccountTokenVolume {
tokenName = generatedVolumeName
}
tokenName := generatedVolumeName
expectedVolume := admit.createVolume(tokenName, tokenName)
expectedVolumeMount := api.VolumeMount{
Name: tokenName,
ReadOnly: true,
MountPath: DefaultAPITokenMountPath,
}
// 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{
{},
},
},
}
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])
}
expectedVolume := api.Volume{
Name: tokenName,
VolumeSource: api.VolumeSource{
Projected: TokenVolumeSource(),
},
}
expectedVolumeMount := api.VolumeMount{
Name: tokenName,
ReadOnly: true,
MountPath: DefaultAPITokenMountPath,
}
// 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),
},
})
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) {
testBoundServiceAccountTokenVolumePhases(t, func(t *testing.T, applyFeatures func(*Plugin) *Plugin) {
ns := "myns"
tokenName := "token-name"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
ns := "myns"
serviceAccountName := DefaultServiceAccountName
serviceAccountUID := "12345"
expectedVolumeMount := api.VolumeMount{
Name: "my-custom-mount",
ReadOnly: false,
MountPath: DefaultAPITokenMountPath,
}
expectedVolumeMount := api.VolumeMount{
Name: "my-custom-mount",
ReadOnly: false,
MountPath: DefaultAPITokenMountPath,
}
admit := applyFeatures(NewServiceAccount())
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = true
admit.RequireAPIToken = true
admit := NewServiceAccount()
informerFactory := informers.NewSharedInformerFactory(nil, controller.NoResyncPeriodFunc())
admit.SetExternalKubeInformerFactory(informerFactory)
admit.MountServiceAccountToken = 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{
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])
}
// 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),
},
})
// 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) {
@@ -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
})
})
}