From aaa02b54fa5baaab77558440f8acc31cb091170c Mon Sep 17 00:00:00 2001 From: Tanjim Hossain Date: Thu, 27 Nov 2025 16:32:40 +0000 Subject: [PATCH 01/17] feat: add CloudEvents webhook support for AWS ECR Adds CloudEvents v1.0 webhook handler to support AWS ECR push events via EventBridge, using the CloudEvents specification for a standardized approach instead of registry-specific handlers. Changes: - Add pkg/webhook/cloudevents.go with CloudEvents webhook handler - Add pkg/webhook/cloudevents_test.go with comprehensive test suite (60+ tests) - Add --cloudevents-webhook-secret flag to webhook/run commands - Add documentation in docs/configuration/webhook.md - Add Terraform example for EventBridge setup - Add trace logging for debugging webhook payloads - Use crypto/subtle.ConstantTimeCompare for secret validation Signed-off-by: Tanjim Hossain --- cmd/common.go | 2 + cmd/run.go | 1 + cmd/webhook.go | 4 +- config/examples/cloudevents/README.md | 38 ++ config/examples/cloudevents/terraform/main.tf | 145 +++++ .../examples/cloudevents/terraform/outputs.tf | 19 + .../cloudevents/terraform/variables.tf | 32 ++ config/examples/cloudevents/test-webhook.sh | 92 ++++ docs/configuration/webhook.md | 72 ++- pkg/webhook/cloudevents.go | 268 ++++++++++ pkg/webhook/cloudevents_test.go | 495 ++++++++++++++++++ 11 files changed, 1166 insertions(+), 2 deletions(-) create mode 100644 config/examples/cloudevents/README.md create mode 100644 config/examples/cloudevents/terraform/main.tf create mode 100644 config/examples/cloudevents/terraform/outputs.tf create mode 100644 config/examples/cloudevents/terraform/variables.tf create mode 100755 config/examples/cloudevents/test-webhook.sh create mode 100644 pkg/webhook/cloudevents.go create mode 100644 pkg/webhook/cloudevents_test.go diff --git a/cmd/common.go b/cmd/common.go index 238319c6..5328536f 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -26,6 +26,7 @@ type WebhookConfig struct { GHCRSecret string QuaySecret string HarborSecret string + CloudEventsSecret string RateLimitNumAllowedRequests int } @@ -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) diff --git a/cmd/run.go b/cmd/run.go index a1a13558..296cbd3f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -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 diff --git a/cmd/webhook.go b/cmd/webhook.go index 9fceb206..ff38784a 100644 --- a/cmd/webhook.go +++ b/cmd/webhook.go @@ -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 { @@ -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 diff --git a/config/examples/cloudevents/README.md b/config/examples/cloudevents/README.md new file mode 100644 index 00000000..d65f4423 --- /dev/null +++ b/config/examples/cloudevents/README.md @@ -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). diff --git a/config/examples/cloudevents/terraform/main.tf b/config/examples/cloudevents/terraform/main.tf new file mode 100644 index 00000000..449af207 --- /dev/null +++ b/config/examples/cloudevents/terraform/main.tf @@ -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": "", + "type": "com.amazon.ecr.image.push", + "source": "urn:aws:ecr:::repository/", + "subject": ":", + "time": "