vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

View File

@ -0,0 +1,85 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"serviceaccounts_controller.go",
"tokengetter.go",
"tokens_controller.go",
],
importpath = "k8s.io/kubernetes/pkg/controller/serviceaccount",
deps = [
"//pkg/apis/core/v1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/registry/core/secret:go_default_library",
"//pkg/registry/core/secret/storage:go_default_library",
"//pkg/registry/core/serviceaccount:go_default_library",
"//pkg/registry/core/serviceaccount/storage:go_default_library",
"//pkg/serviceaccount:go_default_library",
"//pkg/util/metrics:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/client-go/informers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/listers/core/v1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/util/retry:go_default_library",
"//vendor/k8s.io/client-go/util/workqueue:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"serviceaccounts_controller_test.go",
"tokens_controller_test.go",
],
importpath = "k8s.io/kubernetes/pkg/controller/serviceaccount",
library = ":go_default_library",
deps = [
"//pkg/apis/core:go_default_library",
"//pkg/controller:go_default_library",
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/testing:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,7 @@
approvers:
- liggitt
- deads2k
reviewers:
- liggitt
- deads2k
- enj

View File

@ -0,0 +1,19 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package serviceaccount provides implementations
// to manage service accounts and service account tokens
package serviceaccount // import "k8s.io/kubernetes/pkg/controller/serviceaccount"

View File

@ -0,0 +1,221 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"fmt"
"time"
"github.com/golang/glog"
"k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
coreinformers "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/util/metrics"
)
// ServiceAccountsControllerOptions contains options for running a ServiceAccountsController
type ServiceAccountsControllerOptions struct {
// ServiceAccounts is the list of service accounts to ensure exist in every namespace
ServiceAccounts []v1.ServiceAccount
// ServiceAccountResync is the interval between full resyncs of ServiceAccounts.
// If non-zero, all service accounts will be re-listed this often.
// Otherwise, re-list will be delayed as long as possible (until the watch is closed or times out).
ServiceAccountResync time.Duration
// NamespaceResync is the interval between full resyncs of Namespaces.
// If non-zero, all namespaces will be re-listed this often.
// Otherwise, re-list will be delayed as long as possible (until the watch is closed or times out).
NamespaceResync time.Duration
}
func DefaultServiceAccountsControllerOptions() ServiceAccountsControllerOptions {
return ServiceAccountsControllerOptions{
ServiceAccounts: []v1.ServiceAccount{
{ObjectMeta: metav1.ObjectMeta{Name: "default"}},
},
}
}
// NewServiceAccountsController returns a new *ServiceAccountsController.
func NewServiceAccountsController(saInformer coreinformers.ServiceAccountInformer, nsInformer coreinformers.NamespaceInformer, cl clientset.Interface, options ServiceAccountsControllerOptions) (*ServiceAccountsController, error) {
e := &ServiceAccountsController{
client: cl,
serviceAccountsToEnsure: options.ServiceAccounts,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount"),
}
if cl != nil && cl.CoreV1().RESTClient().GetRateLimiter() != nil {
if err := metrics.RegisterMetricAndTrackRateLimiterUsage("serviceaccount_controller", cl.CoreV1().RESTClient().GetRateLimiter()); err != nil {
return nil, err
}
}
saInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
DeleteFunc: e.serviceAccountDeleted,
})
e.saLister = saInformer.Lister()
e.saListerSynced = saInformer.Informer().HasSynced
nsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: e.namespaceAdded,
UpdateFunc: e.namespaceUpdated,
})
e.nsLister = nsInformer.Lister()
e.nsListerSynced = nsInformer.Informer().HasSynced
e.syncHandler = e.syncNamespace
return e, nil
}
// ServiceAccountsController manages ServiceAccount objects inside Namespaces
type ServiceAccountsController struct {
client clientset.Interface
serviceAccountsToEnsure []v1.ServiceAccount
// To allow injection for testing.
syncHandler func(key string) error
saLister corelisters.ServiceAccountLister
saListerSynced cache.InformerSynced
nsLister corelisters.NamespaceLister
nsListerSynced cache.InformerSynced
queue workqueue.RateLimitingInterface
}
func (c *ServiceAccountsController) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
glog.Infof("Starting service account controller")
defer glog.Infof("Shutting down service account controller")
if !controller.WaitForCacheSync("service account", stopCh, c.saListerSynced, c.nsListerSynced) {
return
}
for i := 0; i < workers; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
}
// serviceAccountDeleted reacts to a ServiceAccount deletion by recreating a default ServiceAccount in the namespace if needed
func (c *ServiceAccountsController) serviceAccountDeleted(obj interface{}) {
sa, ok := obj.(*v1.ServiceAccount)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
utilruntime.HandleError(fmt.Errorf("Couldn't get object from tombstone %#v", obj))
return
}
sa, ok = tombstone.Obj.(*v1.ServiceAccount)
if !ok {
utilruntime.HandleError(fmt.Errorf("Tombstone contained object that is not a ServiceAccount %#v", obj))
return
}
}
c.queue.Add(sa.Namespace)
}
// namespaceAdded reacts to a Namespace creation by creating a default ServiceAccount object
func (c *ServiceAccountsController) namespaceAdded(obj interface{}) {
namespace := obj.(*v1.Namespace)
c.queue.Add(namespace.Name)
}
// namespaceUpdated reacts to a Namespace update (or re-list) by creating a default ServiceAccount in the namespace if needed
func (c *ServiceAccountsController) namespaceUpdated(oldObj interface{}, newObj interface{}) {
newNamespace := newObj.(*v1.Namespace)
c.queue.Add(newNamespace.Name)
}
func (c *ServiceAccountsController) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *ServiceAccountsController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncHandler(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *ServiceAccountsController) syncNamespace(key string) error {
startTime := time.Now()
defer func() {
glog.V(4).Infof("Finished syncing namespace %q (%v)", key, time.Now().Sub(startTime))
}()
ns, err := c.nsLister.Get(key)
if apierrs.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if ns.Status.Phase != v1.NamespaceActive {
// If namespace is not active, we shouldn't try to create anything
return nil
}
createFailures := []error{}
for i := range c.serviceAccountsToEnsure {
sa := c.serviceAccountsToEnsure[i]
switch _, err := c.saLister.ServiceAccounts(ns.Name).Get(sa.Name); {
case err == nil:
continue
case apierrs.IsNotFound(err):
case err != nil:
return err
}
// this is only safe because we never read it and we always write it
// TODO eliminate this once the fake client can handle creation without NS
sa.Namespace = ns.Name
if _, err := c.client.CoreV1().ServiceAccounts(ns.Name).Create(&sa); err != nil && !apierrs.IsAlreadyExists(err) {
createFailures = append(createFailures, err)
}
}
return utilerrors.Flatten(utilerrors.NewAggregate(createFailures))
}

View File

