github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/secretbackendrotate/rotate.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package secretbackendrotate 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/juju/clock" 11 "github.com/juju/errors" 12 "github.com/juju/worker/v3" 13 "github.com/juju/worker/v3/catacomb" 14 15 "github.com/juju/juju/core/watcher" 16 ) 17 18 // logger is here to stop the desire of creating a package level logger. 19 // Don't do this, instead use the one passed as manifold config. 20 type logger interface{} 21 22 var _ logger = struct{}{} 23 24 // Logger represents the methods used by the worker to log information. 25 type Logger interface { 26 Debugf(string, ...interface{}) 27 } 28 29 // SecretBackendManagerFacade instances provide a watcher for secret rotation changes. 30 type SecretBackendManagerFacade interface { 31 WatchTokenRotationChanges() (watcher.SecretBackendRotateWatcher, error) 32 RotateBackendTokens(info ...string) error 33 } 34 35 // Config defines the operation of the Worker. 36 type Config struct { 37 SecretBackendManagerFacade SecretBackendManagerFacade 38 Logger Logger 39 Clock clock.Clock 40 } 41 42 // Validate returns an error if config cannot drive the Worker. 43 func (config Config) Validate() error { 44 if config.SecretBackendManagerFacade == nil { 45 return errors.NotValidf("nil Facade") 46 } 47 if config.Clock == nil { 48 return errors.NotValidf("nil Clock") 49 } 50 if config.Logger == nil { 51 return errors.NotValidf("nil Logger") 52 } 53 return nil 54 } 55 56 // NewWorker returns a Secret Backend token rotation Worker backed by config, or an error. 57 func NewWorker(config Config) (worker.Worker, error) { 58 if err := config.Validate(); err != nil { 59 return nil, errors.Trace(err) 60 } 61 62 w := &Worker{ 63 config: config, 64 backendInfo: make(map[string]tokenRotateInfo), 65 } 66 err := catacomb.Invoke(catacomb.Plan{ 67 Site: &w.catacomb, 68 Work: w.loop, 69 }) 70 return w, errors.Trace(err) 71 } 72 73 type tokenRotateInfo struct { 74 ID string 75 backendName string 76 rotateTime time.Time 77 whenFunc func(rotateTime time.Time) time.Duration 78 } 79 80 func (s tokenRotateInfo) GoString() string { 81 return fmt.Sprintf("%s token rotation: in %v at %s", s.backendName, s.whenFunc(s.rotateTime), s.rotateTime.Format(time.RFC3339)) 82 } 83 84 // Worker fires events when secrets should be rotated. 85 type Worker struct { 86 catacomb catacomb.Catacomb 87 config Config 88 89 backendInfo map[string]tokenRotateInfo 90 91 timer clock.Timer 92 nextTrigger time.Time 93 } 94 95 // Kill is defined on worker.Worker. 96 func (w *Worker) Kill() { 97 w.catacomb.Kill(nil) 98 } 99 100 // Wait is part of the worker.Worker interface. 101 func (w *Worker) Wait() error { 102 return w.catacomb.Wait() 103 } 104 105 func (w *Worker) loop() (err error) { 106 changes, err := w.config.SecretBackendManagerFacade.WatchTokenRotationChanges() 107 if err != nil { 108 return errors.Trace(err) 109 } 110 if err := w.catacomb.Add(changes); err != nil { 111 return errors.Trace(err) 112 } 113 for { 114 var timeout <-chan time.Time 115 if w.timer != nil { 116 timeout = w.timer.Chan() 117 } 118 select { 119 case <-w.catacomb.Dying(): 120 return w.catacomb.ErrDying() 121 case ch, ok := <-changes.Changes(): 122 if !ok { 123 return errors.New("secret rotation change channel closed") 124 } 125 w.handleTokenRotateChanges(ch) 126 case now := <-timeout: 127 if err := w.rotate(now); err != nil { 128 return errors.Annotatef(err, "rotating secret backends") 129 } 130 } 131 } 132 } 133 134 func (w *Worker) rotate(now time.Time) error { 135 w.config.Logger.Debugf("processing secret backend token rotation at %s", now) 136 137 var toRotate []string 138 for id, info := range w.backendInfo { 139 w.config.Logger.Debugf("checking %s: rotate at %s... time diff %s", id, info.rotateTime, info.rotateTime.Sub(now)) 140 // A one minute granularity is acceptable for secret rotation. 141 if info.rotateTime.Truncate(time.Minute).Before(now) { 142 w.config.Logger.Debugf("rotating token for %s", info.backendName) 143 toRotate = append(toRotate, id) 144 // Once backend has been queued for rotation, delete it here since 145 // it will re-appear via the watcher after the rotation is actually 146 // performed and the last rotated time is updated. 147 delete(w.backendInfo, id) 148 } 149 } 150 151 if err := w.config.SecretBackendManagerFacade.RotateBackendTokens(toRotate...); err != nil { 152 return errors.Annotatef(err, "cannot rotate secret backend tokens for backend ids %q", toRotate) 153 } 154 w.computeNextRotateTime() 155 return nil 156 } 157 158 func (w *Worker) handleTokenRotateChanges(changes []watcher.SecretBackendRotateChange) { 159 w.config.Logger.Debugf("got rotate secret changes: %#v", changes) 160 if len(changes) == 0 { 161 return 162 } 163 164 for _, ch := range changes { 165 // Next rotate time of 0 means the rotation has been deleted. 166 if ch.NextTriggerTime.IsZero() { 167 w.config.Logger.Debugf("token for %q no longer rotated", ch.Name) 168 delete(w.backendInfo, ch.ID) 169 continue 170 } 171 w.backendInfo[ch.ID] = tokenRotateInfo{ 172 ID: ch.ID, 173 backendName: ch.Name, 174 rotateTime: ch.NextTriggerTime, 175 whenFunc: func(rotateTime time.Time) time.Duration { return rotateTime.Sub(w.config.Clock.Now()) }, 176 } 177 } 178 w.computeNextRotateTime() 179 } 180 181 func (w *Worker) computeNextRotateTime() { 182 w.config.Logger.Debugf("computing next rotated time for secret backends %#v", w.backendInfo) 183 184 if len(w.backendInfo) == 0 { 185 w.timer = nil 186 return 187 } 188 189 // Find the minimum (next) rotateTime from all the tokens. 190 var soonestRotateTime time.Time 191 for _, info := range w.backendInfo { 192 if !soonestRotateTime.IsZero() && info.rotateTime.After(soonestRotateTime) { 193 continue 194 } 195 soonestRotateTime = info.rotateTime 196 } 197 // There's no need to start or reset the timer if there's no changes to make. 198 if soonestRotateTime.IsZero() || w.nextTrigger == soonestRotateTime { 199 return 200 } 201 202 // Account for the worker not running when a secret 203 // should have been rotated. 204 now := w.config.Clock.Now() 205 if soonestRotateTime.Before(now) { 206 soonestRotateTime = now 207 } 208 209 nextDuration := soonestRotateTime.Sub(now) 210 w.config.Logger.Debugf("next token will rotate in %v at %s", nextDuration, soonestRotateTime) 211 212 w.nextTrigger = soonestRotateTime 213 if w.timer == nil { 214 w.timer = w.config.Clock.NewTimer(nextDuration) 215 } else { 216 // See the docs on Timer.Reset() that says it isn't safe to call 217 // on a non-stopped channel, and if it is stopped, you need to check 218 // if the channel needs to be drained anyway. It isn't safe to drain 219 // unconditionally in case another goroutine has already noticed, 220 // but make an attempt. 221 if !w.timer.Stop() { 222 select { 223 case <-w.timer.Chan(): 224 default: 225 } 226 } 227 w.timer.Reset(nextDuration) 228 } 229 }