Skip to content

Commit e6c82f2

Browse files
authored
feat(webhook): add rate limiting to webhook endpoint (#1210)
Signed-off-by: Christopher Coco <[email protected]> Signed-off-by: cjcocokrisp <[email protected]>
1 parent e816c49 commit e6c82f2

File tree

9 files changed

+145
-10
lines changed

9 files changed

+145
-10
lines changed

cmd/run.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math"
78
"os"
89
"strings"
910
"sync"
@@ -26,6 +27,8 @@ import (
2627

2728
"golang.org/x/sync/semaphore"
2829

30+
"go.uber.org/ratelimit"
31+
2932
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3033
)
3134

@@ -216,6 +219,10 @@ func newRunCommand() *cobra.Command {
216219
log.Infof("Starting webhook server on port %d", webhookCfg.Port)
217220
webhookServer = webhook.NewWebhookServer(webhookCfg.Port, handler, cfg.KubeClient, argoClient)
218221

222+
if webhookCfg.RateLimitNumAllowedRequests > 0 {
223+
webhookServer.RateLimiter = ratelimit.New(webhookCfg.RateLimitNumAllowedRequests, ratelimit.Per(time.Hour))
224+
}
225+
219226
// Set updater config
220227
webhookServer.UpdaterConfig = &argocd.UpdateConfiguration{
221228
NewRegFN: registry.NewClient,
@@ -337,6 +344,7 @@ func newRunCommand() *cobra.Command {
337344
runCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
338345
runCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
339346
runCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
347+
runCmd.Flags().IntVar(&webhookCfg.RateLimitNumAllowedRequests, "webhook-ratelimit-allowed", env.ParseNumFromEnv("WEBHOOK_RATELIMIT_ALLOWED", 0, 0, math.MaxInt), "The number of allowed requests in an hour for webhook rate limiting, setting to 0 disables ratelimiting")
340348

341349
return runCmd
342350
}

cmd/webhook.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math"
78
"os"
89
"os/signal"
910
"strconv"
1011
"strings"
1112
"syscall"
1213
"text/template"
14+
"time"
1315

1416
"github.com/argoproj-labs/argocd-image-updater/pkg/argocd"
1517
"github.com/argoproj-labs/argocd-image-updater/pkg/common"
@@ -21,16 +23,18 @@ import (
2123

2224
"github.com/argoproj/argo-cd/v2/util/askpass"
2325
"github.com/spf13/cobra"
26+
"go.uber.org/ratelimit"
2427
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2528
)
2629

2730
// WebhookConfig holds the options for the webhook server
2831
type WebhookConfig struct {
29-
Port int
30-
DockerSecret string
31-
GHCRSecret string
32-
QuaySecret string
33-
HarborSecret string
32+
Port int
33+
DockerSecret string
34+
GHCRSecret string
35+
QuaySecret string
36+
HarborSecret string
37+
RateLimitNumAllowedRequests int
3438
}
3539

3640
// NewWebhookCommand creates a new webhook command
@@ -190,6 +194,7 @@ Supported registries:
190194
webhookCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
191195
webhookCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
192196
webhookCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
197+
webhookCmd.Flags().IntVar(&webhookCfg.RateLimitNumAllowedRequests, "webhook-ratelimit-allowed", env.ParseNumFromEnv("WEBHOOK_RATELIMIT_ALLOWED", 0, 0, math.MaxInt), "The number of allowed requests in an hour for webhook rate limiting, setting to 0 disables ratelimiting")
193198

194199
return webhookCmd
195200
}
@@ -238,6 +243,10 @@ func runWebhook(cfg *ImageUpdaterConfig, webhookCfg *WebhookConfig) error {
238243
// Create webhook server
239244
server := webhook.NewWebhookServer(webhookCfg.Port, handler, cfg.KubeClient, cfg.ArgoClient)
240245

246+
if webhookCfg.RateLimitNumAllowedRequests > 0 {
247+
server.RateLimiter = ratelimit.New(webhookCfg.RateLimitNumAllowedRequests, ratelimit.Per(time.Hour))
248+
}
249+
241250
// Set updater config
242251
server.UpdaterConfig = &argocd.UpdateConfiguration{
243252
NewRegFN: registry.NewClient,
@@ -273,5 +282,6 @@ func runWebhook(cfg *ImageUpdaterConfig, webhookCfg *WebhookConfig) error {
273282
if err := server.Stop(); err != nil {
274283
log.Errorf("Error stopping webhook server: %v", err)
275284
}
285+
276286
return nil
277287
}

docs/configuration/webhook.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,21 @@ to apply them yourself.
155155

156156
They are located in the `manifets/base/networking` directory.
157157

158+
## Rate Limiting
159+
160+
To prevent overloading from the `/webhook` endpoint which could cause Image
161+
Updater to use too many resources rate limiting is implemented for the endpoint.
162+
163+
The rate limiting allows for a certain amount of requests per hour. This setting
164+
is configurable and can be set with the configuration value below. If you go over
165+
the limit the request will wait until it is allowed. The rate limit value defaults
166+
to 0 which means that it is disabled.
167+
```yaml
168+
data:
169+
# How many requests can be made per second. The default is 0 meaning disabled.
170+
webhook.ratelimit-allowed: <SOME_NUMBER>
171+
```
172+
158173
## Environment Variables
159174
160175
The flags for both the `run` and `webhook` CLI commands can also be set via
@@ -168,6 +183,7 @@ environment variables. Below is the list of which variables correspond to which
168183
|`GHCR_WEBHOOK_SECRET` |`--gchr-webhook-secret`|
169184
|`HARBOR_WEBHOOK_SECRET` |`--harbor-webhook-secret`|
170185
|`QUAY_WEBHOOK_SECRET` |`--quay-webhook-secret`|
186+
|`WEBHOOK_RATELIMIT_ALLOWED`|`--webhook-ratelimit-allowed`|
171187

172188
## Adding Support For Other Registries
173189

docs/install/cmd/run.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,20 @@ to workloads it found in need for upgrade.
8787

8888
Secret for validating Docker Hub webhooks.
8989

90+
Can also be set with the `DOCKER_WEBHOOK_SECRET` environment variable.
91+
9092
**--enable-webhook *enabled***
9193

9294
Enable webhook server for receiving registry events.
9395

96+
Can also be set with the `ENABLE_WEBHOOK` environment variable.
97+
9498
**--ghcr-webhook-secret *secret***
9599

96100
Secret for validating GitHub container registry webhooks.
97101

102+
Can also be set with the `GHCR_WEBHOOK_SECRET` environment variable.
103+
98104
**--git-commit-email *email***
99105

100106
E-Mail address to use for Git commits (default "[email protected]")
@@ -131,6 +137,8 @@ Can also be set using the *GIT_COMMIT_USER* environment variable.
131137

132138
Secret for validating Harbor webhooks
133139

140+
Can also be set with the `HARBOR_WEBHOOK_SECRET` environment variable.
141+
134142
**--health-port *port***
135143

136144
Specifies the local port to bind the health server to. The health server is
@@ -210,6 +218,8 @@ Argo CD Image Updater will exit after the first update cycle.
210218

211219
Secret for validating Quay webhooks.
212220

221+
Can also be set with the `QUAY_WEBHOOK_SECRET` environment variable.
222+
213223
**--registries-conf-path *path***
214224

215225
Load the registry configuration from file at *path*. Defaults to the path
@@ -225,4 +235,13 @@ whether to perform a cache warm-up on startup (default true)
225235

226236
Port to listen on for webhook events (default 8082)
227237

238+
Can also be set with the `WEBHOOK_PORT` environment variable.
239+
240+
**--webhook-ratelimit-allowed *numRequests***
241+
242+
The number of allowed requests in an hour for webhook rate limiting, setting to 0
243+
means that the rate limiting is disabled.
244+
245+
Can also be set with the `WEBHOOK_RATELIMIT_ALLOWED` environment variable.
246+
228247
[label selector syntax]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors

docs/install/cmd/webhook.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,14 @@ for images can only be specified from an environment variable.
8989

9090
Secret for validating Docker Hub webhooks.
9191

92+
Can also be set with the `DOCKER_WEBHOOK_SECRET` environment variable.
93+
9294
**--ghcr-webhook-secret *secret***
9395

9496
Secret for validating GitHub container registry secrets.
9597

98+
Can also be set with the `GHCR_WEBHOOK_SECRET` environment variable.
99+
96100
**--git-commit-email *email***
97101

98102
E-Mail address to use for Git commits (default "[email protected]")
@@ -129,6 +133,8 @@ Can also be set using the *GIT_COMMIT_USER* environment variable.
129133

130134
Secret for validating Harbor webhooks
131135

136+
Can also be set with the `HARBOR_WEBHOOK_SECRET` environment variable.
137+
132138
**-h, --help**
133139

134140
help for run
@@ -175,6 +181,8 @@ application processing, specify a number of `1`.
175181

176182
Secret for validating Quay webhooks
177183

184+
Can also be set with the `QUAY_WEBHOOK_SECRET` environment variable.
185+
178186
**--registries-conf-path *path***
179187

180188
Load the registry configuration from file at *path*. Defaults to the path
@@ -186,4 +194,13 @@ default configuration should be used instead, specify the empty string, i.e.
186194

187195
Port to listen on for webhook events (default 8080)
188196

197+
Can also be set with the `WEBHOOK_PORT` environment variable.
198+
199+
**--webhook-ratelimit-allowed *numRequests***
200+
201+
The number of allowed requests in an hour for webhook rate limiting, setting to 0
202+
means that the rate limiting is disabled.
203+
204+
Can also be set with the `WEBHOOK_RATELIMIT_ALLOWED` environment variable.
205+
189206
[label selector syntax]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors

manifests/base/deployment/argocd-image-updater-deployment.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ spec:
149149
name: argocd-image-updater-secret
150150
key: webhook.harbor-secret
151151
optional: true
152+
- name: WEBHOOK_RATELIMIT_ALLOWED
153+
valueFrom:
154+
configMapKeyRef:
155+
name: argocd-image-updater-config
156+
key: webhook.ratelimit-allowed
157+
optional: true
152158
livenessProbe:
153159
httpGet:
154160
path: /healthz

manifests/install.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ spec:
257257
key: webhook.harbor-secret
258258
name: argocd-image-updater-secret
259259
optional: true
260+
- name: WEBHOOK_RATELIMIT_ALLOWED
261+
valueFrom:
262+
configMapKeyRef:
263+
key: webhook.ratelimit-allowed
264+
name: argocd-image-updater-config
265+
optional: true
260266
image: quay.io/argoprojlabs/argocd-image-updater:latest
261267
imagePullPolicy: Always
262268
livenessProbe:

pkg/webhook/server.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
1414

1515
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
16+
"go.uber.org/ratelimit"
1617
)
1718

1819
// WebhookServer manages webhook endpoints and triggers update checks
@@ -33,16 +34,19 @@ type WebhookServer struct {
3334
mutex sync.Mutex
3435
// mutex for concurrent repo access
3536
syncState *argocd.SyncIterationState
37+
// rate limiter to limit requests in an interval
38+
RateLimiter ratelimit.Limiter
3639
}
3740

3841
// NewWebhookServer creates a new webhook server
3942
func NewWebhookServer(port int, handler *WebhookHandler, kubeClient *kube.ImageUpdaterKubernetesClient, argoClient argocd.ArgoCD) *WebhookServer {
4043
return &WebhookServer{
41-
Port: port,
42-
Handler: handler,
43-
KubeClient: kubeClient,
44-
ArgoClient: argoClient,
45-
syncState: argocd.NewSyncIterationState(),
44+
Port: port,
45+
Handler: handler,
46+
KubeClient: kubeClient,
47+
ArgoClient: argoClient,
48+
syncState: argocd.NewSyncIterationState(),
49+
RateLimiter: nil,
4650
}
4751
}
4852

@@ -96,6 +100,10 @@ func (s *WebhookServer) handleWebhook(w http.ResponseWriter, r *http.Request) {
96100

97101
// Process webhook asynchronously
98102
go func() {
103+
if s.RateLimiter != nil {
104+
s.RateLimiter.Take()
105+
}
106+
99107
err := s.processWebhookEvent(event)
100108
if err != nil {
101109
logCtx.Errorf("Failed to process webhook event: %v", err)

pkg/webhook/server_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ var (
7272
}
7373
)
7474

75+
type mockRateLimiter struct {
76+
Called bool
77+
}
78+
79+
func (m *mockRateLimiter) Take() time.Time {
80+
m.Called = true
81+
return time.Now()
82+
}
83+
7584
// Helper function to create a mock server
7685
func createMockServer(t *testing.T, port int) *WebhookServer {
7786
handler := NewWebhookHandler()
@@ -455,3 +464,39 @@ func TestParseImageList(t *testing.T) {
455464
assert.Equal(t, "baz", imgs[0].KustomizeImage.ImageName)
456465
})
457466
}
467+
468+
// TestWebhookServerRateLimit tests to see if the webhook endpoint's rate limiting functionality works
469+
func TestWebhookServerRateLimit(t *testing.T) {
470+
server := createMockServer(t, 8080)
471+
mockArgoClient := server.ArgoClient.(*mocks.ArgoCD)
472+
mockArgoClient.On("ListApplications", mock.Anything).Return([]v1alpha1.Application{}, nil).Maybe()
473+
474+
handler := NewDockerHubWebhook("")
475+
assert.NotNil(t, handler, "Docker handler was nil")
476+
477+
server.Handler.RegisterHandler(handler)
478+
479+
mock := &mockRateLimiter{}
480+
server.RateLimiter = mock
481+
482+
body := []byte(`{
483+
"repository": {
484+
"repo_name": "somepersononthisfakeregistry/myimagethatdoescoolstuff",
485+
"name": "myimagethatdoescoolstuff",
486+
"namespace": "randomplaceincluster"
487+
},
488+
"push_data": {
489+
"tag": "v12.0.9"
490+
}
491+
}`)
492+
493+
req := httptest.NewRequest(http.MethodPost, "/webhook?type=docker", bytes.NewReader(body))
494+
rec := httptest.NewRecorder()
495+
496+
server.handleWebhook(rec, req)
497+
498+
// Wait for thread to call it.
499+
time.Sleep(time.Second)
500+
501+
assert.True(t, mock.Called, "Take was not called")
502+
}

0 commit comments

Comments
 (0)