@ -0,0 +1,242 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"testing"
"time"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/kubernetes/pkg/controller"
)
type serverResponse struct {
statusCode int
obj interface{}
}
func TestServiceAccountCreation(t *testing.T) {
ns := metav1.NamespaceDefault
defaultName := "default"
managedName := "managed"
activeNS := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: ns},
Status: v1.NamespaceStatus{
Phase: v1.NamespaceActive,
},
}
terminatingNS := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{Name: ns},
Status: v1.NamespaceStatus{
Phase: v1.NamespaceTerminating,
},
}
defaultServiceAccount := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: defaultName,
Namespace: ns,
ResourceVersion: "1",
},
}
managedServiceAccount := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: managedName,
Namespace: ns,
ResourceVersion: "1",
},
}
unmanagedServiceAccount := &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "other-unmanaged",
Namespace: ns,
ResourceVersion: "1",
},
}
testcases := map[string]struct {
ExistingNamespace *v1.Namespace
ExistingServiceAccounts []*v1.ServiceAccount
AddedNamespace *v1.Namespace
UpdatedNamespace *v1.Namespace
DeletedServiceAccount *v1.ServiceAccount
ExpectCreatedServiceAccounts []string
}{
"new active namespace missing serviceaccounts": {
ExistingServiceAccounts: []*v1.ServiceAccount{},
AddedNamespace: activeNS,
ExpectCreatedServiceAccounts: sets.NewString(defaultName, managedName).List(),
},
"new active namespace missing serviceaccount": {
ExistingServiceAccounts: []*v1.ServiceAccount{managedServiceAccount},
AddedNamespace: activeNS,
ExpectCreatedServiceAccounts: []string{defaultName},
},
"new active namespace with serviceaccounts": {
ExistingServiceAccounts: []*v1.ServiceAccount{defaultServiceAccount, managedServiceAccount},
AddedNamespace: activeNS,
ExpectCreatedServiceAccounts: []string{},
},
"new terminating namespace": {
ExistingServiceAccounts: []*v1.ServiceAccount{},
AddedNamespace: terminatingNS,
ExpectCreatedServiceAccounts: []string{},
},
"updated active namespace missing serviceaccounts": {
ExistingServiceAccounts: []*v1.ServiceAccount{},
UpdatedNamespace: activeNS,
ExpectCreatedServiceAccounts: sets.NewString(defaultName, managedName).List(),
},
"updated active namespace missing serviceaccount": {
ExistingServiceAccounts: []*v1.ServiceAccount{defaultServiceAccount},
UpdatedNamespace: activeNS,
ExpectCreatedServiceAccounts: []string{managedName},
},
"updated active namespace with serviceaccounts": {
ExistingServiceAccounts: []*v1.ServiceAccount{defaultServiceAccount, managedServiceAccount},
UpdatedNamespace: activeNS,
ExpectCreatedServiceAccounts: []string{},
},
"updated terminating namespace": {
ExistingServiceAccounts: []*v1.ServiceAccount{},
UpdatedNamespace: terminatingNS,
ExpectCreatedServiceAccounts: []string{},
},
"deleted serviceaccount without namespace": {
DeletedServiceAccount: defaultServiceAccount,
ExpectCreatedServiceAccounts: []string{},
},
"deleted serviceaccount with active namespace": {
ExistingServiceAccounts: []*v1.ServiceAccount{managedServiceAccount},
ExistingNamespace: activeNS,
DeletedServiceAccount: defaultServiceAccount,
ExpectCreatedServiceAccounts: []string{defaultName},
},
"deleted serviceaccount with terminating namespace": {
ExistingNamespace: terminatingNS,
DeletedServiceAccount: defaultServiceAccount,
ExpectCreatedServiceAccounts: []string{},
},
"deleted unmanaged serviceaccount with active namespace": {
ExistingServiceAccounts: []*v1.ServiceAccount{defaultServiceAccount, managedServiceAccount},
ExistingNamespace: activeNS,
DeletedServiceAccount: unmanagedServiceAccount,
ExpectCreatedServiceAccounts: []string{},
},
"deleted unmanaged serviceaccount with terminating namespace": {
ExistingNamespace: terminatingNS,
DeletedServiceAccount: unmanagedServiceAccount,
ExpectCreatedServiceAccounts: []string{},
},
}
for k, tc := range testcases {
client := fake.NewSimpleClientset(defaultServiceAccount, managedServiceAccount)
informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), controller.NoResyncPeriodFunc())
options := DefaultServiceAccountsControllerOptions()
options.ServiceAccounts = []v1.ServiceAccount{
{ObjectMeta: metav1.ObjectMeta{Name: defaultName}},
{ObjectMeta: metav1.ObjectMeta{Name: managedName}},
}
saInformer := informers.Core().V1().ServiceAccounts()
nsInformer := informers.Core().V1().Namespaces()
controller, err := NewServiceAccountsController(
saInformer,
nsInformer,
client,
options,
)
if err != nil {
t.Fatalf("error creating ServiceAccounts controller: %v", err)
}
controller.saListerSynced = alwaysReady
controller.nsListerSynced = alwaysReady
saStore := saInformer.Informer().GetStore()
nsStore := nsInformer.Informer().GetStore()
syncCalls := make(chan struct{})
controller.syncHandler = func(key string) error {
err := controller.syncNamespace(key)
if err != nil {
t.Logf("%s: %v", k, err)
}
syncCalls <- struct{}{}
return err
}
stopCh := make(chan struct{})
defer close(stopCh)
go controller.Run(1, stopCh)
if tc.ExistingNamespace != nil {
nsStore.Add(tc.ExistingNamespace)
}
for _, s := range tc.ExistingServiceAccounts {
saStore.Add(s)
}
if tc.AddedNamespace != nil {
nsStore.Add(tc.AddedNamespace)
controller.namespaceAdded(tc.AddedNamespace)
}
if tc.UpdatedNamespace != nil {
nsStore.Add(tc.UpdatedNamespace)
controller.namespaceUpdated(nil, tc.UpdatedNamespace)
}
if tc.DeletedServiceAccount != nil {
controller.serviceAccountDeleted(tc.DeletedServiceAccount)
}
// wait to be called
select {
case <-syncCalls:
case <-time.After(10 * time.Second):
t.Errorf("%s: took too long", k)
}
actions := client.Actions()
if len(tc.ExpectCreatedServiceAccounts) != len(actions) {
t.Errorf("%s: Expected to create accounts %#v. Actual actions were: %#v", k, tc.ExpectCreatedServiceAccounts, actions)
continue
}
for i, expectedName := range tc.ExpectCreatedServiceAccounts {
action := actions[i]
if !action.Matches("create", "serviceaccounts") {
t.Errorf("%s: Unexpected action %s", k, action)
break
}
createdAccount := action.(core.CreateAction).GetObject().(*v1.ServiceAccount)
if createdAccount.Name != expectedName {
t.Errorf("%s: Expected %s to be created, got %s", k, expectedName, createdAccount.Name)
}
}
}
}
var alwaysReady = func() bool { return true }

View File

