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 }