226 lines
5.4 KiB
Go
226 lines
5.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/hashicorp/boundary/internal/cmd/config"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/go-rate"
|
|
)
|
|
|
|
// limit is a representation of a rate.Limit that is used when emitting a sys
|
|
// event to report the rate limit configuration.
|
|
type limit struct {
|
|
Resource string `json:"resource"`
|
|
Action string `json:"action"`
|
|
Per string `json:"per"`
|
|
Unlimited bool `json:"unlimited"`
|
|
Limit uint64 `json:"limit"`
|
|
Period string `json:"period"`
|
|
}
|
|
|
|
// Types used for the nested structure of the sys event.
|
|
type (
|
|
actionLimits []limit
|
|
resourceActions map[string]actionLimits
|
|
resources map[string]resourceActions
|
|
)
|
|
|
|
type rateLimiterConfig struct {
|
|
maxSize int
|
|
disabled bool
|
|
configs ratelimit.Configs
|
|
|
|
limits []rate.Limit
|
|
}
|
|
|
|
func newRateLimiterConfig(ctx context.Context, configs ratelimit.Configs, maxSize int, disabled bool) (*rateLimiterConfig, error) {
|
|
const op = "controller.newRateLimiterConfig"
|
|
|
|
switch {
|
|
case disabled && len(configs) != 0:
|
|
return nil, errors.New(ctx, errors.InvalidConfiguration, op, "disabled rate limiter with rate limit configs")
|
|
}
|
|
|
|
var limits []rate.Limit
|
|
var err error
|
|
if !disabled {
|
|
if limits, err = configs.Limits(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &rateLimiterConfig{
|
|
maxSize: maxSize,
|
|
disabled: disabled,
|
|
configs: configs,
|
|
limits: limits,
|
|
}, nil
|
|
}
|
|
|
|
// writeSysEvent writes a sys event for c
|
|
func (c *rateLimiterConfig) writeSysEvent(ctx context.Context) {
|
|
const op = "controller.(rateLimiterConfig).writeSysEvent"
|
|
|
|
if c.disabled {
|
|
event.WriteSysEvent(
|
|
ctx,
|
|
op,
|
|
"controller api rate limiter",
|
|
"disabled",
|
|
true,
|
|
)
|
|
return
|
|
}
|
|
|
|
e := make(resources)
|
|
|
|
for _, l := range c.limits {
|
|
var r resourceActions
|
|
var a actionLimits
|
|
var ok bool
|
|
r, ok = e[l.GetResource()]
|
|
if !ok {
|
|
r = make(resourceActions)
|
|
e[l.GetResource()] = r
|
|
}
|
|
|
|
a, ok = r[l.GetAction()]
|
|
if !ok {
|
|
a = make(actionLimits, 0, 3)
|
|
}
|
|
|
|
switch ll := l.(type) {
|
|
case *rate.Limited:
|
|
a = append(a, limit{
|
|
Resource: ll.Resource,
|
|
Action: ll.Action,
|
|
Per: ll.Per.String(),
|
|
Unlimited: false,
|
|
Limit: ll.MaxRequests,
|
|
Period: ll.Period.String(),
|
|
})
|
|
case *rate.Unlimited:
|
|
a = append(a, limit{
|
|
Resource: ll.Resource,
|
|
Action: ll.Action,
|
|
Per: ll.Per.String(),
|
|
Unlimited: true,
|
|
})
|
|
}
|
|
r[l.GetAction()] = a
|
|
}
|
|
event.WriteSysEvent(
|
|
ctx,
|
|
op,
|
|
"controller api rate limiter",
|
|
"limits",
|
|
e,
|
|
"max_size",
|
|
c.maxSize,
|
|
)
|
|
}
|
|
|
|
func (c *Controller) initializeRateLimiter(conf *config.Config) error {
|
|
const op = "controller.(Controller).initializeRateLimiter"
|
|
switch {
|
|
case conf == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
|
|
case conf.Controller == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
|
|
}
|
|
|
|
c.rateLimiterMu.Lock()
|
|
defer c.rateLimiterMu.Unlock()
|
|
|
|
rlConfig, err := newRateLimiterConfig(
|
|
c.baseContext,
|
|
conf.Controller.ApiRateLimits,
|
|
conf.Controller.ApiRateLimiterMaxQuotas,
|
|
conf.Controller.ApiRateLimitDisable,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case rlConfig.disabled:
|
|
c.rateLimiter = rate.NopLimiter
|
|
default:
|
|
c.rateLimiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
c.conf.rateLimiterConfig = rlConfig
|
|
|
|
rlConfig.writeSysEvent(c.baseContext)
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) getRateLimiter() ratelimit.Limiter {
|
|
c.rateLimiterMu.RLock()
|
|
defer c.rateLimiterMu.RUnlock()
|
|
return c.rateLimiter
|
|
}
|
|
|
|
// ReloadRateLimiter replaces the Controller's rate.Limiter with a new rate.Limiter
|
|
// using the supplied ratelimit.Configs and max entries. If the configs and max
|
|
// entries match the current values, the rate.Limiter is not replaced and no
|
|
// error is returned. Otherwise the rate.Limiter is replaced, and the old rate.Limiter
|
|
// is shutdown. This means that a client's quota is effectively reset if the
|
|
// configuration changes.
|
|
func (c *Controller) ReloadRateLimiter(newConfig *config.Config) error {
|
|
const op = "controller.(Controller).ReloadRateLimiter"
|
|
|
|
switch {
|
|
case newConfig == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config")
|
|
case newConfig.Controller == nil:
|
|
return errors.New(c.baseContext, errors.InvalidParameter, op, "nil config.Controller")
|
|
}
|
|
|
|
rlConfig, err := newRateLimiterConfig(
|
|
c.baseContext,
|
|
newConfig.Controller.ApiRateLimits,
|
|
newConfig.Controller.ApiRateLimiterMaxQuotas,
|
|
newConfig.Controller.ApiRateLimitDisable,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Config has not changed, no need to reload.
|
|
if c.conf.rateLimiterConfig.maxSize == rlConfig.maxSize &&
|
|
c.conf.rateLimiterConfig.disabled == rlConfig.disabled &&
|
|
c.conf.rateLimiterConfig.configs.Equal(rlConfig.configs) {
|
|
return nil
|
|
}
|
|
|
|
var limiter ratelimit.Limiter
|
|
switch {
|
|
case rlConfig.disabled:
|
|
limiter = rate.NopLimiter
|
|
default:
|
|
limiter, err = ratelimit.NewLimiter(rlConfig.limits, rlConfig.maxSize)
|
|
if err != nil {
|
|
return errors.Wrap(c.baseContext, err, op)
|
|
}
|
|
|
|
}
|
|
c.rateLimiterMu.Lock()
|
|
old := c.rateLimiter
|
|
c.rateLimiter = limiter
|
|
c.conf.rateLimiterConfig = rlConfig
|
|
c.rateLimiterMu.Unlock()
|
|
rlConfig.writeSysEvent(c.baseContext)
|
|
|
|
return old.Shutdown()
|
|
}
|