Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type WebhookConfig struct {
GHCRSecret string
QuaySecret string
HarborSecret string
CloudEventsSecret string
RateLimitNumAllowedRequests int
}

Expand Down Expand Up @@ -117,6 +118,7 @@ func SetupWebhookServer(webhookCfg *WebhookConfig, reconciler *controller.ImageU
handler.RegisterHandler(webhook.NewGHCRWebhook(webhookCfg.GHCRSecret))
handler.RegisterHandler(webhook.NewHarborWebhook(webhookCfg.HarborSecret))
handler.RegisterHandler(webhook.NewQuayWebhook(webhookCfg.QuaySecret))
handler.RegisterHandler(webhook.NewCloudEventsWebhook(webhookCfg.CloudEventsSecret))

// Create webhook server
server := webhook.NewWebhookServer(webhookCfg.Port, handler, reconciler)
Expand Down
1 change: 1 addition & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ This enables a CRD-driven approach to automated image updates with Argo CD.
controllerCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
controllerCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
controllerCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
controllerCmd.Flags().StringVar(&webhookCfg.CloudEventsSecret, "cloudevents-webhook-secret", env.GetStringVal("CLOUDEVENTS_WEBHOOK_SECRET", ""), "Secret for validating CloudEvents webhooks")
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")

return controllerCmd
Expand Down
4 changes: 3 additions & 1 deletion cmd/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ Supported registries:
- Docker Hub
- GitHub Container Registry (GHCR)
- Quay
- Harbor
- Harbor
- AWS ECR (via EventBridge CloudEvents)
`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := log.SetLogLevel(cfg.LogLevel); err != nil {
Expand Down Expand Up @@ -89,6 +90,7 @@ Supported registries:
webhookCmd.Flags().StringVar(&webhookCfg.GHCRSecret, "ghcr-webhook-secret", env.GetStringVal("GHCR_WEBHOOK_SECRET", ""), "Secret for validating GitHub Container Registry webhooks")
webhookCmd.Flags().StringVar(&webhookCfg.QuaySecret, "quay-webhook-secret", env.GetStringVal("QUAY_WEBHOOK_SECRET", ""), "Secret for validating Quay webhooks")
webhookCmd.Flags().StringVar(&webhookCfg.HarborSecret, "harbor-webhook-secret", env.GetStringVal("HARBOR_WEBHOOK_SECRET", ""), "Secret for validating Harbor webhooks")
webhookCmd.Flags().StringVar(&webhookCfg.CloudEventsSecret, "cloudevents-webhook-secret", env.GetStringVal("CLOUDEVENTS_WEBHOOK_SECRET", ""), "Secret for validating CloudEvents webhooks")
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")

return webhookCmd
Expand Down
38 changes: 38 additions & 0 deletions config/examples/cloudevents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CloudEvents Webhook Example

Example Terraform configuration for setting up AWS EventBridge to send ECR push events to ArgoCD Image Updater via CloudEvents.

## Quick Start

### 1. Configure EventBridge with Terraform

```bash
cd terraform

# Set your variables
export TF_VAR_webhook_url="https://your-domain.com/webhook?type=cloudevents"
export TF_VAR_webhook_secret="your-webhook-secret"
export TF_VAR_aws_region="us-east-1"

# Apply the configuration
terraform init
terraform apply

# Return to parent directory
cd ..
```

### 2. Test the Webhook

```bash
./test-webhook.sh https://your-webhook-url/webhook?type=cloudevents your-secret
```

## Files

- `terraform/` - EventBridge configuration with input transformer for ECR events
- `test-webhook.sh` - Script to test the webhook endpoint

## Documentation

For complete setup instructions, see the [webhook documentation](../../../docs/configuration/webhook.md#aws-ecr-via-eventbridge-cloudevents).
145 changes: 145 additions & 0 deletions config/examples/cloudevents/terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
terraform {
required_version = ">= 1.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

provider "aws" {
region = var.aws_region
}

# EventBridge Rule to capture ECR push events
resource "aws_cloudwatch_event_rule" "ecr_push" {
name = "argocd-image-updater-ecr-push"
description = "Capture ECR image push events for ArgoCD Image Updater"

event_pattern = jsonencode({
source = ["aws.ecr"]
detail-type = ["ECR Image Action"]
detail = {
action-type = ["PUSH"]
result = ["SUCCESS"]
# Filter for events with image tags (excludes untagged/manifest-only pushes)
image-tag = [{
exists = true
}]
# Optional: Filter by specific repositories
repository-name = length(var.ecr_repository_filter) > 0 ? var.ecr_repository_filter : null
}
})

tags = var.tags
}

# EventBridge Connection for API authentication
resource "aws_cloudwatch_event_connection" "webhook" {
name = "argocd-image-updater-webhook"
description = "Connection to ArgoCD Image Updater webhook"
authorization_type = "API_KEY"

auth_parameters {
api_key {
key = "X-Webhook-Secret"
value = var.webhook_secret
}
}
}

# API Destination pointing to ArgoCD Image Updater webhook
resource "aws_cloudwatch_event_api_destination" "webhook" {
name = "argocd-image-updater-webhook"
description = "ArgoCD Image Updater CloudEvents webhook endpoint"
invocation_endpoint = var.webhook_url
http_method = "POST"
invocation_rate_limit_per_second = 10
connection_arn = aws_cloudwatch_event_connection.webhook.arn
}

# IAM Role for EventBridge to invoke API Destination
resource "aws_iam_role" "eventbridge" {
name = "argocd-image-updater-eventbridge-role"
assume_role_policy = data.aws_iam_policy_document.eventbridge_assume_role.json
tags = var.tags
}

data "aws_iam_policy_document" "eventbridge_assume_role" {
statement {
effect = "Allow"

principals {
type = "Service"
identifiers = ["events.amazonaws.com"]
}

actions = ["sts:AssumeRole"]
}
}

# IAM Policy for EventBridge to invoke API Destination
resource "aws_iam_role_policy" "eventbridge_invoke_api_destination" {
name = "invoke-api-destination"
role = aws_iam_role.eventbridge.id
policy = data.aws_iam_policy_document.eventbridge_invoke_api_destination.json
}

data "aws_iam_policy_document" "eventbridge_invoke_api_destination" {
statement {
effect = "Allow"

actions = [
"events:InvokeApiDestination"
]

resources = [
aws_cloudwatch_event_api_destination.webhook.arn
]
}
}

# EventBridge Target with Input Transformer (ECR -> CloudEvents)
resource "aws_cloudwatch_event_target" "api_destination" {
rule = aws_cloudwatch_event_rule.ecr_push.name
target_id = "ArgocdImageUpdaterCloudEvent"
arn = aws_cloudwatch_event_api_destination.webhook.arn
role_arn = aws_iam_role.eventbridge.arn

input_transformer {
input_paths = {
id = "$.id"
time = "$.time"
account = "$.account"
region = "$.region"
repo = "$.detail.repository-name"
digest = "$.detail.image-digest"
tag = "$.detail.image-tag"
}

input_template = <<-EOF
{
"specversion": "1.0",
"id": "<id>",
"type": "com.amazon.ecr.image.push",
"source": "urn:aws:ecr:<region>:<account>:repository/<repo>",
"subject": "<repo>:<tag>",
"time": "<time>",
"datacontenttype": "application/json",
"data": {
"repositoryName": "<repo>",
"imageDigest": "<digest>",
"imageTag": "<tag>",
"registryId": "<account>"
}
}
EOF
}

retry_policy {
maximum_event_age_in_seconds = 3600
maximum_retry_attempts = 3
}
}
19 changes: 19 additions & 0 deletions config/examples/cloudevents/terraform/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "eventbridge_rule_arn" {
description = "ARN of the EventBridge rule"
value = aws_cloudwatch_event_rule.ecr_push.arn
}

output "api_destination_arn" {
description = "ARN of the API destination"
value = aws_cloudwatch_event_api_destination.webhook.arn
}

output "eventbridge_role_arn" {
description = "ARN of the EventBridge IAM role"
value = aws_iam_role.eventbridge.arn
}

output "webhook_endpoint" {
description = "Configured webhook endpoint URL"
value = var.webhook_url
}
32 changes: 32 additions & 0 deletions config/examples/cloudevents/terraform/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
variable "aws_region" {
description = "AWS region for ECR and EventBridge"
type = string
default = "us-east-1"
}

variable "webhook_url" {
description = "ArgoCD Image Updater webhook endpoint URL"
type = string
# Example: "https://image-updater-webhook.example.com/webhook?type=cloudevents"
}

variable "webhook_secret" {
description = "Secret for webhook authentication"
type = string
sensitive = true
}

variable "ecr_repository_filter" {
description = "List of ECR repository names to monitor (empty list = all repositories)"
type = list(string)
default = []
}

variable "tags" {
description = "Tags to apply to all resources"
type = map(string)
default = {
ManagedBy = "Terraform"
Purpose = "ArgoCD-Image-Updater"
}
}
92 changes: 92 additions & 0 deletions config/examples/cloudevents/test-webhook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/bin/bash
# Test script for CloudEvents webhook
# Usage: ./test-webhook.sh <webhook-url> <secret>

set -e

WEBHOOK_URL="${1:-http://localhost:8080/webhook?type=cloudevents}"
SECRET="${2:-test-secret}"

echo "Testing CloudEvents webhook..."
echo "URL: ${WEBHOOK_URL}"
echo ""

# Test 1: ECR event
echo "Test 1: AWS ECR push event"
curl -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: ${SECRET}" \
-d '{
"specversion": "1.0",
"id": "test-ecr-event-001",
"type": "com.amazon.ecr.image.push",
"source": "urn:aws:ecr:us-east-1:123456789012:repository/my-app",
"subject": "my-app:v1.2.3",
"time": "2025-11-27T10:00:00Z",
"datacontenttype": "application/json",
"data": {
"repositoryName": "my-app",
"imageDigest": "sha256:abcdef1234567890",
"imageTag": "v1.2.3",
"registryId": "123456789012"
}
}' \
-w "\nHTTP Status: %{http_code}\n\n"

# Test 2: Generic container event
echo "Test 2: Generic container push event"
curl -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/cloudevents+json" \
-H "X-Webhook-Secret: ${SECRET}" \
-d '{
"specversion": "1.0",
"id": "test-generic-event-001",
"type": "container.image.push",
"source": "https://registry.example.com",
"subject": "myapp/backend:v2.0.0",
"time": "2025-11-27T10:05:00Z",
"datacontenttype": "application/json",
"data": {
"repository": "myapp/backend",
"tag": "v2.0.0",
"digest": "sha256:fedcba0987654321",
"registryUrl": "registry.example.com"
}
}' \
-w "\nHTTP Status: %{http_code}\n\n"

# Test 3: Missing secret (should fail)
echo "Test 3: Missing secret (expected to fail)"
curl -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-d '{
"specversion": "1.0",
"id": "test-fail-event-001",
"type": "com.amazon.ecr.image.push",
"source": "urn:aws:ecr:us-east-1:123456789012:repository/test",
"subject": "test:latest",
"data": {
"repositoryName": "test",
"imageTag": "latest",
"registryId": "123456789012"
}
}' \
-w "\nHTTP Status: %{http_code}\n\n" || echo "Expected failure"

# Test 4: Invalid event type (should fail)
echo "Test 4: Invalid event type (expected to fail)"
curl -X POST "${WEBHOOK_URL}" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: ${SECRET}" \
-d '{
"specversion": "1.0",
"id": "test-invalid-event-001",
"type": "com.example.database.updated",
"source": "https://db.example.com",
"data": {
"table": "users"
}
}' \
-w "\nHTTP Status: %{http_code}\n\n" || echo "Expected failure"

echo "Testing complete!"
Loading