diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index ac5e2b9d87..b8db696d0b 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -221,6 +221,14 @@ type Controller struct { // License is the license used by HCP builds License string `hcl:"license"` + + // MaxPageSize overrides the default and max page size. + // The default page size is what is used when the page size + // is not explicitly provided by the user. The max page size + // is the greatest number the page size can be set to before + // it is rejected by the controller. + MaxPageSizeRaw any `hcl:"max_page_size"` + MaxPageSize uint `hcl:"-"` } func (c *Controller) InitNameIfEmpty(ctx context.Context) error { @@ -630,6 +638,31 @@ func Parse(d string) (*Config, error) { return nil, errors.New("Controller liveness time to stale value is negative") } + if result.Controller.MaxPageSizeRaw != nil { + switch t := result.Controller.MaxPageSizeRaw.(type) { + case string: + maxPageSizeString, err := parseutil.ParsePath(t) + if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) { + return nil, fmt.Errorf("Error parsing max page size: %w", err) + } + pageSize, err := strconv.Atoi(maxPageSizeString) + if err != nil { + return nil, fmt.Errorf("Max page size value is not an int: %w", err) + } + if pageSize <= 0 { + return nil, fmt.Errorf("Max page size value must be at least 1, was %d", pageSize) + } + result.Controller.MaxPageSize = uint(pageSize) + case int: + if t <= 0 { + return nil, fmt.Errorf("Max page size value must be at least 1, was %d", t) + } + result.Controller.MaxPageSize = uint(t) + default: + return nil, fmt.Errorf("Max page size: unsupported type %q", reflect.TypeOf(t).String()) + } + } + if result.Controller.Database != nil { if result.Controller.Database.MaxOpenConnectionsRaw != nil { switch t := result.Controller.Database.MaxOpenConnectionsRaw.(type) { @@ -685,7 +718,6 @@ func Parse(d string) (*Config, error) { reflect.TypeOf(t).String()) } } - } result.Controller.ApiRateLimits, err = parseApiRateLimits(obj.Node) diff --git a/internal/cmd/config/config_load_test.go b/internal/cmd/config/config_load_test.go index 949fed77e9..16bd1526fc 100644 --- a/internal/cmd/config/config_load_test.go +++ b/internal/cmd/config/config_load_test.go @@ -420,6 +420,8 @@ func TestLoad(t *testing.T) { LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + MaxPageSizeRaw: nil, + MaxPageSize: 0, }, DevController: false, DevUiPassthroughDir: "", @@ -845,6 +847,8 @@ func TestLoad(t *testing.T) { LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + MaxPageSizeRaw: nil, + MaxPageSize: 0, }, DevController: false, DevUiPassthroughDir: "", @@ -1284,6 +1288,8 @@ func TestLoad(t *testing.T) { }, }, ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + MaxPageSizeRaw: nil, + MaxPageSize: 0, }, DevController: false, DevUiPassthroughDir: "", @@ -1704,6 +1710,8 @@ func TestLoad(t *testing.T) { LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + MaxPageSizeRaw: nil, + MaxPageSize: 0, }, DevController: false, DevUiPassthroughDir: "", @@ -1795,6 +1803,8 @@ func TestLoad(t *testing.T) { LivenessTimeToStaleDuration: 0, ApiRateLimits: make(ratelimit.Configs, 0), ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(), + MaxPageSizeRaw: nil, + MaxPageSize: 0, }, DevController: false, DevUiPassthroughDir: "", diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 0c4bce20e1..2d20130ee8 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -2683,3 +2683,105 @@ func TestSetupWorkerInitialUpstreams(t *testing.T) { }) } } + +func TestMaxPageSize(t *testing.T) { + tests := []struct { + name string + in string + envMaxPageSize string + expMaxPageSize uint + expErr bool + expErrStr string + }{ + { + name: "Valid integer value", + in: ` + controller { + name = "example-controller" + max_page_size = 5 + }`, + expMaxPageSize: 5, + expErr: false, + }, + { + name: "Valid string value", + in: ` + controller { + name = "example-controller" + max_page_size = "5" + }`, + expMaxPageSize: 5, + expErr: false, + }, + { + name: "Invalid value integer", + in: ` + controller { + name = "example-controller" + max_page_size = 0 + }`, + expErr: true, + expErrStr: "Max page size value must be at least 1, was 0", + }, + { + name: "Invalid value string", + in: ` + controller { + name = "example-controller" + max_page_size = "string bad" + }`, + expErr: true, + expErrStr: "Max page size value is not an int: " + + "strconv.Atoi: parsing \"string bad\": invalid syntax", + }, + { + name: "Invalid value type", + in: ` + controller { + name = "example-controller" + max_page_size = false + }`, + expErr: true, + expErrStr: "Max page size: unsupported type \"bool\"", + }, + { + name: "Valid env var", + in: ` + controller { + name = "example-controller" + max_page_size = "env://ENV_MAX_CONN" + }`, + expMaxPageSize: 8, + envMaxPageSize: "8", + expErr: false, + }, + { + name: "Invalid env var", + in: ` + controller { + name = "example-controller" + max_page_size = "env://ENV_MAX_CONN" + }`, + envMaxPageSize: "bogus value", + expErr: true, + expErrStr: "Max page size value is not an int: " + + "strconv.Atoi: parsing \"bogus value\": invalid syntax", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("ENV_MAX_CONN", tt.envMaxPageSize) + c, err := Parse(tt.in) + if tt.expErr { + require.EqualError(t, err, tt.expErrStr) + require.Nil(t, c) + return + } + + require.NoError(t, err) + require.NotNil(t, c) + require.NotNil(t, c.Controller) + require.Equal(t, tt.expMaxPageSize, c.Controller.MaxPageSize) + }) + } +} diff --git a/website/content/docs/configuration/controller.mdx b/website/content/docs/configuration/controller.mdx index 6daa43f63c..c20ae0acd1 100644 --- a/website/content/docs/configuration/controller.mdx +++ b/website/content/docs/configuration/controller.mdx @@ -139,6 +139,10 @@ The `api_rate_limit` configuration stanza contains the following fields: If `api_rate_limit_disable` is set to `true`, and you have provided any `api_rate_limit` stanzas, you will receive an error. - `api_rate_limit_max_quotas` - Specifies the maximum number of API rate limiting quotas that Boundary allows. +- `max_page_size` - The max allowed page size when paginating. If a user specifies a page size greater than + this number, it will be truncated to this number. This is also used as the default page size for any request + that don't explicitly specify a page size. Default is 1000. + ## Signals The `SIGHUP` signal causes a controller to reload its configuration file to pick up any updates to the `database url` value. Any other updated values are ignored.