mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Merge pull request #101992 from zshihang/bound
BoundServiceAccountTokenVolume ga
This commit is contained in:
commit
548f16dbbb
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user