package token import ( "context" "fmt" "time" "github.com/hashicorp/vault/api" ) // RenewEvent is emitted by AutoRenewer as the watched token's lifecycle // changes. Err set + Terminal true means the watcher has given up and the // token will eventually expire without further notice. type RenewEvent struct { At time.Time TTL time.Duration Err error Terminal bool } // AutoRenewer wraps api.LifetimeWatcher with the two special cases that // matter for a token we did not necessarily just mint ourselves: batch // tokens are not renewable, and root tokens have TTL == 0 (never expire). // NewAutoRenewer returns (nil, nil) for either case — callers should treat // a nil renewer as "nothing to do", not an error. type AutoRenewer struct { client *api.Client w *api.LifetimeWatcher events chan RenewEvent } // NewAutoRenewer builds a renewer from a *Info (e.g. from Lookup) rather // than requiring the original login *api.Secret, since a token resolved // from ~/.vault-token has no such Secret — this synthesises an equivalent // one from the lookup data. func NewAutoRenewer(c *api.Client, info *Info, increment time.Duration) (*AutoRenewer, error) { if !info.Renewable || info.ExpireTime == nil { return nil, nil } sec := &api.Secret{ Auth: &api.SecretAuth{ ClientToken: c.Token(), LeaseDuration: int(info.TTL.Seconds()), Renewable: info.Renewable, }, } w, err := c.NewLifetimeWatcher(&api.LifetimeWatcherInput{ Secret: sec, Increment: int(increment.Seconds()), RenewBuffer: api.DefaultLifetimeWatcherRenewBuffer, RenewBehavior: api.RenewBehaviorIgnoreErrors, }) if err != nil { return nil, fmt.Errorf("creating lifetime watcher: %w", err) } return &AutoRenewer{client: c, w: w, events: make(chan RenewEvent, 8)}, nil } // Start runs the watcher until ctx is cancelled or Stop is called. func (a *AutoRenewer) Start(ctx context.Context) { go a.w.Start() go func() { defer close(a.events) for { select { case <-ctx.Done(): a.w.Stop() return case out, ok := <-a.w.RenewCh(): if !ok { return } a.emit(RenewEvent{At: time.Now(), TTL: leaseDuration(out)}) case err, ok := <-a.w.DoneCh(): if !ok { return } a.emit(RenewEvent{At: time.Now(), Err: err, Terminal: true}) return } } }() } func (a *AutoRenewer) emit(e RenewEvent) { select { case a.events <- e: default: } } // Events returns the channel of renewal notifications; closed when the // watcher stops. func (a *AutoRenewer) Events() <-chan RenewEvent { return a.events } // Stop releases the underlying watcher. func (a *AutoRenewer) Stop() { a.w.Stop() } // leaseDuration reads the renewed TTL out of a RenewOutput. Token renewals // (our only use case) carry it on Secret.Auth, not the top-level // Secret.LeaseDuration field that non-auth lease renewals use. func leaseDuration(out *api.RenewOutput) time.Duration { if out == nil || out.Secret == nil { return 0 } if out.Secret.Auth != nil { return time.Duration(out.Secret.Auth.LeaseDuration) * time.Second } return time.Duration(out.Secret.LeaseDuration) * time.Second }