@ -0,0 +1,101 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage/storagebackend"
clientset "k8s.io/client-go/kubernetes"
apiv1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/registry/core/secret"
secretstore "k8s.io/kubernetes/pkg/registry/core/secret/storage"
serviceaccountregistry "k8s.io/kubernetes/pkg/registry/core/serviceaccount"
serviceaccountstore "k8s.io/kubernetes/pkg/registry/core/serviceaccount/storage"
"k8s.io/kubernetes/pkg/serviceaccount"
)
// clientGetter implements ServiceAccountTokenGetter using a clientset.Interface
type clientGetter struct {
client clientset.Interface
}
// NewGetterFromClient returns a ServiceAccountTokenGetter that
// uses the specified client to retrieve service accounts and secrets.
// The client should NOT authenticate using a service account token
// the returned getter will be used to retrieve, or recursion will result.
func NewGetterFromClient(c clientset.Interface) serviceaccount.ServiceAccountTokenGetter {
return clientGetter{c}
}
func (c clientGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) {
return c.client.CoreV1().ServiceAccounts(namespace).Get(name, metav1.GetOptions{})
}
func (c clientGetter) GetSecret(namespace, name string) (*v1.Secret, error) {
return c.client.CoreV1().Secrets(namespace).Get(name, metav1.GetOptions{})
}
// registryGetter implements ServiceAccountTokenGetter using a service account and secret registry
type registryGetter struct {
serviceAccounts serviceaccountregistry.Registry
secrets secret.Registry
}
// NewGetterFromRegistries returns a ServiceAccountTokenGetter that
// uses the specified registries to retrieve service accounts and secrets.
func NewGetterFromRegistries(serviceAccounts serviceaccountregistry.Registry, secrets secret.Registry) serviceaccount.ServiceAccountTokenGetter {
return &registryGetter{serviceAccounts, secrets}
}
func (r *registryGetter) GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), namespace)
internalServiceAccount, err := r.serviceAccounts.GetServiceAccount(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, err
}
v1ServiceAccount := v1.ServiceAccount{}
err = apiv1.Convert_core_ServiceAccount_To_v1_ServiceAccount(internalServiceAccount, &v1ServiceAccount, nil)
return &v1ServiceAccount, err
}
func (r *registryGetter) GetSecret(namespace, name string) (*v1.Secret, error) {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), namespace)
internalSecret, err := r.secrets.GetSecret(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, err
}
v1Secret := v1.Secret{}
err = apiv1.Convert_core_Secret_To_v1_Secret(internalSecret, &v1Secret, nil)
return &v1Secret, err
}
// NewGetterFromStorageInterface returns a ServiceAccountTokenGetter that
// uses the specified storage to retrieve service accounts and secrets.
func NewGetterFromStorageInterface(
saConfig *storagebackend.Config,
saPrefix string,
secretConfig *storagebackend.Config,
secretPrefix string) serviceaccount.ServiceAccountTokenGetter {
saOpts := generic.RESTOptions{StorageConfig: saConfig, Decorator: generic.UndecoratedStorage, ResourcePrefix: saPrefix}
secretOpts := generic.RESTOptions{StorageConfig: secretConfig, Decorator: generic.UndecoratedStorage, ResourcePrefix: secretPrefix}
return NewGetterFromRegistries(
serviceaccountregistry.NewRegistry(serviceaccountstore.NewREST(saOpts)),
secret.NewRegistry(secretstore.NewREST(secretOpts)),
)
}

View File

