github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/caasrbacmapper/mapper.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package caasrbacmapper
     5  
     6  import (
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/worker/v3"
    12  	"github.com/juju/worker/v3/catacomb"
    13  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    14  	"k8s.io/apimachinery/pkg/types"
    15  	"k8s.io/apimachinery/pkg/util/wait"
    16  	core "k8s.io/client-go/informers/core/v1"
    17  	"k8s.io/client-go/tools/cache"
    18  	"k8s.io/client-go/util/workqueue"
    19  
    20  	"github.com/juju/juju/caas/kubernetes/provider"
    21  )
    22  
    23  // Mapper describes an interface for mapping k8s service account UID's to juju
    24  // application names.
    25  type Mapper interface {
    26  	// AppNameForServiceAccount fetches the juju application name associated
    27  	// with a given kubernetes service account UID. If no result is found
    28  	// errors.NotFound is returned. All other errors should be considered
    29  	// internal to the interface operation.
    30  	AppNameForServiceAccount(types.UID) (string, error)
    31  }
    32  
    33  // MapperWorker is a Mapper that also implements the worker interface
    34  type MapperWorker interface {
    35  	Mapper
    36  	worker.Worker
    37  }
    38  
    39  // DefaultMapper is a default implementation of the MapperWorker interface. It's
    40  // responsible for watching ServiceAccounts on a given model and
    41  type DefaultMapper struct {
    42  	catacomb     catacomb.Catacomb
    43  	lock         *sync.RWMutex
    44  	logger       Logger
    45  	saInformer   core.ServiceAccountInformer
    46  	saNameUIDMap map[string]types.UID
    47  	saUIDAppMap  map[types.UID]string
    48  	workQueue    workqueue.RateLimitingInterface
    49  }
    50  
    51  // AppNameForServiceAccount implements Mapper interface
    52  func (d *DefaultMapper) AppNameForServiceAccount(id types.UID) (string, error) {
    53  	d.lock.RLock()
    54  	defer d.lock.RUnlock()
    55  	appName, found := d.saUIDAppMap[id]
    56  	if !found {
    57  		return "", errors.NotFoundf("service account for app with id %v", id)
    58  	}
    59  	return appName, nil
    60  }
    61  
    62  func (d *DefaultMapper) enqueueServiceAccount(obj interface{}) {
    63  	key, err := cache.MetaNamespaceKeyFunc(obj)
    64  	if err != nil {
    65  		d.logger.Errorf("failed enqueuing service account: %v", err)
    66  		return
    67  	}
    68  	d.workQueue.Add(key)
    69  }
    70  
    71  // Kill implements Kill() from the Worker interface
    72  func (d *DefaultMapper) Kill() {
    73  	d.catacomb.Kill(nil)
    74  }
    75  
    76  func (d *DefaultMapper) loop() error {
    77  	defer d.workQueue.ShutDown()
    78  
    79  	go d.saInformer.Informer().Run(d.catacomb.Dying())
    80  
    81  	if ok := cache.WaitForCacheSync(
    82  		d.catacomb.Dying(), d.saInformer.Informer().HasSynced); !ok {
    83  		return errors.New("failed to wait for cache to sync")
    84  	}
    85  
    86  	// Wait until runs the below for loop every one second until the catacomb
    87  	// dies. The for loop processes all items in the queue till it's empty and
    88  	// the cycle repeats. This is to stop checking thrashing about and is the
    89  	// prescribed k8s way to process.
    90  	go wait.Until(func() {
    91  		for d.processNextQueueItem() {
    92  		}
    93  	}, time.Second, d.catacomb.Dying())
    94  
    95  	select {
    96  	case <-d.catacomb.Dying():
    97  		return d.catacomb.ErrDying()
    98  	}
    99  }
   100  
   101  // NewMapper constructs a new DefaultMapper for the supplied logger and
   102  // ServiceAccountInformer
   103  func NewMapper(logger Logger, informer core.ServiceAccountInformer) (*DefaultMapper, error) {
   104  	dm := &DefaultMapper{
   105  		lock:         new(sync.RWMutex),
   106  		logger:       logger,
   107  		saInformer:   informer,
   108  		saNameUIDMap: map[string]types.UID{},
   109  		saUIDAppMap:  map[types.UID]string{},
   110  		workQueue:    workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
   111  	}
   112  
   113  	_, err := dm.saInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   114  		AddFunc:    dm.enqueueServiceAccount,
   115  		DeleteFunc: dm.enqueueServiceAccount,
   116  		UpdateFunc: func(_, newObj interface{}) {
   117  			dm.enqueueServiceAccount(newObj)
   118  		},
   119  	})
   120  	if err != nil {
   121  		return nil, errors.Trace(err)
   122  	}
   123  
   124  	if err := catacomb.Invoke(catacomb.Plan{
   125  		Site: &dm.catacomb,
   126  		Work: dm.loop,
   127  	}); err != nil {
   128  		return dm, errors.Trace(err)
   129  	}
   130  	return dm, nil
   131  }
   132  
   133  func (d *DefaultMapper) processNextQueueItem() bool {
   134  	obj, shutdown := d.workQueue.Get()
   135  	if shutdown {
   136  		return false
   137  	}
   138  
   139  	defer d.workQueue.Done(obj)
   140  	key, ok := obj.(string)
   141  	if !ok {
   142  		d.workQueue.Forget(obj)
   143  		d.logger.Errorf("failed converting service account queue item to string")
   144  		return true
   145  	}
   146  
   147  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   148  	if err != nil {
   149  		d.workQueue.Forget(obj)
   150  		d.logger.Errorf("failed spliting key into namespace and name for service account queue: %v", err)
   151  		return true
   152  	}
   153  
   154  	sa, err := d.saInformer.Lister().ServiceAccounts(namespace).Get(name)
   155  	if k8serrors.IsNotFound(err) {
   156  		d.lock.Lock()
   157  		defer d.lock.Unlock()
   158  		uid, exists := d.saNameUIDMap[key]
   159  		if !exists {
   160  			return true
   161  		}
   162  		delete(d.saUIDAppMap, uid)
   163  		delete(d.saNameUIDMap, name)
   164  		return true
   165  	}
   166  	if err != nil {
   167  		d.workQueue.Forget(obj)
   168  		d.logger.Errorf("failed fetching service account for %s/%s", namespace, name)
   169  		return true
   170  	}
   171  
   172  	appName, err := provider.AppNameForServiceAccount(sa)
   173  	if errors.IsNotFound(err) {
   174  		return true
   175  	} else if err != nil {
   176  		d.logger.Errorf("failure getting app name for service account: %v", err)
   177  		return true
   178  	}
   179  
   180  	d.lock.Lock()
   181  	defer d.lock.Unlock()
   182  	d.saNameUIDMap[key] = sa.UID
   183  	d.saUIDAppMap[sa.UID] = appName
   184  	return true
   185  }
   186  
   187  // Wait implements Wait() from the Worker interface
   188  func (d *DefaultMapper) Wait() error {
   189  	return d.catacomb.Wait()
   190  }