Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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
19 changes: 14 additions & 5 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ import (

// WebhookConfig holds the options for the webhook server
type WebhookConfig struct {
Port int
DockerSecret string
GHCRSecret string
QuaySecret string
HarborSecret string
// Port is the port number for the webhook server to listen on
Port int
// DockerSecret is the secret for validating Docker Hub webhooks
DockerSecret string
// GHCRSecret is the secret for validating GitHub Container Registry webhooks
GHCRSecret string
// QuaySecret is the secret for validating Quay webhooks
QuaySecret string
// HarborSecret is the secret for validating Harbor webhooks
HarborSecret string
// CloudEventsSecret is the secret for validating CloudEvents webhooks
CloudEventsSecret string
// RateLimitNumAllowedRequests is the number of allowed requests per hour for rate limiting
RateLimitNumAllowedRequests int
}

Expand Down Expand Up @@ -117,6 +125,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
Loading