mirror of https://github.com/k3s-io/k3s
ServiceAccountTokens controller
parent
53d55f4192
commit
0955808668
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
@ -40,6 +41,7 @@ import (
|
|||
"github.com/GoogleCloudPlatform/kubernetes/pkg/namespace"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/service"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder"
|
||||
|
||||
|
@ -249,6 +251,12 @@ func (s *CMServer) Run(_ []string) error {
|
|||
pvclaimBinder.Run()
|
||||
}
|
||||
|
||||
// TODO: generate signed token
|
||||
tokenGenerator := serviceaccount.TokenGeneratorFunc(func(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
|
||||
return fmt.Sprintf("serviceaccount:%s:%s:%s:%s", serviceAccount.Namespace, serviceAccount.Name, serviceAccount.UID, secret.Name), nil
|
||||
})
|
||||
serviceaccount.NewTokensController(kubeClient, serviceaccount.DefaultTokenControllerOptions(tokenGenerator)).Run()
|
||||
|
||||
select {}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1843,6 +1843,8 @@ const (
|
|||
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
|
||||
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountTokenKey = "token"
|
||||
// ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets
|
||||
ServiceAccountKubeconfigKey = "kubernetes.kubeconfig"
|
||||
)
|
||||
|
||||
type SecretList struct {
|
||||
|
|
|
@ -228,3 +228,69 @@ func NewInformer(
|
|||
}
|
||||
return clientState, New(cfg)
|
||||
}
|
||||
|
||||
// NewIndexerInformer returns a cache.Indexer and a controller for populating the index
|
||||
// while also providing event notifications. You should only used the returned
|
||||
// cache.Index for Get/List operations; Add/Modify/Deletes will cause the event
|
||||
// notifications to be faulty.
|
||||
//
|
||||
// Parameters:
|
||||
// * lw is list and watch functions for the source of the resource you want to
|
||||
// be informed of.
|
||||
// * objType is an object of the type that you expect to receive.
|
||||
// * resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate
|
||||
// calls, even if nothing changed). Otherwise, re-list will be delayed as
|
||||
// long as possible (until the upstream source closes the watch or times out,
|
||||
// or you stop the controller).
|
||||
// * h is the object you want notifications sent to.
|
||||
//
|
||||
func NewIndexerInformer(
|
||||
lw cache.ListerWatcher,
|
||||
objType runtime.Object,
|
||||
resyncPeriod time.Duration,
|
||||
h ResourceEventHandler,
|
||||
indexers cache.Indexers,
|
||||
) (cache.Indexer, *Controller) {
|
||||
// This will hold the client state, as we know it.
|
||||
clientState := cache.NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)
|
||||
|
||||
// This will hold incoming changes. Note how we pass clientState in as a
|
||||
// KeyLister, that way resync operations will result in the correct set
|
||||
// of update/delete deltas.
|
||||
fifo := cache.NewDeltaFIFO(cache.MetaNamespaceKeyFunc, nil, clientState)
|
||||
|
||||
cfg := &Config{
|
||||
Queue: fifo,
|
||||
ListerWatcher: lw,
|
||||
ObjectType: objType,
|
||||
FullResyncPeriod: resyncPeriod,
|
||||
RetryOnError: false,
|
||||
|
||||
Process: func(obj interface{}) error {
|
||||
// from oldest to newest
|
||||
for _, d := range obj.(cache.Deltas) {
|
||||
switch d.Type {
|
||||
case cache.Sync, cache.Added, cache.Updated:
|
||||
if old, exists, err := clientState.Get(d.Object); err == nil && exists {
|
||||
if err := clientState.Update(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnUpdate(old, d.Object)
|
||||
} else {
|
||||
if err := clientState.Add(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnAdd(d.Object)
|
||||
}
|
||||
case cache.Deleted:
|
||||
if err := clientState.Delete(d.Object); err != nil {
|
||||
return err
|
||||
}
|
||||
h.OnDelete(d.Object)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return clientState, New(cfg)
|
||||
}
|
||||
|
|
|
@ -107,6 +107,10 @@ func finalize(kubeClient client.Interface, namespace api.Namespace) (*api.Namesp
|
|||
|
||||
// deleteAllContent will delete all content known to the system in a namespace
|
||||
func deleteAllContent(kubeClient client.Interface, namespace string) (err error) {
|
||||
err = deleteServiceAccounts(kubeClient, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = deleteServices(kubeClient, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -217,6 +221,20 @@ func deleteResourceQuotas(kubeClient client.Interface, ns string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func deleteServiceAccounts(kubeClient client.Interface, ns string) error {
|
||||
items, err := kubeClient.ServiceAccounts(ns).List(labels.Everything(), fields.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range items.Items {
|
||||
err := kubeClient.ServiceAccounts(ns).Delete(items.Items[i].Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteServices(kubeClient client.Interface, ns string) error {
|
||||
items, err := kubeClient.Services(ns).List(labels.Everything())
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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
|
|
@ -0,0 +1,442 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
// TokensControllerOptions contains options for the TokensController
|
||||
type TokensControllerOptions struct {
|
||||
// TokenGenerator is the generator to use to create new tokens
|
||||
TokenGenerator 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
|
||||
}
|
||||
|
||||
// DefaultTokenControllerOptions returns
|
||||
func DefaultTokenControllerOptions(tokenGenerator TokenGenerator) TokensControllerOptions {
|
||||
return TokensControllerOptions{TokenGenerator: tokenGenerator}
|
||||
}
|
||||
|
||||
// NewTokensController returns a new *TokensController.
|
||||
func NewTokensController(cl client.Interface, options TokensControllerOptions) *TokensController {
|
||||
e := &TokensController{
|
||||
client: cl,
|
||||
token: options.TokenGenerator,
|
||||
}
|
||||
|
||||
e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), fields.Everything())
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), fields.Everything(), rv)
|
||||
},
|
||||
},
|
||||
&api.ServiceAccount{},
|
||||
options.ServiceAccountResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.serviceAccountAdded,
|
||||
UpdateFunc: e.serviceAccountUpdated,
|
||||
DeleteFunc: e.serviceAccountDeleted,
|
||||
},
|
||||
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
|
||||
)
|
||||
|
||||
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
|
||||
e.secrets, e.secretController = framework.NewIndexerInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func() (runtime.Object, error) {
|
||||
return e.client.Secrets(api.NamespaceAll).List(labels.Everything(), tokenSelector)
|
||||
},
|
||||
WatchFunc: func(rv string) (watch.Interface, error) {
|
||||
return e.client.Secrets(api.NamespaceAll).Watch(labels.Everything(), tokenSelector, rv)
|
||||
},
|
||||
},
|
||||
&api.Secret{},
|
||||
options.SecretResync,
|
||||
framework.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.secretAdded,
|
||||
UpdateFunc: e.secretUpdated,
|
||||
DeleteFunc: e.secretDeleted,
|
||||
},
|
||||
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
|
||||
)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// TokensController manages ServiceAccountToken secrets for ServiceAccount objects
|
||||
type TokensController struct {
|
||||
stopChan chan struct{}
|
||||
|
||||
client client.Interface
|
||||
token TokenGenerator
|
||||
|
||||
serviceAccounts cache.Indexer
|
||||
secrets cache.Indexer
|
||||
|
||||
// Since we join two objects, we'll watch both of them with controllers.
|
||||
serviceAccountController *framework.Controller
|
||||
secretController *framework.Controller
|
||||
}
|
||||
|
||||
// Runs controller loops and returns immediately
|
||||
func (e *TokensController) Run() {
|
||||
if e.stopChan == nil {
|
||||
e.stopChan = make(chan struct{})
|
||||
go e.serviceAccountController.Run(e.stopChan)
|
||||
go e.secretController.Run(e.stopChan)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down this controller
|
||||
func (e *TokensController) Stop() {
|
||||
if e.stopChan != nil {
|
||||
close(e.stopChan)
|
||||
e.stopChan = nil
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountAdded reacts to a ServiceAccount creation by creating a corresponding ServiceAccountToken Secret
|
||||
func (e *TokensController) serviceAccountAdded(obj interface{}) {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
err := e.createSecretIfNeeded(serviceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountUpdated reacts to a ServiceAccount update (or re-list) by ensuring a corresponding ServiceAccountToken Secret exists
|
||||
func (e *TokensController) serviceAccountUpdated(oldObj interface{}, newObj interface{}) {
|
||||
newServiceAccount := newObj.(*api.ServiceAccount)
|
||||
err := e.createSecretIfNeeded(newServiceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountDeleted reacts to a ServiceAccount deletion by deleting all corresponding ServiceAccountToken Secrets
|
||||
func (e *TokensController) serviceAccountDeleted(obj interface{}) {
|
||||
serviceAccount, ok := obj.(*api.ServiceAccount)
|
||||
if !ok {
|
||||
// Unknown type. If we missed a ServiceAccount deletion, the
|
||||
// corresponding secrets will be cleaned up during the Secret re-list
|
||||
return
|
||||
}
|
||||
secrets, err := e.listTokenSecrets(serviceAccount)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
for _, secret := range secrets {
|
||||
if err := e.deleteSecret(secret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// secretAdded reacts to a Secret create by ensuring the referenced ServiceAccount exists, and by adding a token to the secret if needed
|
||||
func (e *TokensController) secretAdded(obj interface{}) {
|
||||
secret := obj.(*api.Secret)
|
||||
serviceAccount, err := e.getServiceAccount(secret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if serviceAccount == nil {
|
||||
if err := e.deleteSecret(secret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", secret.Namespace, secret.Name, err)
|
||||
}
|
||||
} else {
|
||||
e.generateTokenIfNeeded(serviceAccount, secret)
|
||||
}
|
||||
}
|
||||
|
||||
// secretUpdated reacts to a Secret update (or re-list) by deleting the secret (if the referenced ServiceAccount does not exist)
|
||||
func (e *TokensController) secretUpdated(oldObj interface{}, newObj interface{}) {
|
||||
newSecret := newObj.(*api.Secret)
|
||||
newServiceAccount, err := e.getServiceAccount(newSecret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if newServiceAccount == nil {
|
||||
if err := e.deleteSecret(newSecret); err != nil {
|
||||
glog.Errorf("Error deleting secret %s/%s: %v", newSecret.Namespace, newSecret.Name, err)
|
||||
}
|
||||
} else {
|
||||
e.generateTokenIfNeeded(newServiceAccount, newSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// secretDeleted reacts to a Secret being deleted by removing a reference from the corresponding ServiceAccount if needed
|
||||
func (e *TokensController) secretDeleted(obj interface{}) {
|
||||
secret, ok := obj.(*api.Secret)
|
||||
if !ok {
|
||||
// Unknown type. If we missed a Secret deletion, the corresponding ServiceAccount (if it exists)
|
||||
// will get a secret recreated (if needed) during the ServiceAccount re-list
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount, err := e.getServiceAccount(secret)
|
||||
if err != nil {
|
||||
glog.Error(err)
|
||||
return
|
||||
}
|
||||
if serviceAccount == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := e.removeSecretReferenceIfNeeded(serviceAccount, secret.Name); err != nil {
|
||||
glog.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createSecretIfNeeded makes sure at least one ServiceAccountToken secret exists, and is included in the serviceAccount's Secrets list
|
||||
func (e *TokensController) createSecretIfNeeded(serviceAccount *api.ServiceAccount) error {
|
||||
// If the service account references no secrets, short-circuit and create a new one
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return e.createSecret(serviceAccount)
|
||||
}
|
||||
|
||||
// If any existing token secrets are referenced by the service account, return
|
||||
allSecrets, err := e.listTokenSecrets(serviceAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
referencedSecrets := getSecretReferences(serviceAccount)
|
||||
for _, secret := range allSecrets {
|
||||
if referencedSecrets.Has(secret.Name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create a new token secret
|
||||
return e.createSecret(serviceAccount)
|
||||
}
|
||||
|
||||
// createSecret creates a secret of type ServiceAccountToken for the given ServiceAccount
|
||||
func (e *TokensController) createSecret(serviceAccount *api.ServiceAccount) error {
|
||||
// Build the secret
|
||||
secret := &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
|
||||
Namespace: serviceAccount.Namespace,
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: serviceAccount.Name,
|
||||
api.ServiceAccountUIDKey: string(serviceAccount.UID),
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{},
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
token, err := e.token.GenerateToken(*serviceAccount, *secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Data[api.ServiceAccountTokenKey] = []byte(token)
|
||||
|
||||
// Save the secret
|
||||
if _, err := e.client.Secrets(serviceAccount.Namespace).Create(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.ServiceAccounts(serviceAccount.Namespace)
|
||||
serviceAccount, err = serviceAccounts.Get(serviceAccount.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceAccount.Secrets = append(serviceAccount.Secrets, api.ObjectReference{Name: secret.Name})
|
||||
|
||||
_, err = serviceAccounts.Update(serviceAccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTokenIfNeeded populates the token data for the given Secret if not already set
|
||||
func (e *TokensController) generateTokenIfNeeded(serviceAccount *api.ServiceAccount, secret *api.Secret) error {
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = map[string]string{}
|
||||
}
|
||||
if secret.Data == nil {
|
||||
secret.Data = map[string][]byte{}
|
||||
}
|
||||
|
||||
tokenData, ok := secret.Data[api.ServiceAccountTokenKey]
|
||||
if ok && len(tokenData) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
token, err := e.token.GenerateToken(*serviceAccount, *secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the token and annotations
|
||||
secret.Data[api.ServiceAccountTokenKey] = []byte(token)
|
||||
secret.Annotations[api.ServiceAccountNameKey] = serviceAccount.Name
|
||||
secret.Annotations[api.ServiceAccountUIDKey] = string(serviceAccount.UID)
|
||||
|
||||
// Save the secret
|
||||
if _, err := e.client.Secrets(secret.Namespace).Update(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteSecret deletes the given secret
|
||||
func (e *TokensController) deleteSecret(secret *api.Secret) error {
|
||||
return e.client.Secrets(secret.Namespace).Delete(secret.Name)
|
||||
}
|
||||
|
||||
// removeSecretReferenceIfNeeded updates the given ServiceAccount to remove a reference to the given secretName if needed.
|
||||
// Returns whether an update was performed, and any error that occurred
|
||||
func (e *TokensController) removeSecretReferenceIfNeeded(serviceAccount *api.ServiceAccount, secretName string) (bool, error) {
|
||||
// See if the account even referenced the secret
|
||||
if !getSecretReferences(serviceAccount).Has(secretName) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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.ServiceAccounts(serviceAccount.Namespace)
|
||||
serviceAccount, err := serviceAccounts.Get(serviceAccount.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Double-check to see if the account still references the secret
|
||||
if !getSecretReferences(serviceAccount).Has(secretName) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
secrets := []api.ObjectReference{}
|
||||
for _, s := range serviceAccount.Secrets {
|
||||
if s.Name != secretName {
|
||||
secrets = append(secrets, s)
|
||||
}
|
||||
}
|
||||
serviceAccount.Secrets = secrets
|
||||
|
||||
_, err = serviceAccounts.Update(serviceAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getServiceAccount returns the ServiceAccount referenced by the given secret. If the secret is not
|
||||
// of type ServiceAccountToken, or if the referenced ServiceAccount does not exist, nil is returned
|
||||
func (e *TokensController) getServiceAccount(secret *api.Secret) (*api.ServiceAccount, error) {
|
||||
name, uid := serviceAccountNameAndUID(secret)
|
||||
if len(name) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: secret.Namespace}}
|
||||
namespaceAccounts, err := e.serviceAccounts.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, obj := range namespaceAccounts {
|
||||
serviceAccount := obj.(*api.ServiceAccount)
|
||||
if name != serviceAccount.Name {
|
||||
// Name must match
|
||||
continue
|
||||
}
|
||||
if len(uid) > 0 && uid != string(serviceAccount.UID) {
|
||||
// If UID is specified, it must match
|
||||
continue
|
||||
}
|
||||
return serviceAccount, 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 *api.ServiceAccount) ([]*api.Secret, error) {
|
||||
key := &api.Secret{ObjectMeta: api.ObjectMeta{Namespace: serviceAccount.Namespace}}
|
||||
namespaceSecrets, err := e.secrets.Index("namespace", key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := []*api.Secret{}
|
||||
for _, obj := range namespaceSecrets {
|
||||
secret := obj.(*api.Secret)
|
||||
name, uid := serviceAccountNameAndUID(secret)
|
||||
if name != serviceAccount.Name {
|
||||
// Name must match
|
||||
continue
|
||||
}
|
||||
if len(uid) > 0 && uid != string(serviceAccount.UID) {
|
||||
// If UID is specified, it must match
|
||||
continue
|
||||
}
|
||||
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 *api.Secret) (string, string) {
|
||||
if secret.Type != api.SecretTypeServiceAccountToken {
|
||||
return "", ""
|
||||
}
|
||||
return secret.Annotations[api.ServiceAccountNameKey], secret.Annotations[api.ServiceAccountUIDKey]
|
||||
}
|
||||
|
||||
func getSecretReferences(serviceAccount *api.ServiceAccount) util.StringSet {
|
||||
references := util.NewStringSet()
|
||||
for _, secret := range serviceAccount.Secrets {
|
||||
references.Insert(secret.Name)
|
||||
}
|
||||
return references
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
|
||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||
)
|
||||
|
||||
type testGenerator struct {
|
||||
GeneratedServiceAccounts []api.ServiceAccount
|
||||
GeneratedSecrets []api.Secret
|
||||
Token string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (t *testGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.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() []api.ObjectReference {
|
||||
return []api.ObjectReference{}
|
||||
}
|
||||
|
||||
// missingSecretReferences is used by a service account that references secrets which do no exist
|
||||
func missingSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "missing-secret-1"}}
|
||||
}
|
||||
|
||||
// regularSecretReferences is used by a service account that references secrets which are not ServiceAccountTokens
|
||||
func regularSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "regular-secret-1"}}
|
||||
}
|
||||
|
||||
// tokenSecretReferences is used by a service account that references a ServiceAccountToken secret
|
||||
func tokenSecretReferences() []api.ObjectReference {
|
||||
return []api.ObjectReference{{Name: "token-secret-1"}}
|
||||
}
|
||||
|
||||
// addTokenSecretReference adds a reference to the ServiceAccountToken that will be created
|
||||
func addTokenSecretReference(refs []api.ObjectReference) []api.ObjectReference {
|
||||
return append(refs, api.ObjectReference{Name: "default-token-fplln"})
|
||||
}
|
||||
|
||||
// serviceAccount returns a service account with the given secret refs
|
||||
func serviceAccount(secretRefs []api.ObjectReference) *api.ServiceAccount {
|
||||
return &api.ServiceAccount{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default",
|
||||
UID: "12345",
|
||||
Namespace: "default",
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Secrets: secretRefs,
|
||||
}
|
||||
}
|
||||
|
||||
// opaqueSecret returns a persisted non-ServiceAccountToken secret named "regular-secret-1"
|
||||
func opaqueSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.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-fplln", since that is the first generated name after rand.Seed(1)
|
||||
func createdTokenSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "default-token-fplln",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: "default",
|
||||
api.ServiceAccountUIDKey: "12345",
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"token": []byte("ABC"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountTokenSecret returns an existing ServiceAccountToken secret named "token-secret-1"
|
||||
func serviceAccountTokenSecret() *api.Secret {
|
||||
return &api.Secret{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "token-secret-1",
|
||||
Namespace: "default",
|
||||
UID: "23456",
|
||||
ResourceVersion: "1",
|
||||
Annotations: map[string]string{
|
||||
api.ServiceAccountNameKey: "default",
|
||||
api.ServiceAccountUIDKey: "12345",
|
||||
},
|
||||
},
|
||||
Type: api.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"token": []byte("ABC"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// serviceAccountTokenSecretWithoutTokenData returns an existing ServiceAccountToken secret that lacks token data
|
||||
func serviceAccountTokenSecretWithoutTokenData() *api.Secret {
|
||||
secret := serviceAccountTokenSecret()
|
||||
secret.Data = nil
|
||||
return secret
|
||||
}
|
||||
|
||||
func TestTokenCreation(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
ClientObjects []runtime.Object
|
||||
|
||||
ExistingServiceAccount *api.ServiceAccount
|
||||
ExistingSecrets []*api.Secret
|
||||
|
||||
AddedServiceAccount *api.ServiceAccount
|
||||
UpdatedServiceAccount *api.ServiceAccount
|
||||
DeletedServiceAccount *api.ServiceAccount
|
||||
AddedSecret *api.Secret
|
||||
UpdatedSecret *api.Secret
|
||||
DeletedSecret *api.Secret
|
||||
|
||||
ExpectedActions []testclient.FakeAction
|
||||
}{
|
||||
"new serviceaccount with no secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with missing secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"new serviceaccount with token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences()), serviceAccountTokenSecret()},
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
AddedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
|
||||
"updated serviceaccount with no secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(emptySecretReferences()), createdTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(emptySecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with missing secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(missingSecretReferences()), createdTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(missingSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(regularSecretReferences()), createdTokenSecret(), opaqueSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "create-secret", Value: createdTokenSecret()},
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(addTokenSecretReference(regularSecretReferences()))},
|
||||
},
|
||||
},
|
||||
"updated serviceaccount with token secrets": {
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
UpdatedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
|
||||
"deleted serviceaccount with no secrets": {
|
||||
DeletedServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with missing secrets": {
|
||||
DeletedServiceAccount: serviceAccount(missingSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with non-token secrets": {
|
||||
ClientObjects: []runtime.Object{opaqueSecret()},
|
||||
|
||||
DeletedServiceAccount: serviceAccount(regularSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted serviceaccount with token secrets": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
ExistingSecrets: []*api.Secret{serviceAccountTokenSecret()},
|
||||
|
||||
DeletedServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
|
||||
"added secret without serviceaccount": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
|
||||
AddedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
"added secret with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
AddedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"added token secret without token data": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
AddedSecret: serviceAccountTokenSecretWithoutTokenData(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "update-secret", Value: serviceAccountTokenSecret()},
|
||||
},
|
||||
},
|
||||
|
||||
"updated secret without serviceaccount": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecret()},
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "delete-secret", Value: "token-secret-1"},
|
||||
},
|
||||
},
|
||||
"updated secret with serviceaccount": {
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"updated token secret without token data": {
|
||||
ClientObjects: []runtime.Object{serviceAccountTokenSecretWithoutTokenData()},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
UpdatedSecret: serviceAccountTokenSecretWithoutTokenData(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "update-secret", Value: serviceAccountTokenSecret()},
|
||||
},
|
||||
},
|
||||
|
||||
"deleted secret without serviceaccount": {
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
"deleted secret with serviceaccount with reference": {
|
||||
ClientObjects: []runtime.Object{serviceAccount(tokenSecretReferences())},
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{
|
||||
{Action: "get-serviceaccount", Value: "default"},
|
||||
{Action: "update-serviceaccount", Value: serviceAccount(emptySecretReferences())},
|
||||
},
|
||||
},
|
||||
"deleted secret with serviceaccount without reference": {
|
||||
ExistingServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
|
||||
DeletedSecret: serviceAccountTokenSecret(),
|
||||
ExpectedActions: []testclient.FakeAction{},
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
|
||||
// Re-seed to reset name generation
|
||||
rand.Seed(1)
|
||||
|
||||
generator := &testGenerator{Token: "ABC"}
|
||||
|
||||
client := testclient.NewSimpleFake(tc.ClientObjects...)
|
||||
|
||||
controller := NewTokensController(client, DefaultTokenControllerOptions(generator))
|
||||
|
||||
if tc.ExistingServiceAccount != nil {
|
||||
controller.serviceAccounts.Add(tc.ExistingServiceAccount)
|
||||
}
|
||||
for _, s := range tc.ExistingSecrets {
|
||||
controller.secrets.Add(s)
|
||||
}
|
||||
|
||||
if tc.AddedServiceAccount != nil {
|
||||
controller.serviceAccountAdded(tc.AddedServiceAccount)
|
||||
}
|
||||
if tc.UpdatedServiceAccount != nil {
|
||||
controller.serviceAccountUpdated(nil, tc.UpdatedServiceAccount)
|
||||
}
|
||||
if tc.DeletedServiceAccount != nil {
|
||||
controller.serviceAccountDeleted(tc.DeletedServiceAccount)
|
||||
}
|
||||
if tc.AddedSecret != nil {
|
||||
controller.secretAdded(tc.AddedSecret)
|
||||
}
|
||||
if tc.UpdatedSecret != nil {
|
||||
controller.secretUpdated(nil, tc.UpdatedSecret)
|
||||
}
|
||||
if tc.DeletedSecret != nil {
|
||||
controller.secretDeleted(tc.DeletedSecret)
|
||||
}
|
||||
|
||||
for i, action := range client.Actions {
|
||||
if len(tc.ExpectedActions) < i+1 {
|
||||
t.Errorf("%s: %d unexpected actions: %+v", k, len(client.Actions)-len(tc.ExpectedActions), client.Actions[i:])
|
||||
break
|
||||
}
|
||||
|
||||
expectedAction := tc.ExpectedActions[i]
|
||||
if expectedAction.Action != action.Action {
|
||||
t.Errorf("%s: Expected %s, got %s", k, expectedAction.Action, action.Action)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(expectedAction.Value, action.Value) {
|
||||
t.Errorf("%s: Expected\n\t%#v\ngot\n\t%#v", k, expectedAction.Value, action.Value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(tc.ExpectedActions) > len(client.Actions) {
|
||||
t.Errorf("%s: %d additional expected actions:%+v", k, len(tc.ExpectedActions)-len(client.Actions), tc.ExpectedActions[len(client.Actions):])
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue