2019-03-05 19:55:07 +00:00
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"sort"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/errwrap"
|
2019-04-12 21:54:35 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
2019-03-05 19:55:07 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
requestCounterDatePathFormat = "2006/01"
|
|
|
|
countersPath = systemBarrierPrefix + "counters"
|
2019-04-11 16:35:26 +00:00
|
|
|
requestCountersPath = "sys/counters/requests/"
|
2019-03-05 19:55:07 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type counters struct {
|
|
|
|
// requests counts requests seen by Vault this month; does not include requests
|
|
|
|
// excluded by design, e.g. health checks and UI asset requests.
|
|
|
|
requests *uint64
|
|
|
|
// activePath is set at startup to the path we primed the requests counter from,
|
|
|
|
// or empty string if there wasn't a relevant path - either because this is the first
|
|
|
|
// time Vault starts with the feature enabled, or because Vault hadn't written
|
|
|
|
// out the request counter this month yet.
|
|
|
|
// Whenever we write out the counters, we update activePath if it's no longer
|
|
|
|
// accurate. This coincides with a reset of the counters.
|
|
|
|
// There's no lock because the only reader/writer of activePath is the goroutine
|
|
|
|
// doing background syncs.
|
|
|
|
activePath string
|
|
|
|
// syncInterval determines how often the counters get written to storage (on primary)
|
|
|
|
// or synced to primary.
|
|
|
|
syncInterval time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
// RequestCounter stores the state of request counters for a single unspecified period.
|
|
|
|
type RequestCounter struct {
|
|
|
|
// Total is the number of requests seen during a given period.
|
|
|
|
Total *uint64 `json:"total"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// DatedRequestCounter holds request counters from a single period of time.
|
|
|
|
type DatedRequestCounter struct {
|
|
|
|
// StartTime is when the period starts.
|
|
|
|
StartTime time.Time `json:"start_time"`
|
|
|
|
// RequestCounter counts requests.
|
|
|
|
RequestCounter
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadAllRequestCounters returns all request counters found in storage,
|
|
|
|
// ordered by time (oldest first.)
|
|
|
|
func (c *Core) loadAllRequestCounters(ctx context.Context, now time.Time) ([]DatedRequestCounter, error) {
|
2019-04-11 16:35:26 +00:00
|
|
|
view := NewBarrierView(c.barrier, requestCountersPath)
|
2019-03-05 19:55:07 +00:00
|
|
|
|
|
|
|
datepaths, err := view.List(ctx, "")
|
|
|
|
if err != nil {
|
|
|
|
return nil, errwrap.Wrapf("failed to read request counters: {{err}}", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var all []DatedRequestCounter
|
|
|
|
sort.Strings(datepaths)
|
|
|
|
for _, datepath := range datepaths {
|
|
|
|
datesubpaths, err := view.List(ctx, datepath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errwrap.Wrapf("failed to read request counters: {{err}}", err)
|
|
|
|
}
|
|
|
|
sort.Strings(datesubpaths)
|
|
|
|
for _, datesubpath := range datesubpaths {
|
|
|
|
fullpath := datepath + datesubpath
|
|
|
|
counter, err := c.loadRequestCounters(ctx, fullpath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
t, err := time.Parse(requestCounterDatePathFormat, fullpath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
all = append(all, DatedRequestCounter{StartTime: t, RequestCounter: *counter})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
start, _ := time.Parse(requestCounterDatePathFormat, now.Format(requestCounterDatePathFormat))
|
|
|
|
idx := sort.Search(len(all), func(i int) bool {
|
|
|
|
return !all[i].StartTime.Before(start)
|
|
|
|
})
|
|
|
|
cur := atomic.LoadUint64(c.counters.requests)
|
|
|
|
if idx < len(all) {
|
|
|
|
all[idx].RequestCounter.Total = &cur
|
|
|
|
} else {
|
|
|
|
all = append(all, DatedRequestCounter{StartTime: start, RequestCounter: RequestCounter{Total: &cur}})
|
|
|
|
}
|
|
|
|
|
|
|
|
return all, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadCurrentRequestCounters reads the current RequestCounter out of storage.
|
|
|
|
// The in-memory current request counter is populated with the value read, if any.
|
|
|
|
// now should be the current time; it is a parameter to facilitate testing.
|
|
|
|
func (c *Core) loadCurrentRequestCounters(ctx context.Context, now time.Time) error {
|
|
|
|
datepath := now.Format(requestCounterDatePathFormat)
|
|
|
|
counter, err := c.loadRequestCounters(ctx, datepath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if counter != nil {
|
|
|
|
c.counters.activePath = datepath
|
|
|
|
atomic.StoreUint64(c.counters.requests, *counter.Total)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadRequestCounters reads a RequestCounter out of storage at location datepath.
|
|
|
|
// If nothing is found at that path, that isn't an error: a reference to a zero
|
|
|
|
// RequestCounter is returned.
|
|
|
|
func (c *Core) loadRequestCounters(ctx context.Context, datepath string) (*RequestCounter, error) {
|
2019-04-11 16:35:26 +00:00
|
|
|
view := NewBarrierView(c.barrier, requestCountersPath)
|
2019-03-05 19:55:07 +00:00
|
|
|
|
|
|
|
out, err := view.Get(ctx, datepath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errwrap.Wrapf("failed to read request counters: {{err}}", err)
|
|
|
|
}
|
|
|
|
if out == nil {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
newCounters := &RequestCounter{}
|
|
|
|
err = out.DecodeJSON(newCounters)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return newCounters, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// saveCurrentRequestCounters writes the current RequestCounter to storage.
|
|
|
|
// The in-memory current request counter is reset to zero after writing if
|
|
|
|
// we've entered a new month.
|
|
|
|
// now should be the current time; it is a parameter to facilitate testing.
|
|
|
|
func (c *Core) saveCurrentRequestCounters(ctx context.Context, now time.Time) error {
|
2019-04-11 16:35:26 +00:00
|
|
|
view := NewBarrierView(c.barrier, requestCountersPath)
|
2019-03-05 19:55:07 +00:00
|
|
|
requests := atomic.LoadUint64(c.counters.requests)
|
|
|
|
curDatePath := now.Format(requestCounterDatePathFormat)
|
|
|
|
|
|
|
|
// If activePath is empty string, we were started with nothing in storage
|
|
|
|
// for the current month, so we should not reset the in-mem counter.
|
|
|
|
// But if activePath is nonempty and not curDatePath, we should reset.
|
|
|
|
shouldReset, writeDatePath := false, curDatePath
|
|
|
|
if c.counters.activePath != "" && c.counters.activePath != curDatePath {
|
|
|
|
shouldReset, writeDatePath = true, c.counters.activePath
|
|
|
|
}
|
|
|
|
|
|
|
|
localCounters := &RequestCounter{
|
|
|
|
Total: &requests,
|
|
|
|
}
|
|
|
|
entry, err := logical.StorageEntryJSON(writeDatePath, localCounters)
|
|
|
|
if err != nil {
|
|
|
|
return errwrap.Wrapf("failed to create request counters entry: {{err}}", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := view.Put(ctx, entry); err != nil {
|
|
|
|
return errwrap.Wrapf("failed to save request counters: {{err}}", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if shouldReset {
|
|
|
|
atomic.StoreUint64(c.counters.requests, 0)
|
|
|
|
}
|
|
|
|
if c.counters.activePath != curDatePath {
|
|
|
|
c.counters.activePath = curDatePath
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|