Skip to content

Commit 8e5af82

Browse files
authored
feat: add api-rate-limit flag (#5013)
1 parent 2042b57 commit 8e5af82

File tree

7 files changed

+110
-1
lines changed

7 files changed

+110
-1
lines changed

cli/deployment/config.go

+6
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ func newConfig() *codersdk.DeploymentConfig {
372372
Default: 10 * time.Minute,
373373
},
374374
},
375+
APIRateLimit: &codersdk.DeploymentConfigField[int]{
376+
Name: "API Rate Limit",
377+
Usage: "Maximum number of requests per minute allowed to the API per user, or per IP address for unauthenticated users. Negative values mean no rate limit. Some API endpoints are always rate limited regardless of this value to prevent denial-of-service attacks.",
378+
Flag: "api-rate-limit",
379+
Default: 512,
380+
},
375381
Experimental: &codersdk.DeploymentConfigField[bool]{
376382
Name: "Experimental",
377383
Usage: "Enable experimental features. Experimental features are not ready for production.",

cli/server.go

+1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
363363
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value,
364364
DeploymentConfig: cfg,
365365
PrometheusRegistry: prometheus.NewRegistry(),
366+
APIRateLimit: cfg.APIRateLimit.Value,
366367
}
367368
if tlsConfig != nil {
368369
options.TLSCertificates = tlsConfig.Certificates

cli/server_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,94 @@ func TestServer(t *testing.T) {
633633
cancelFunc()
634634
<-serverErr
635635
})
636+
637+
t.Run("RateLimit", func(t *testing.T) {
638+
t.Parallel()
639+
640+
t.Run("Default", func(t *testing.T) {
641+
t.Parallel()
642+
ctx, cancelFunc := context.WithCancel(context.Background())
643+
defer cancelFunc()
644+
645+
root, cfg := clitest.New(t,
646+
"server",
647+
"--in-memory",
648+
"--address", ":0",
649+
"--access-url", "http://example.com",
650+
)
651+
serverErr := make(chan error, 1)
652+
go func() {
653+
serverErr <- root.ExecuteContext(ctx)
654+
}()
655+
accessURL := waitAccessURL(t, cfg)
656+
client := codersdk.New(accessURL)
657+
658+
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
659+
require.NoError(t, err)
660+
defer resp.Body.Close()
661+
require.Equal(t, http.StatusOK, resp.StatusCode)
662+
require.Equal(t, "512", resp.Header.Get("X-Ratelimit-Limit"))
663+
cancelFunc()
664+
<-serverErr
665+
})
666+
667+
t.Run("Changed", func(t *testing.T) {
668+
t.Parallel()
669+
ctx, cancelFunc := context.WithCancel(context.Background())
670+
defer cancelFunc()
671+
672+
val := "100"
673+
root, cfg := clitest.New(t,
674+
"server",
675+
"--in-memory",
676+
"--address", ":0",
677+
"--access-url", "http://example.com",
678+
"--api-rate-limit", val,
679+
)
680+
serverErr := make(chan error, 1)
681+
go func() {
682+
serverErr <- root.ExecuteContext(ctx)
683+
}()
684+
accessURL := waitAccessURL(t, cfg)
685+
client := codersdk.New(accessURL)
686+
687+
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
688+
require.NoError(t, err)
689+
defer resp.Body.Close()
690+
require.Equal(t, http.StatusOK, resp.StatusCode)
691+
require.Equal(t, val, resp.Header.Get("X-Ratelimit-Limit"))
692+
cancelFunc()
693+
<-serverErr
694+
})
695+
696+
t.Run("Disabled", func(t *testing.T) {
697+
t.Parallel()
698+
ctx, cancelFunc := context.WithCancel(context.Background())
699+
defer cancelFunc()
700+
701+
root, cfg := clitest.New(t,
702+
"server",
703+
"--in-memory",
704+
"--address", ":0",
705+
"--access-url", "http://example.com",
706+
"--api-rate-limit", "-1",
707+
)
708+
serverErr := make(chan error, 1)
709+
go func() {
710+
serverErr <- root.ExecuteContext(ctx)
711+
}()
712+
accessURL := waitAccessURL(t, cfg)
713+
client := codersdk.New(accessURL)
714+
715+
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
716+
require.NoError(t, err)
717+
defer resp.Body.Close()
718+
require.Equal(t, http.StatusOK, resp.StatusCode)
719+
require.Equal(t, "", resp.Header.Get("X-Ratelimit-Limit"))
720+
cancelFunc()
721+
<-serverErr
722+
})
723+
})
636724
}
637725

638726
func generateTLSCertificate(t testing.TB, commonName ...string) (certPath, keyPath string) {

cli/testdata/coder_server_--help.golden

+8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ Flags:
1717
-a, --address string Bind address of the server.
1818
Consumes $CODER_ADDRESS (default
1919
"127.0.0.1:3000")
20+
--api-rate-limit int Maximum number of requests per minute
21+
allowed to the API per user, or per IP
22+
address for unauthenticated users.
23+
Negative values mean no rate limit. Some
24+
API endpoints are always rate limited
25+
regardless of this value to prevent
26+
denial-of-service attacks.
27+
Consumes $CODER_API_RATE_LIMIT (default 512)
2028
--cache-dir string The directory to cache temporary files.
2129
If unspecified and $CACHE_DIRECTORY is
2230
set, it will be used for compatibility

coderd/database/generate.sh

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
2222

2323
first=true
2424
for fi in queries/*.sql.go; do
25-
# Find the last line from the imports section and add 1.
25+
# Find the last line from the imports section and add 1. We have to
26+
# disable pipefail temporarily to avoid ERRPIPE errors when piping into
27+
# `head -n1`.
28+
set +o pipefail
2629
cut=$(grep -n ')' "$fi" | head -n 1 | cut -d: -f1)
30+
set -o pipefail
2731
cut=$((cut + 1))
2832

2933
# Copy the header from the first file only, ignoring the source comment.

codersdk/deploymentconfig.go

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type DeploymentConfig struct {
3939
SCIMAPIKey *DeploymentConfigField[string] `json:"scim_api_key" typescript:",notnull"`
4040
UserWorkspaceQuota *DeploymentConfigField[int] `json:"user_workspace_quota" typescript:",notnull"`
4141
Provisioner *ProvisionerConfig `json:"provisioner" typescript:",notnull"`
42+
APIRateLimit *DeploymentConfigField[int] `json:"api_rate_limit" typescript:",notnull"`
4243
Experimental *DeploymentConfigField[bool] `json:"experimental" typescript:",notnull"`
4344
}
4445

site/src/api/typesGenerated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export interface DeploymentConfig {
303303
readonly scim_api_key: DeploymentConfigField<string>
304304
readonly user_workspace_quota: DeploymentConfigField<number>
305305
readonly provisioner: ProvisionerConfig
306+
readonly api_rate_limit: DeploymentConfigField<number>
306307
readonly experimental: DeploymentConfigField<boolean>
307308
}
308309

0 commit comments

Comments
 (0)