Files
f.weber ae30ba1240 feat(vault-tui): implement KV client and service for managing secrets
- Added internal/vault/client.go for creating a Vault client with configuration settings.
- Introduced internal/vault/errors.go to classify Vault API errors for better UI handling.
- Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations.
- Implemented internal/vault/mounts.go to list and describe secret engine mounts.
- Developed internal/vault/service.go to provide a unified entry point for Vault operations.
- Added internal/vault/kv_test.go for comprehensive testing of KV operations.
- Introduced internal/ui/toast.go for transient notifications in the UI.
- Added renovate.json for dependency management and updates.
2026-08-14 11:09:03 +02:00

111 lines
3.1 KiB
Go

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
}