Skip to content

Commit f66440c

Browse files
committed
feat: add CloudEvents webhook support for AWS ECR
Add CloudEvents v1.0 webhook handler to support AWS ECR push events via EventBridge, providing a standardized approach without registry-specific handlers. - Add CloudEvents webhook handler with ECR URN parsing - Add comprehensive test suite (60+ tests, all passing) - Add --cloudevents-webhook-secret flag to webhook command - Add documentation and Terraform examples for EventBridge setup - Add trace logging for debugging webhook payloads The EventBridge input transformer converts native ECR events to CloudEvents format, eliminating the need for ECR-specific code. Signed-off-by: Tanjim Hossain <[email protected]>
1 parent 73263e7 commit f66440c

File tree

11 files changed

+1164
-2
lines changed

11 files changed

+1164
-2
lines changed

cmd/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type WebhookConfig struct {
2626
GHCRSecret string
2727
QuaySecret string
2828
HarborSecret string
29+
CloudEventsSecret string
2930
RateLimitNumAllowedRequests int
3031
}
3132

@@ -117,6 +118,7 @@ func SetupWebhookServer(webhookCfg *WebhookConfig, reconciler *controller.ImageU
117118
handler.RegisterHandler(webhook.NewGHCRWebhook(webhookCfg.GHCRSecret))
118119
handler.RegisterHandler(webhook.NewHarborWebhook(webhookCfg.HarborSecret))
119120
handler.RegisterHandler(webhook.NewQuayWebhook(webhookCfg.QuaySecret))
121+
handler.RegisterHandler(webhook.NewCloudEventsWebhook(webhookCfg.CloudEventsSecret))
120122

121123
// Create webhook server
122124
server := webhook.NewWebhookServer(webhookCfg.Port, handler, reconciler)

cmd/run.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ This enables a CRD-driven approach to automated image updates with Argo CD.
304304
controllerCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
305305
controllerCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
306306
controllerCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
307+
controllerCmd.Flags().StringVar(&webhookCfg.CloudEventsSecret, "cloudevents-webhook-secret", env.GetStringVal("CLOUDEVENTS_WEBHOOK_SECRET", ""), "Secret for validating CloudEvents webhooks")
307308
controllerCmd.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")
308309

309310
return controllerCmd

cmd/webhook.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ Supported registries:
4141
- Docker Hub
4242
- GitHub Container Registry (GHCR)
4343
- Quay
44-
- Harbor
44+
- Harbor
45+
- AWS ECR (via EventBridge CloudEvents)
4546
`,
4647
RunE: func(cmd *cobra.Command, args []string) error {
4748
if err := log.SetLogLevel(cfg.LogLevel); err != nil {
@@ -89,6 +90,7 @@ Supported registries:
8990
webhookCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
9091
webhookCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
9192
webhookCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
93+
webhookCmd.Flags().StringVar(&webhookCfg.CloudEventsSecret, "cloudevents-webhook-secret", env.GetStringVal("CLOUDEVENTS_WEBHOOK_SECRET", ""), "Secret for validating CloudEvents webhooks")
9294
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")
9395

9496
return webhookCmd
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# CloudEvents Webhook Example
2+
3+
Example Terraform configuration for setting up AWS EventBridge to send ECR push events to ArgoCD Image Updater via CloudEvents.
4+
5+
## Quick Start
6+
7+
### 1. Configure EventBridge with Terraform
8+
9+
```bash
10+
cd terraform
11+
12+
# Set your variables
13+
export TF_VAR_webhook_url="https://your-domain.com/webhook?type=cloudevents"
14+
export TF_VAR_webhook_secret="your-webhook-secret"
15+
export TF_VAR_aws_region="us-east-1"
16+
17+
# Apply the configuration
18+
terraform init
19+
terraform apply
20+
```
21+
22+
### 2. Test the Webhook
23+
24+
```bash
25+
./test-webhook.sh https://your-webhook-url/webhook?type=cloudevents your-secret
26+
```
27+
28+
## Files
29+
30+
- `terraform/` - EventBridge configuration with input transformer for ECR events
31+
- `test-webhook.sh` - Script to test the webhook endpoint
32+
33+
## Documentation
34+
35+
For complete setup instructions, see the [webhook documentation](../../../docs/configuration/webhook.md#aws-ecr-via-eventbridge-cloudevents).
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
terraform {
2+
required_version = ">= 1.0"
3+
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = "~> 5.0"
8+
}
9+
}
10+
}
11+
12+
provider "aws" {
13+
region = var.aws_region
14+
}
15+
16+
# EventBridge Rule to capture ECR push events
17+
resource "aws_cloudwatch_event_rule" "ecr_push" {
18+
name = "argocd-image-updater-ecr-push"
19+
description = "Capture ECR image push events for ArgoCD Image Updater"
20+
21+
event_pattern = jsonencode({
22+
source = ["aws.ecr"]
23+
detail-type = ["ECR Image Action"]
24+
detail = {
25+
action-type = ["PUSH"]
26+
result = ["SUCCESS"]
27+
# Optional: Filter by specific repositories
28+
repository-name = length(var.ecr_repository_filter) > 0 ? var.ecr_repository_filter : null
29+
}
30+
})
31+
32+
tags = var.tags
33+
}
34+
35+
# EventBridge Connection for API authentication
36+
resource "aws_cloudwatch_event_connection" "webhook" {
37+
name = "argocd-image-updater-webhook"
38+
description = "Connection to ArgoCD Image Updater webhook"
39+
authorization_type = "API_KEY"
40+
41+
auth_parameters {
42+
api_key {
43+
key = "X-Webhook-Secret"
44+
value = var.webhook_secret
45+
}
46+
}
47+
}
48+
49+
# API Destination pointing to ArgoCD Image Updater webhook
50+
resource "aws_cloudwatch_event_api_destination" "webhook" {
51+
name = "argocd-image-updater-webhook"
52+
description = "ArgoCD Image Updater CloudEvents webhook endpoint"
53+
invocation_endpoint = var.webhook_url
54+
http_method = "POST"
55+
invocation_rate_limit_per_second = 10
56+
connection_arn = aws_cloudwatch_event_connection.webhook.arn
57+
}
58+
59+
# IAM Role for EventBridge to invoke API Destination
60+
resource "aws_iam_role" "eventbridge" {
61+
name = "argocd-image-updater-eventbridge-role"
62+
assume_role_policy = data.aws_iam_policy_document.eventbridge_assume_role.json
63+
tags = var.tags
64+
}
65+
66+
data "aws_iam_policy_document" "eventbridge_assume_role" {
67+
statement {
68+
effect = "Allow"
69+
70+
principals {
71+
type = "Service"
72+
identifiers = ["events.amazonaws.com"]
73+
}
74+
75+
actions = ["sts:AssumeRole"]
76+
}
77+
}
78+
79+
# IAM Policy for EventBridge to invoke API Destination
80+
resource "aws_iam_role_policy" "eventbridge_invoke_api_destination" {
81+
name = "invoke-api-destination"
82+
role = aws_iam_role.eventbridge.id
83+
policy = data.aws_iam_policy_document.eventbridge_invoke_api_destination.json
84+
}
85+
86+
data "aws_iam_policy_document" "eventbridge_invoke_api_destination" {
87+
statement {
88+
effect = "Allow"
89+
90+
actions = [
91+
"events:InvokeApiDestination"
92+
]
93+
94+
resources = [
95+
aws_cloudwatch_event_api_destination.webhook.arn
96+
]
97+
}
98+
}
99+
100+
# EventBridge Target with Input Transformer (ECR -> CloudEvents)
101+
resource "aws_cloudwatch_event_target" "api_destination" {
102+
rule = aws_cloudwatch_event_rule.ecr_push.name
103+
target_id = "ArgocdImageUpdaterCloudEvent"
104+
arn = aws_cloudwatch_event_api_destination.webhook.arn
105+
role_arn = aws_iam_role.eventbridge.arn
106+
107+
input_transformer {
108+
input_paths = {
109+
id = "$.id"
110+
time = "$.time"
111+
account = "$.account"
112+
region = "$.region"
113+
repo = "$.detail.repository-name"
114+
digest = "$.detail.image-digest"
115+
tag = "$.detail.image-tag"
116+
}
117+
118+
input_template = <<-EOF
119+
{
120+
"specversion": "1.0",
121+
"id": "<id>",
122+
"type": "com.amazon.ecr.image.push",
123+
"source": "urn:aws:ecr:<region>:<account>:repository/<repo>",
124+
"subject": "<repo>:<tag>",
125+
"time": "<time>",
126+
"datacontenttype": "application/json",
127+
"data": {
128+
"repositoryName": "<repo>",
129+
"imageDigest": "<digest>",
130+
"imageTag": "<tag>",
131+
"registryId": "<account>"
132+
}
133+
}
134+
EOF
135+
}
136+
137+
retry_policy {
138+
maximum_event_age_in_seconds = 3600
139+
maximum_retry_attempts = 3
140+
}
141+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
output "eventbridge_rule_arn" {
2+
description = "ARN of the EventBridge rule"
3+
value = aws_cloudwatch_event_rule.ecr_push.arn
4+
}
5+
6+
output "api_destination_arn" {
7+
description = "ARN of the API destination"
8+
value = aws_cloudwatch_event_api_destination.webhook.arn
9+
}
10+
11+
output "eventbridge_role_arn" {
12+
description = "ARN of the EventBridge IAM role"
13+
value = aws_iam_role.eventbridge.arn
14+
}
15+
16+
output "webhook_endpoint" {
17+
description = "Configured webhook endpoint URL"
18+
value = var.webhook_url
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
variable "aws_region" {
2+
description = "AWS region for ECR and EventBridge"
3+
type = string
4+
default = "us-east-1"
5+
}
6+
7+
variable "webhook_url" {
8+
description = "ArgoCD Image Updater webhook endpoint URL"
9+
type = string
10+
# Example: "https://image-updater-webhook.example.com/webhook?type=cloudevents"
11+
}
12+
13+
variable "webhook_secret" {
14+
description = "Secret for webhook authentication"
15+
type = string
16+
sensitive = true
17+
}
18+
19+
variable "ecr_repository_filter" {
20+
description = "List of ECR repository names to monitor (empty list = all repositories)"
21+
type = list(string)
22+
default = []
23+
}
24+
25+
variable "tags" {
26+
description = "Tags to apply to all resources"
27+
type = map(string)
28+
default = {
29+
ManagedBy = "Terraform"
30+
Purpose = "ArgoCD-Image-Updater"
31+
}
32+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/bin/bash
2+
# Test script for CloudEvents webhook
3+
# Usage: ./test-cloudevents-webhook.sh <webhook-url> <secret>
4+
5+
set -e
6+
7+
WEBHOOK_URL="${1:-http://localhost:8080/webhook?type=cloudevents}"
8+
SECRET="${2:-test-secret}"
9+
10+
echo "Testing CloudEvents webhook..."
11+
echo "URL: ${WEBHOOK_URL}"
12+
echo ""
13+
14+
# Test 1: ECR event
15+
echo "Test 1: AWS ECR push event"
16+
curl -X POST "${WEBHOOK_URL}" \
17+
-H "Content-Type: application/json" \
18+
-H "X-Webhook-Secret: ${SECRET}" \
19+
-d '{
20+
"specversion": "1.0",
21+
"id": "test-ecr-event-001",
22+
"type": "com.amazon.ecr.image.push",
23+
"source": "urn:aws:ecr:us-east-1:123456789012:repository/my-app",
24+
"subject": "my-app:v1.2.3",
25+
"time": "2025-11-27T10:00:00Z",
26+
"datacontenttype": "application/json",
27+
"data": {
28+
"repositoryName": "my-app",
29+
"imageDigest": "sha256:abcdef1234567890",
30+
"imageTag": "v1.2.3",
31+
"registryId": "123456789012"
32+
}
33+
}' \
34+
-w "\nHTTP Status: %{http_code}\n\n"
35+
36+
# Test 2: Generic container event
37+
echo "Test 2: Generic container push event"
38+
curl -X POST "${WEBHOOK_URL}" \
39+
-H "Content-Type: application/cloudevents+json" \
40+
-H "X-Webhook-Secret: ${SECRET}" \
41+
-d '{
42+
"specversion": "1.0",
43+
"id": "test-generic-event-001",
44+
"type": "container.image.push",
45+
"source": "https://registry.example.com",
46+
"subject": "myapp/backend:v2.0.0",
47+
"time": "2025-11-27T10:05:00Z",
48+
"datacontenttype": "application/json",
49+
"data": {
50+
"repository": "myapp/backend",
51+
"tag": "v2.0.0",
52+
"digest": "sha256:fedcba0987654321",
53+
"registryUrl": "registry.example.com"
54+
}
55+
}' \
56+
-w "\nHTTP Status: %{http_code}\n\n"
57+
58+
# Test 3: Missing secret (should fail)
59+
echo "Test 3: Missing secret (expected to fail)"
60+
curl -X POST "${WEBHOOK_URL}" \
61+
-H "Content-Type: application/json" \
62+
-d '{
63+
"specversion": "1.0",
64+
"id": "test-fail-event-001",
65+
"type": "com.amazon.ecr.image.push",
66+
"source": "urn:aws:ecr:us-east-1:123456789012:repository/test",
67+
"subject": "test:latest",
68+
"data": {
69+
"repositoryName": "test",
70+
"imageTag": "latest",
71+
"registryId": "123456789012"
72+
}
73+
}' \
74+
-w "\nHTTP Status: %{http_code}\n\n" || echo "Expected failure"
75+
76+
# Test 4: Invalid event type (should fail)
77+
echo "Test 4: Invalid event type (expected to fail)"
78+
curl -X POST "${WEBHOOK_URL}" \
79+
-H "Content-Type: application/json" \
80+
-H "X-Webhook-Secret: ${SECRET}" \
81+
-d '{
82+
"specversion": "1.0",
83+
"id": "test-invalid-event-001",
84+
"type": "com.example.database.updated",
85+
"source": "https://db.example.com",
86+
"data": {
87+
"table": "users"
88+
}
89+
}' \
90+
-w "\nHTTP Status: %{http_code}\n\n" || echo "Expected failure"
91+
92+
echo "Testing complete!"

0 commit comments

Comments
 (0)