@ -0,0 +1,781 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"bytes"
"fmt"
"time"
"github.com/golang/glog"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
informers "k8s.io/client-go/informers/core/v1"
clientset "k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
clientretry "k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/registry/core/secret"
"k8s.io/kubernetes/pkg/serviceaccount"
"k8s.io/kubernetes/pkg/util/metrics"
)
// RemoveTokenBackoff is the recommended (empirical) retry interval for removing
// a secret reference from a service account when the secret is deleted. It is
// exported for use by custom secret controllers.
var RemoveTokenBackoff = wait.Backoff{
Steps: 10,
Duration: 100 * time.Millisecond,
Jitter: 1.0,
}
// TokensControllerOptions contains options for the TokensController
type TokensControllerOptions struct {
// TokenGenerator is the generator to use to create new tokens
TokenGenerator serviceaccount.TokenGenerator
// ServiceAccountResync is the time.Duration at which to fully re-list service accounts.
// If zero, re-list will be delayed as long as possible
ServiceAccountResync time.Duration
// SecretResync is the time.Duration at which to fully re-list secrets.
// If zero, re-list will be delayed as long as possible
SecretResync time.Duration
// This CA will be added in the secrets of service accounts
RootCA []byte
// MaxRetries controls the maximum number of times a particular key is retried before giving up
// If zero, a default max is used
MaxRetries int
}
// NewTokensController returns a new *TokensController.
func NewTokensController(serviceAccounts informers.ServiceAccountInformer, secrets informers.SecretInformer, cl clientset.Interface, options TokensControllerOptions) (*TokensController, error) {
maxRetries := options.MaxRetries
if maxRetries == 0 {
maxRetries = 10
}
e := &TokensController{
client: cl,
token: options.TokenGenerator,
rootCA: options.RootCA,
syncServiceAccountQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_service"),
syncSecretQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_secret"),
maxRetries: maxRetries,
}
if cl != nil && cl.CoreV1().RESTClient().GetRateLimiter() != nil {
if err := metrics.RegisterMetricAndTrackRateLimiterUsage("serviceaccount_tokens_controller", cl.CoreV1().RESTClient().GetRateLimiter()); err != nil {
return nil, err
}
}
e.serviceAccounts = serviceAccounts.Lister()
e.serviceAccountSynced = serviceAccounts.Informer().HasSynced
serviceAccounts.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
AddFunc: e.queueServiceAccountSync,
UpdateFunc: e.queueServiceAccountUpdateSync,
DeleteFunc: e.queueServiceAccountSync,
},
options.ServiceAccountResync,
)
secretCache := secrets.Informer().GetIndexer()
e.updatedSecrets = cache.NewIntegerResourceVersionMutationCache(secretCache, secretCache, 60*time.Second, true)
e.secretSynced = secrets.Informer().HasSynced
secrets.Informer().AddEventHandlerWithResyncPeriod(
cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
switch t := obj.(type) {
case *v1.Secret:
return t.Type == v1.SecretTypeServiceAccountToken
default:
utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
return false
}
},
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: e.queueSecretSync,
UpdateFunc: e.queueSecretUpdateSync,
DeleteFunc: e.queueSecretSync,
},
},
options.SecretResync,
)
return e, nil
}
// TokensController manages ServiceAccountToken secrets for ServiceAccount objects
type TokensController struct {
client clientset.Interface
token serviceaccount.TokenGenerator
rootCA []byte
serviceAccounts listersv1.ServiceAccountLister
// updatedSecrets is a wrapper around the shared cache which allows us to record
// and return our local mutations (since we're very likely to act on an updated
// secret before the watch reports it).
updatedSecrets cache.MutationCache
// Since we join two objects, we'll watch both of them with controllers.
serviceAccountSynced cache.InformerSynced
secretSynced cache.InformerSynced
// syncServiceAccountQueue handles service account events:
// * ensures a referenced token exists for service accounts which still exist
// * ensures tokens are removed for service accounts which no longer exist
// key is "<namespace>/<name>/<uid>"
syncServiceAccountQueue workqueue.RateLimitingInterface
// syncSecretQueue handles secret events:
// * deletes tokens whose service account no longer exists
// * updates tokens with missing token or namespace data, or mismatched ca data
// * ensures service account secret references are removed for tokens which are deleted
// key is a secretQueueKey{}
syncSecretQueue workqueue.RateLimitingInterface
maxRetries int
}
// Runs controller blocks until stopCh is closed
func (e *TokensController) Run(workers int, stopCh <-chan struct{}) {
// Shut down queues
defer utilruntime.HandleCrash()
defer e.syncServiceAccountQueue.ShutDown()
defer e.syncSecretQueue.ShutDown()
if !controller.WaitForCacheSync("tokens", stopCh, e.serviceAccountSynced, e.secretSynced) {
return
}
glog.V(5).Infof("Starting workers")
for i := 0; i < workers; i++ {
go wait.Until(e.syncServiceAccount, 0, stopCh)
go wait.Until(e.syncSecret, 0, stopCh)
}
<-stopCh
glog.V(1).Infof("Shutting down")
}
func (e *TokensController) queueServiceAccountSync(obj interface{}) {
if serviceAccount, ok := obj.(*v1.ServiceAccount); ok {
e.syncServiceAccountQueue.Add(makeServiceAccountKey(serviceAccount))
}
}
func (e *TokensController) queueServiceAccountUpdateSync(oldObj interface{}, newObj interface{}) {
if serviceAccount, ok := newObj.(*v1.ServiceAccount); ok {
e.syncServiceAccountQueue.Add(makeServiceAccountKey(serviceAccount))
}
}
// complete optionally requeues key, then calls queue.Done(key)
func (e *TokensController) retryOrForget(queue workqueue.RateLimitingInterface, key interface{}, requeue bool) {
if !requeue {
queue.Forget(key)
return
}
requeueCount := queue.NumRequeues(key)
if requeueCount < e.maxRetries {
queue.AddRateLimited(key)
return
}
glog.V(4).Infof("retried %d times: %#v", requeueCount, key)
queue.Forget(key)
}
func (e *TokensController) queueSecretSync(obj interface{}) {
if secret, ok := obj.(*v1.Secret); ok {
e.syncSecretQueue.Add(makeSecretQueueKey(secret))
}
}
func (e *TokensController) queueSecretUpdateSync(oldObj interface{}, newObj interface{}) {
if secret, ok := newObj.(*v1.Secret); ok {
e.syncSecretQueue.Add(makeSecretQueueKey(secret))
}
}
func (e *TokensController) syncServiceAccount() {
key, quit := e.syncServiceAccountQueue.Get()
if quit {
return
}
defer e.syncServiceAccountQueue.Done(key)
retry := false
defer func() {
e.retryOrForget(e.syncServiceAccountQueue, key, retry)
}()
saInfo, err := parseServiceAccountKey(key)
if err != nil {
glog.Error(err)
return
}
sa, err := e.getServiceAccount(saInfo.namespace, saInfo.name, saInfo.uid, false)
switch {
case err != nil:
glog.Error(err)
retry = true
case sa == nil:
// service account no longer exists, so delete related tokens
glog.V(4).Infof("syncServiceAccount(%s/%s), service account deleted, removing tokens", saInfo.namespace, saInfo.name)
sa = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: saInfo.namespace, Name: saInfo.name, UID: saInfo.uid}}
retry, err = e.deleteTokens(sa)
if err != nil {
glog.Errorf("error deleting serviceaccount tokens for %s/%s: %v", saInfo.namespace, saInfo.name, err)
}
default:
// ensure a token exists and is referenced by this service account
retry, err = e.ensureReferencedToken(sa)
if err != nil {
glog.Errorf("error synchronizing serviceaccount %s/%s: %v", saInfo.namespace, saInfo.name, err)
}
}
}
func (e *TokensController) syncSecret() {
key, quit := e.syncSecretQueue.Get()
if quit {
return
}
defer e.syncSecretQueue.Done(key)
// Track whether or not we should retry this sync
retry := false
defer func() {
e.retryOrForget(e.syncSecretQueue, key, retry)
}()
secretInfo, err := parseSecretQueueKey(key)
if err != nil {
glog.Error(err)
return
}
secret, err := e.getSecret(secretInfo.namespace, secretInfo.name, secretInfo.uid, false)
switch {
case err != nil:
glog.Error(err)
retry = true
case secret == nil:
// If the service account exists
if sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, false); saErr == nil && sa != nil {
// secret no longer exists, so delete references to this secret from the service account
if err := clientretry.RetryOnConflict(RemoveTokenBackoff, func() error {
return e.removeSecretReference(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, secretInfo.name)
}); err != nil {
glog.Error(err)
}
}
default:
// Ensure service account exists
sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, true)
switch {
case saErr != nil:
glog.Error(saErr)
retry = true
case sa == nil:
// Delete token
glog.V(4).Infof("syncSecret(%s/%s), service account does not exist, deleting token", secretInfo.namespace, secretInfo.name)
if retriable, err := e.deleteToken(secretInfo.namespace, secretInfo.name, secretInfo.uid); err != nil {
glog.Errorf("error deleting serviceaccount token %s/%s for service account %s: %v", secretInfo.namespace, secretInfo.name, secretInfo.saName, err)
retry = retriable
}
default:
// Update token if needed
if retriable, err := e.generateTokenIfNeeded(sa, secret); err != nil {
glog.Errorf("error populating serviceaccount token %s/%s for service account %s: %v", secretInfo.namespace, secretInfo.name, secretInfo.saName, err)
retry = retriable
}
}
}
}
func (e *TokensController) deleteTokens(serviceAccount *v1.ServiceAccount) ( /*retry*/ bool, error) {
tokens, err := e.listTokenSecrets(serviceAccount)
if err != nil {
// don't retry on cache lookup errors
return false, err
}
retry := false
errs := []error{}
for _, token := range tokens {
r, err := e.deleteToken(token.Namespace, token.Name, token.UID)
if err != nil {
errs = append(errs, err)
}
if r {
retry = true
}
}
return retry, utilerrors.NewAggregate(errs)
}
func (e *TokensController) deleteToken(ns, name string, uid types.UID) ( /*retry*/ bool, error) {
var opts *metav1.DeleteOptions
if len(uid) > 0 {
opts = &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}}
}
err := e.client.CoreV1().Secrets(ns).Delete(name, opts)
// NotFound doesn't need a retry (it's already been deleted)
// Conflict doesn't need a retry (the UID precondition failed)
if err == nil || apierrors.IsNotFound(err) || apierrors.IsConflict(err) {
return false, nil
}
// Retry for any other error
return true, err
}
// ensureReferencedToken makes sure at least one ServiceAccountToken secret exists, and is included in the serviceAccount's Secrets list
func (e *TokensController) ensureReferencedToken(serviceAccount *v1.ServiceAccount) ( /* retry */ bool, error) {
if hasToken, err := e.hasReferencedToken(serviceAccount); err != nil {
// Don't retry cache lookup errors
return false, err
} else if hasToken {
// A service account token already exists, and is referenced, short-circuit
return false, nil
}
// We don't want to update the cache's copy of the service account
// so add the secret to a freshly retrieved copy of the service account
serviceAccounts := e.client.CoreV1().ServiceAccounts(serviceAccount.Namespace)
liveServiceAccount, err := serviceAccounts.Get(serviceAccount.Name, metav1.GetOptions{})
if err != nil {
// Retry if we cannot fetch the live service account (for a NotFound error, either the live lookup or our cache are stale)
return true, err
}
if liveServiceAccount.ResourceVersion != serviceAccount.ResourceVersion {
// Retry if our liveServiceAccount doesn't match our cache's resourceVersion (either the live lookup or our cache are stale)
glog.V(4).Infof("liveServiceAccount.ResourceVersion (%s) does not match cache (%s), retrying", liveServiceAccount.ResourceVersion, serviceAccount.ResourceVersion)
return true, nil
}
// Build the secret
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
Namespace: serviceAccount.Namespace,
Annotations: map[string]string{
v1.ServiceAccountNameKey: serviceAccount.Name,
v1.ServiceAccountUIDKey: string(serviceAccount.UID),
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{},
}
// Generate the token
token, err := e.token.GenerateToken(*serviceAccount, *secret)
if err != nil {
// retriable error
return true, err
}
secret.Data[v1.ServiceAccountTokenKey] = []byte(token)
secret.Data[v1.ServiceAccountNamespaceKey] = []byte(serviceAccount.Namespace)
if e.rootCA != nil && len(e.rootCA) > 0 {
secret.Data[v1.ServiceAccountRootCAKey] = e.rootCA
}
// Save the secret
createdToken, err := e.client.CoreV1().Secrets(serviceAccount.Namespace).Create(secret)
if err != nil {
// retriable error
return true, err
}
// Manually add the new token to the cache store.
// This prevents the service account update (below) triggering another token creation, if the referenced token couldn't be found in the store
e.updatedSecrets.Mutation(createdToken)
// Try to add a reference to the newly created token to the service account
addedReference := false
err = clientretry.RetryOnConflict(clientretry.DefaultRetry, func() error {
// refresh liveServiceAccount on every retry
defer func() { liveServiceAccount = nil }()
// fetch the live service account if needed, and verify the UID matches and that we still need a token
if liveServiceAccount == nil {
liveServiceAccount, err = serviceAccounts.Get(serviceAccount.Name, metav1.GetOptions{})
if err != nil {
return err
}
if liveServiceAccount.UID != serviceAccount.UID {
// If we don't have the same service account, stop trying to add a reference to the token made for the old service account.
return nil
}
if hasToken, err := e.hasReferencedToken(liveServiceAccount); err != nil {
// Don't retry cache lookup errors
return nil
} else if hasToken {
// A service account token already exists, and is referenced, short-circuit
return nil
}
}
// Try to add a reference to the token
liveServiceAccount.Secrets = append(liveServiceAccount.Secrets, v1.ObjectReference{Name: secret.Name})
if _, err := serviceAccounts.Update(liveServiceAccount); err != nil {
return err
}
addedReference = true
return nil
})
if !addedReference {
// we weren't able to use the token, try to clean it up.
glog.V(2).Infof("deleting secret %s/%s because reference couldn't be added (%v)", secret.Namespace, secret.Name, err)
deleteOpts := &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &createdToken.UID}}
if deleteErr := e.client.CoreV1().Secrets(createdToken.Namespace).Delete(createdToken.Name, deleteOpts); deleteErr != nil {
glog.Error(deleteErr) // if we fail, just log it
}
}
if err != nil {
if apierrors.IsConflict(err) || apierrors.IsNotFound(err) {
// if we got a Conflict error, the service account was updated by someone else, and we'll get an update notification later
// if we got a NotFound error, the service account no longer exists, and we don't need to create a token for it
return false, nil
}
// retry in all other cases
return true, err
}
// success!
return false, nil
}
// hasReferencedToken returns true if the serviceAccount references a service account token secret
func (e *TokensController) hasReferencedToken(serviceAccount *v1.ServiceAccount) (bool, error) {
if len(serviceAccount.Secrets) == 0 {
return false, nil
}
allSecrets, err := e.listTokenSecrets(serviceAccount)
if err != nil {
return false, err
}
referencedSecrets := getSecretReferences(serviceAccount)
for _, secret := range allSecrets {
if referencedSecrets.Has(secret.Name) {
return true, nil
}
}
return false, nil
}
func (e *TokensController) secretUpdateNeeded(secret *v1.Secret) (bool, bool, bool) {
caData := secret.Data[v1.ServiceAccountRootCAKey]
needsCA := len(e.rootCA) > 0 && bytes.Compare(caData, e.rootCA) != 0
needsNamespace := len(secret.Data[v1.ServiceAccountNamespaceKey]) == 0
tokenData := secret.Data[v1.ServiceAccountTokenKey]
needsToken := len(tokenData) == 0
return needsCA, needsNamespace, needsToken
}
// generateTokenIfNeeded populates the token data for the given Secret if not already set
func (e *TokensController) generateTokenIfNeeded(serviceAccount *v1.ServiceAccount, cachedSecret *v1.Secret) ( /* retry */ bool, error) {
// Check the cached secret to see if changes are needed
if needsCA, needsNamespace, needsToken := e.secretUpdateNeeded(cachedSecret); !needsCA && !needsToken && !needsNamespace {
return false, nil
}
// We don't want to update the cache's copy of the secret
// so add the token to a freshly retrieved copy of the secret
secrets := e.client.CoreV1().Secrets(cachedSecret.Namespace)
liveSecret, err := secrets.Get(cachedSecret.Name, metav1.GetOptions{})
if err != nil {
// Retry for any error other than a NotFound
return !apierrors.IsNotFound(err), err
}
if liveSecret.ResourceVersion != cachedSecret.ResourceVersion {
// our view of the secret is not up to date
// we'll get notified of an update event later and get to try again
glog.V(2).Infof("secret %s/%s is not up to date, skipping token population", liveSecret.Namespace, liveSecret.Name)
return false, nil
}
needsCA, needsNamespace, needsToken := e.secretUpdateNeeded(liveSecret)
if !needsCA && !needsToken && !needsNamespace {
return false, nil
}
if liveSecret.Annotations == nil {
liveSecret.Annotations = map[string]string{}
}
if liveSecret.Data == nil {
liveSecret.Data = map[string][]byte{}
}
// Set the CA
if needsCA {
liveSecret.Data[v1.ServiceAccountRootCAKey] = e.rootCA
}
// Set the namespace
if needsNamespace {
liveSecret.Data[v1.ServiceAccountNamespaceKey] = []byte(liveSecret.Namespace)
}
// Generate the token
if needsToken {
token, err := e.token.GenerateToken(*serviceAccount, *liveSecret)
if err != nil {
return false, err
}
liveSecret.Data[v1.ServiceAccountTokenKey] = []byte(token)
}
// Set annotations
liveSecret.Annotations[v1.ServiceAccountNameKey] = serviceAccount.Name
liveSecret.Annotations[v1.ServiceAccountUIDKey] = string(serviceAccount.UID)
// Save the secret
_, err = secrets.Update(liveSecret)
if apierrors.IsConflict(err) || apierrors.IsNotFound(err) {
// if we got a Conflict error, the secret was updated by someone else, and we'll get an update notification later
// if we got a NotFound error, the secret no longer exists, and we don't need to populate a token
return false, nil
}
if err != nil {
return true, err
}
return false, nil
}
// removeSecretReference updates the given ServiceAccount to remove a reference to the given secretName if needed.
func (e *TokensController) removeSecretReference(saNamespace string, saName string, saUID types.UID, secretName string) error {
// We don't want to update the cache's copy of the service account
// so remove the secret from a freshly retrieved copy of the service account
serviceAccounts := e.client.CoreV1().ServiceAccounts(saNamespace)
serviceAccount, err := serviceAccounts.Get(saName, metav1.GetOptions{})
// Ignore NotFound errors when attempting to remove a reference
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
// Short-circuit if the UID doesn't match
if len(saUID) > 0 && saUID != serviceAccount.UID {
return nil
}
// Short-circuit if the secret is no longer referenced
if !getSecretReferences(serviceAccount).Has(secretName) {
return nil
}
// Remove the secret
secrets := []v1.ObjectReference{}
for _, s := range serviceAccount.Secrets {
if s.Name != secretName {
secrets = append(secrets, s)
}
}
serviceAccount.Secrets = secrets
_, err = serviceAccounts.Update(serviceAccount)
// Ignore NotFound errors when attempting to remove a reference
if apierrors.IsNotFound(err) {
return nil
}
return err
}
func (e *TokensController) getServiceAccount(ns string, name string, uid types.UID, fetchOnCacheMiss bool) (*v1.ServiceAccount, error) {
// Look up in cache
sa, err := e.serviceAccounts.ServiceAccounts(ns).Get(name)
if err != nil && !apierrors.IsNotFound(err) {
return nil, err
}
if sa != nil {
// Ensure UID matches if given
if len(uid) == 0 || uid == sa.UID {
return sa, nil
}
}
if !fetchOnCacheMiss {
return nil, nil
}
// Live lookup
sa, err = e.client.CoreV1().ServiceAccounts(ns).Get(name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
// Ensure UID matches if given
if len(uid) == 0 || uid == sa.UID {
return sa, nil
}
return nil, nil
}
func (e *TokensController) getSecret(ns string, name string, uid types.UID, fetchOnCacheMiss bool) (*v1.Secret, error) {
// Look up in cache
obj, exists, err := e.updatedSecrets.GetByKey(makeCacheKey(ns, name))
if err != nil {
return nil, err
}
if exists {
secret, ok := obj.(*v1.Secret)
if !ok {
return nil, fmt.Errorf("expected *v1.Secret, got %#v", secret)
}
// Ensure UID matches if given
if len(uid) == 0 || uid == secret.UID {
return secret, nil
}
}
if !fetchOnCacheMiss {
return nil, nil
}
// Live lookup
secret, err := e.client.CoreV1().Secrets(ns).Get(name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
// Ensure UID matches if given
if len(uid) == 0 || uid == secret.UID {
return secret, nil
}
return nil, nil
}
// listTokenSecrets returns a list of all of the ServiceAccountToken secrets that
// reference the given service account's name and uid
func (e *TokensController) listTokenSecrets(serviceAccount *v1.ServiceAccount) ([]*v1.Secret, error) {
namespaceSecrets, err := e.updatedSecrets.ByIndex("namespace", serviceAccount.Namespace)
if err != nil {
return nil, err
}
items := []*v1.Secret{}
for _, obj := range namespaceSecrets {
secret := obj.(*v1.Secret)
if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
items = append(items, secret)
}
}
return items, nil
}
// serviceAccountNameAndUID is a helper method to get the ServiceAccount Name and UID from the given secret
// Returns "","" if the secret is not a ServiceAccountToken secret
// If the name or uid annotation is missing, "" is returned instead
func serviceAccountNameAndUID(secret *v1.Secret) (string, string) {
if secret.Type != v1.SecretTypeServiceAccountToken {
return "", ""
}
return secret.Annotations[v1.ServiceAccountNameKey], secret.Annotations[v1.ServiceAccountUIDKey]
}
func getSecretReferences(serviceAccount *v1.ServiceAccount) sets.String {
references := sets.NewString()
for _, secret := range serviceAccount.Secrets {
references.Insert(secret.Name)
}
return references
}
// serviceAccountQueueKey holds information we need to sync a service account.
// It contains enough information to look up the cached service account,
// or delete owned tokens if the service account no longer exists.
type serviceAccountQueueKey struct {
namespace string
name string
uid types.UID
}
func makeServiceAccountKey(sa *v1.ServiceAccount) interface{} {
return serviceAccountQueueKey{
namespace: sa.Namespace,
name: sa.Name,
uid: sa.UID,
}
}
func parseServiceAccountKey(key interface{}) (serviceAccountQueueKey, error) {
queueKey, ok := key.(serviceAccountQueueKey)
if !ok || len(queueKey.namespace) == 0 || len(queueKey.name) == 0 || len(queueKey.uid) == 0 {
return serviceAccountQueueKey{}, fmt.Errorf("invalid serviceaccount key: %#v", key)
}
return queueKey, nil
}
// secretQueueKey holds information we need to sync a service account token secret.
// It contains enough information to look up the cached service account,
// or delete the secret reference if the secret no longer exists.
type secretQueueKey struct {
namespace string
name string
uid types.UID
saName string
// optional, will be blank when syncing tokens missing the service account uid annotation
saUID types.UID
}
func makeSecretQueueKey(secret *v1.Secret) interface{} {
return secretQueueKey{
namespace: secret.Namespace,
name: secret.Name,
uid: secret.UID,
saName: secret.Annotations[v1.ServiceAccountNameKey],
saUID: types.UID(secret.Annotations[v1.ServiceAccountUIDKey]),
}
}
func parseSecretQueueKey(key interface{}) (secretQueueKey, error) {
queueKey, ok := key.(secretQueueKey)
if !ok || len(queueKey.namespace) == 0 || len(queueKey.name) == 0 || len(queueKey.uid) == 0 || len(queueKey.saName) == 0 {
return secretQueueKey{}, fmt.Errorf("invalid secret key: %#v", key)
}
return queueKey, nil
}
// produce the same key format as cache.MetaNamespaceKeyFunc
func makeCacheKey(namespace, name string) string {
return namespace + "/" + name
}

View File

@ -0,0 +1,694 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"errors"
"reflect"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/golang/glog"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/controller"
)
type testGenerator struct {
GeneratedServiceAccounts []v1.ServiceAccount
GeneratedSecrets []v1.Secret
Token string
Err error
}
func (t *testGenerator) GenerateToken(serviceAccount v1.ServiceAccount, secret v1.Secret) (string, error) {
t.GeneratedSecrets = append(t.GeneratedSecrets, secret)
t.GeneratedServiceAccounts = append(t.GeneratedServiceAccounts, serviceAccount)
return t.Token, t.Err
}
// emptySecretReferences is used by a service account without any secrets
func emptySecretReferences() []v1.ObjectReference {
return []v1.ObjectReference{}
}
// missingSecretReferences is used by a service account that references secrets which do no exist
func missingSecretReferences() []v1.ObjectReference {
return []v1.ObjectReference{{Name: "missing-secret-1"}}
}
// regularSecretReferences is used by a service account that references secrets which are not ServiceAccountTokens
func regularSecretReferences() []v1.ObjectReference {
return []v1.ObjectReference{{Name: "regular-secret-1"}}
}
// tokenSecretReferences is used by a service account that references a ServiceAccountToken secret
func tokenSecretReferences() []v1.ObjectReference {
return []v1.ObjectReference{{Name: "token-secret-1"}}
}
// addTokenSecretReference adds a reference to the ServiceAccountToken that will be created
func addTokenSecretReference(refs []v1.ObjectReference) []v1.ObjectReference {
return addNamedTokenSecretReference(refs, "default-token-xn8fg")
}
// addNamedTokenSecretReference adds a reference to the named ServiceAccountToken
func addNamedTokenSecretReference(refs []v1.ObjectReference, name string) []v1.ObjectReference {
return append(refs, v1.ObjectReference{Name: name})
}
// serviceAccount returns a service account with the given secret refs
func serviceAccount(secretRefs []v1.ObjectReference) *v1.ServiceAccount {
return &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
UID: "12345",
Namespace: "default",
ResourceVersion: "1",
},
Secrets: secretRefs,
}
}
// updatedServiceAccount returns a service account with the resource version modified
func updatedServiceAccount(secretRefs []v1.ObjectReference) *v1.ServiceAccount {
sa := serviceAccount(secretRefs)
sa.ResourceVersion = "2"
return sa
}
// opaqueSecret returns a persisted non-ServiceAccountToken secret named "regular-secret-1"
func opaqueSecret() *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "regular-secret-1",
Namespace: "default",
UID: "23456",
ResourceVersion: "1",
},
Type: "Opaque",
Data: map[string][]byte{
"mykey": []byte("mydata"),
},
}
}
// createdTokenSecret returns the ServiceAccountToken secret posted when creating a new token secret.
// Named "default-token-xn8fg", since that is the first generated name after rand.Seed(1)
func createdTokenSecret(overrideName ...string) *v1.Secret {
return namedCreatedTokenSecret("default-token-xn8fg")
}
// namedTokenSecret returns the ServiceAccountToken secret posted when creating a new token secret with the given name.
func namedCreatedTokenSecret(name string) *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
Annotations: map[string]string{
v1.ServiceAccountNameKey: "default",
v1.ServiceAccountUIDKey: "12345",
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
"token": []byte("ABC"),
"ca.crt": []byte("CA Data"),
"namespace": []byte("default"),
},
}
}
// serviceAccountTokenSecret returns an existing ServiceAccountToken secret named "token-secret-1"
func serviceAccountTokenSecret() *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "token-secret-1",
Namespace: "default",
UID: "23456",
ResourceVersion: "1",
Annotations: map[string]string{
v1.ServiceAccountNameKey: "default",
v1.ServiceAccountUIDKey: "12345",
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{
"token": []byte("ABC"),
"ca.crt": []byte("CA Data"),
"namespace": []byte("default"),
},
}
}
// serviceAccountTokenSecretWithoutTokenData returns an existing ServiceAccountToken secret that lacks token data
func serviceAccountTokenSecretWithoutTokenData() *v1.Secret {
secret := serviceAccountTokenSecret()
delete(secret.Data, v1.ServiceAccountTokenKey)
return secret
}
// serviceAccountTokenSecretWithoutCAData returns an existing ServiceAccountToken secret that lacks ca data
func serviceAccountTokenSecretWithoutCAData() *v1.Secret {
secret := serviceAccountTokenSecret()
delete(secret.Data, v1.ServiceAccountRootCAKey)
return secret
}
// serviceAccountTokenSecretWithCAData returns an existing ServiceAccountToken secret with the specified ca data
func serviceAccountTokenSecretWithCAData(data []byte) *v1.Secret {
secret := serviceAccountTokenSecret()
secret.Data[v1.ServiceAccountRootCAKey] = data
return secret
}
// serviceAccountTokenSecretWithoutNamespaceData returns an existing ServiceAccountToken secret that lacks namespace data
func serviceAccountTokenSecretWithoutNamespaceData() *v1.Secret {
secret := serviceAccountTokenSecret()
delete(secret.Data, v1.ServiceAccountNamespaceKey)
return secret
}
// serviceAccountTokenSecretWithNamespaceData returns an existing ServiceAccountToken secret with the specified namespace data
func serviceAccountTokenSecretWithNamespaceData(data []byte) *v1.Secret {
secret := serviceAccountTokenSecret()
secret.Data[v1.ServiceAccountNamespaceKey] = data
return secret
}
type reaction struct {
verb string
resource string
reactor func(t *testing.T) core.ReactionFunc
}
func TestTokenCreation(t *testing.T) {
testcases := map[string]struct {
ClientObjects []runtime.Object
IsAsync bool
MaxRetries int
Reactors []reaction
ExistingServiceAccount *v1.ServiceAccount
ExistingSecrets []*v1.Secret
AddedServiceAccount *v1.ServiceAccount
UpdatedServiceAccount *v1.ServiceAccount
DeletedServiceAccount *v1.ServiceAccount
AddedSecret *v1.Secret
AddedSecretLocal *v1.Secret
UpdatedSecret *v1.Secret
DeletedSecret *v1.Secret
ExpectedActions []core.Action
}{
"new serviceaccount with no secrets": {
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())},
AddedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(emptySecretReferences()))),
},
},
"new serviceaccount with no secrets encountering create error": {
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())},
MaxRetries: 10,
IsAsync: true,
Reactors: []reaction{{
verb: "create",
resource: "secrets",
reactor: func(t *testing.T) core.ReactionFunc {
i := 0
return func(core.Action) (bool, runtime.Object, error) {
i++
if i < 3 {
return true, nil, apierrors.NewForbidden(api.Resource("secrets"), "foo", errors.New("No can do"))
}
return false, nil, nil
}
},
}},
AddedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
// Attempt 1
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
// Attempt 2
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, namedCreatedTokenSecret("default-token-txhzt")),
// Attempt 3
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, namedCreatedTokenSecret("default-token-vnmz7")),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addNamedTokenSecretReference(emptySecretReferences(), "default-token-vnmz7"))),
},
},
"new serviceaccount with no secrets encountering unending create error": {
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()},
MaxRetries: 2,
IsAsync: true,
Reactors: []reaction{{
verb: "create",
resource: "secrets",
reactor: func(t *testing.T) core.ReactionFunc {
return func(core.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewForbidden(api.Resource("secrets"), "foo", errors.New("No can do"))
}
},
}},
AddedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
// Attempt
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
// Retry 1
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, namedCreatedTokenSecret("default-token-txhzt")),
// Retry 2
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, namedCreatedTokenSecret("default-token-vnmz7")),
},
},
"new serviceaccount with missing secrets": {
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
AddedServiceAccount: serviceAccount(missingSecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(missingSecretReferences()))),
},
},
"new serviceaccount with missing secrets and a local secret in the cache": {
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
AddedServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecretLocal: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{},
},
"new serviceaccount with non-token secrets": {
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()},
AddedServiceAccount: serviceAccount(regularSecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(regularSecretReferences()))),
},
},
"new serviceaccount with token secrets": {
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()},
ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
AddedServiceAccount: serviceAccount(tokenSecretReferences()),
ExpectedActions: []core.Action{},
},
"new serviceaccount with no secrets with resource conflict": {
ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences()), createdTokenSecret()},
IsAsync: true,
MaxRetries: 1,
AddedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
},
},
"updated serviceaccount with no secrets": {
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences())},
UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(emptySecretReferences()))),
},
},
"updated serviceaccount with missing secrets": {
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences())},
UpdatedServiceAccount: serviceAccount(missingSecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(missingSecretReferences()))),
},
},
"updated serviceaccount with non-token secrets": {
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), opaqueSecret()},
UpdatedServiceAccount: serviceAccount(regularSecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewCreateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, createdTokenSecret()),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(addTokenSecretReference(regularSecretReferences()))),
},
},
"updated serviceaccount with token secrets": {
ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
UpdatedServiceAccount: serviceAccount(tokenSecretReferences()),
ExpectedActions: []core.Action{},
},
"updated serviceaccount with no secrets with resource conflict": {
ClientObjects: []runtime.Object{updatedServiceAccount(emptySecretReferences())},
IsAsync: true,
MaxRetries: 1,
UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
},
},
"deleted serviceaccount with no secrets": {
DeletedServiceAccount: serviceAccount(emptySecretReferences()),
ExpectedActions: []core.Action{},
},
"deleted serviceaccount with missing secrets": {
DeletedServiceAccount: serviceAccount(missingSecretReferences()),
ExpectedActions: []core.Action{},
},
"deleted serviceaccount with non-token secrets": {
ClientObjects: []runtime.Object{opaqueSecret()},
DeletedServiceAccount: serviceAccount(regularSecretReferences()),
ExpectedActions: []core.Action{},
},
"deleted serviceaccount with token secrets": {
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
ExistingSecrets: []*v1.Secret{serviceAccountTokenSecret()},
DeletedServiceAccount: serviceAccount(tokenSecretReferences()),
ExpectedActions: []core.Action{
core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
},
},
"added secret without serviceaccount": {
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
AddedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
},
},
"added secret with serviceaccount": {
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{},
},
"added token secret without token data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecretWithoutTokenData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"added token secret without ca data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutCAData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecretWithoutCAData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"added token secret with mismatched ca data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"added token secret without namespace data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecretWithoutNamespaceData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"added token secret with custom namespace data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
AddedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")),
ExpectedActions: []core.Action{
// no update is performed... the custom namespace is preserved
},
},
"updated secret without serviceaccount": {
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
UpdatedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewDeleteAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
},
},
"updated secret with serviceaccount": {
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{},
},
"updated token secret without token data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"updated token secret without ca data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutCAData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecretWithoutCAData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"updated token secret with mismatched ca data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithCAData([]byte("mismatched"))},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecretWithCAData([]byte("mismatched")),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"updated token secret without namespace data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutNamespaceData()},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecretWithoutNamespaceData(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, "token-secret-1"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, metav1.NamespaceDefault, serviceAccountTokenSecret()),
},
},
"updated token secret with custom namespace data": {
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithNamespaceData([]byte("custom"))},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
UpdatedSecret: serviceAccountTokenSecretWithNamespaceData([]byte("custom")),
ExpectedActions: []core.Action{
// no update is performed... the custom namespace is preserved
},
},
"deleted secret without serviceaccount": {
DeletedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{},
},
"deleted secret with serviceaccount with reference": {
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences())},
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
DeletedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, serviceAccount(emptySecretReferences())),
},
},
"deleted secret with serviceaccount without reference": {
ExistingServiceAccount: serviceAccount(emptySecretReferences()),
DeletedSecret: serviceAccountTokenSecret(),
ExpectedActions: []core.Action{
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"}, metav1.NamespaceDefault, "default"),
},
},
}
for k, tc := range testcases {
glog.Infof(k)
// Re-seed to reset name generation
utilrand.Seed(1)
generator := &testGenerator{Token: "ABC"}
client := fake.NewSimpleClientset(tc.ClientObjects...)
for _, reactor := range tc.Reactors {
client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactor(t))
}
informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
secretInformer := informers.Core().V1().Secrets().Informer()
secrets := secretInformer.GetStore()
serviceAccounts := informers.Core().V1().ServiceAccounts().Informer().GetStore()
controller, err := NewTokensController(informers.Core().V1().ServiceAccounts(), informers.Core().V1().Secrets(), client, TokensControllerOptions{TokenGenerator: generator, RootCA: []byte("CA Data"), MaxRetries: tc.MaxRetries})
if err != nil {
t.Fatalf("error creating Tokens controller: %v", err)
}
if tc.ExistingServiceAccount != nil {
serviceAccounts.Add(tc.ExistingServiceAccount)
}
for _, s := range tc.ExistingSecrets {
secrets.Add(s)
}
if tc.AddedServiceAccount != nil {
serviceAccounts.Add(tc.AddedServiceAccount)
controller.queueServiceAccountSync(tc.AddedServiceAccount)
}
if tc.UpdatedServiceAccount != nil {
serviceAccounts.Add(tc.UpdatedServiceAccount)
controller.queueServiceAccountUpdateSync(nil, tc.UpdatedServiceAccount)
}
if tc.DeletedServiceAccount != nil {
serviceAccounts.Delete(tc.DeletedServiceAccount)
controller.queueServiceAccountSync(tc.DeletedServiceAccount)
}
if tc.AddedSecret != nil {
secrets.Add(tc.AddedSecret)
controller.queueSecretSync(tc.AddedSecret)
}
if tc.AddedSecretLocal != nil {
controller.updatedSecrets.Mutation(tc.AddedSecretLocal)
}
if tc.UpdatedSecret != nil {
secrets.Add(tc.UpdatedSecret)
controller.queueSecretUpdateSync(nil, tc.UpdatedSecret)
}
if tc.DeletedSecret != nil {
secrets.Delete(tc.DeletedSecret)
controller.queueSecretSync(tc.DeletedSecret)
}
// This is the longest we'll wait for async tests
timeout := time.Now().Add(30 * time.Second)
waitedForAdditionalActions := false
for {
if controller.syncServiceAccountQueue.Len() > 0 {
controller.syncServiceAccount()
}
if controller.syncSecretQueue.Len() > 0 {
controller.syncSecret()
}
// The queues still have things to work on
if controller.syncServiceAccountQueue.Len() > 0 || controller.syncSecretQueue.Len() > 0 {
continue
}
// If we expect this test to work asynchronously...
if tc.IsAsync {
// if we're still missing expected actions within our test timeout
if len(client.Actions()) < len(tc.ExpectedActions) && time.Now().Before(timeout) {
// wait for the expected actions (without hotlooping)
time.Sleep(time.Millisecond)
continue
}
// if we exactly match our expected actions, wait a bit to make sure no other additional actions show up
if len(client.Actions()) == len(tc.ExpectedActions) && !waitedForAdditionalActions {
time.Sleep(time.Second)
waitedForAdditionalActions = true
continue
}
}
break
}
if controller.syncServiceAccountQueue.Len() > 0 {
t.Errorf("%s: unexpected items in service account queue: %d", k, controller.syncServiceAccountQueue.Len())
}
if controller.syncSecretQueue.Len() > 0 {
t.Errorf("%s: unexpected items in secret queue: %d", k, controller.syncSecretQueue.Len())
}
actions := client.Actions()
for i, action := range actions {
if len(tc.ExpectedActions) < i+1 {
t.Errorf("%s: %d unexpected actions: %+v", k, len(actions)-len(tc.ExpectedActions), actions[i:])
break
}
expectedAction := tc.ExpectedActions[i]
if !reflect.DeepEqual(expectedAction, action) {
t.Errorf("%s:\nExpected:\n%s\ngot:\n%s", k, spew.Sdump(expectedAction), spew.Sdump(action))
continue
}
}
if len(tc.ExpectedActions) > len(actions) {
t.Errorf("%s: %d additional expected actions", k, len(tc.ExpectedActions)-len(actions))
for _, a := range tc.ExpectedActions[len(actions):] {
t.Logf(" %+v", a)
}
}
}
}