Transform ArgoCD notification webhooks into CDEvents.
This transformer converts ArgoCD application lifecycle events (deployments, sync operations, health status) into standardized CDEvents following the ArgoCD CDEvents Integration Proposal.
The transformer uses a simplified configuration approach: one ArgoCD notification template for all event types, with event detection logic implemented in VRL. This keeps ArgoCD configuration minimal while providing full transformation flexibility.
Two chained transformers process ArgoCD webhooks:
- argocd_metadata: Injects
environment_idfromapp.spec.destination.server - argocd_notifications: Detects event type and transforms to CDEvents
Event type detection is performed in VRL based on payload fields:
| ArgoCD Condition | CDEvent Type | Detection Logic |
|---|---|---|
| Application deleted | service.removed | metadata.deletionTimestamp exists |
| Sync succeeded + Healthy | service.deployed | operationState.phase=Succeeded AND health.status=Healthy |
| Sync failed/error | incident.detected | operationState.phase in [Failed, Error] |
| Health degraded | incident.detected | health.status in [Degraded, Missing, Unknown] |
| Health healthy (standalone) | service.deployed | health.status=Healthy (no operation state) |
| Sync running | (skipped) | operationState.phase=Running |
For service.deployed events, artifactId is generated from the ArgoCD application's source:
- Helm Chart:
pkg:helm/<chart>@<version>?repository_url=<encoded_url> - Git Repository:
pkg:git/<app_name>@<revision>?repository_url=<encoded_url>
The transformer uses operationState.syncResult.sources (completed sync) or falls back to spec.source (other states).
To capture authentic ArgoCD webhook payloads for testing:
- Visit webhook.site
- Copy your unique URL (e.g.,
https://webhook.site/abc123-def456-...) - Keep the browser tab open to monitor incoming webhooks
Edit the argocd-notifications-cm ConfigMap:
kubectl edit configmap argocd-notifications-cm -n argocdAdd webhook service configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
# For testing: register webhook.site as a webhook service
service.webhook.webhook-site: |
url: https://webhook.site/YOUR-UNIQUE-ID
headers:
- name: Content-Type
value: application/json
# For production: register your cdviz-collector endpoint
service.webhook.cdviz: |
url: https://your-cdviz-collector.example.com/webhook/000-argocd
headers:
- name: Content-Type
value: application/json
# Optional: Add authentication
# - name: Authorization
# value: Bearer YOUR-TOKENAdd one template for all event types (event detection happens in VRL):
# Unified template that sends full application state for all events
template.webhook-cdviz: |
webhook:
cdviz: # or webhook-site for testing
method: POST
body: |
{
"timestamp": "{{(call .time.Now).Format \"2006-01-02T15:04:05.000000-07:00\"}}",
"context": {{toJson .context}},
"app": {{toJson .app}}
}Key points:
- Single template for all events keeps ArgoCD configuration simple
- Event type detection is handled by VRL transformer logic
- Full
.appobject provides all necessary state for transformation
Add triggers to send webhooks on status changes:
# Trigger on any sync operation state change
trigger.on-sync-status-unknown: |
- when: app.status.operationState.phase in ['Running', 'Failed', 'Error', 'Succeeded']
send: [webhook-cdviz]
# Trigger on health status changes
trigger.on-health-status: |
- when: app.status.health.status != nil
send: [webhook-cdviz]
# Trigger on application deletion
trigger.on-app-deleted: |
- when: app.metadata.deletionTimestamp != nil
send: [webhook-cdviz]Alternative: Use defaultTriggers for automatic notifications:
# Apply to all applications by default
defaultTriggers: |
- on-sync-status-unknown
- on-health-status
- on-app-deletedOption A: Use default triggers (recommended)
If you configured defaultTriggers above, all applications automatically send notifications. No per-app configuration needed.
Option B: Per-application subscription
Add annotations to specific applications:
kubectl annotate app <app-name> -n argocd \
notifications.argoproj.io/subscribe.on-sync-status-unknown.cdviz="" \
notifications.argoproj.io/subscribe.on-health-status.cdviz="" \
notifications.argoproj.io/subscribe.on-app-deleted.cdviz=""Or in Application manifest:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
annotations:
notifications.argoproj.io/subscribe.on-sync-status-unknown.cdviz: ""
notifications.argoproj.io/subscribe.on-health-status.cdviz: ""
notifications.argoproj.io/subscribe.on-app-deleted.cdviz: ""
spec:
# ... your application specsee Troubleshooting commands - Notifications
# after login
# argocd login ARGOCD_HOST --grpc-web-root-path / --username admin --grpc-web
# argocd admin notifications template notify NAME RESOURCE_NAME [flags]
argocd admin notifications template notify webhook-cdviz podinfo -n argocdwebhook:
cdviz:
body: |
{
"timestamp": "2025-10-02T17:45:00.442161+02:00",
"context": {"argocdUrl":"https://demo.cdviz.dev","notificationType":"console"},
"app": {"apiVersion":"argoproj.io/v1alpha1","kind":"Application","metadata":{"annotations":{"argocd.argoproj.io/tracking-id":"bootstrap:argoproj.io/Application:argocd/podinfo"},"creationTimestamp":"2025-10-02T15:32:45Z","generation":2,"labels":{"app.kubernetes.io/managed-by":"argocd","cdviz.dev/cluster":"demo-cluster"},"managedFields":[{"apiVersion":"argoproj.io/v1alpha1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{"f:argocd.argoproj.io/tracking-id":{}},"f:labels":{"f:app.kubernetes.io/managed-by":{},"f:cdviz.dev/cluster":{}}},"f:spec":{"f:destination":{"f:namespace":{},"f:server":{}},"f:project":{},"f:revisionHistoryLimit":{},"f:sources":{},"f:syncPolicy":{"f:automated":{"f:prune":{},"f:selfHeal":{}},"f:retry":{"f:backoff":{"f:duration":{},"f:factor":{},"f:maxDuration":{}},"f:limit":{}},"f:syncOptions":{}}}},"manager":"argocd-controller","operation":"Apply","time":"2025-10-02T15:32:45Z"},{"apiVersion":"argoproj.io/v1alpha1","fieldsType":"FieldsV1","fieldsV1":{"f:status":{".":{},"f:conditions":{},"f:health":{".":{},"f:lastTransitionTime":{},"f:status":{}},"f:sync":{".":{},"f:status":{}}}},"manager":"argocd-application-controller","operation":"Update","time":"2025-10-02T15:32:45Z"}],"name":"podinfo","namespace":"argocd","resourceVersion":"70725964","uid":"648b3973-e90b-4496-84c0-febef8536eec"},"spec":{"destination":{"namespace":"podinfo","server":"https://kubernetes.default.svc"},"project":"demo-cluster","revisionHistoryLimit":3,"sources":[{"chart":"podinfo","repoURL":"oci://ghcr.io/stefanprodan/charts/","targetRevision":"6.9.0"}],"syncPolicy":{"automated":{"prune":true,"selfHeal":true},"retry":{"backoff":{"duration":"5s","factor":2,"maxDuration":"3m"},"limit":5},"syncOptions":["CreateNamespace=true","ServerSideApply=true","ApplyOutOfSyncOnly=true"]}},"status":{"conditions":[{"lastTransitionTime":"2025-10-02T15:32:45Z","message":"application repo oci://ghcr.io/stefanprodan/charts/ is not permitted in project 'demo-cluster'","type":"InvalidSpecError"}],"health":{"lastTransitionTime":"2025-10-02T15:32:45Z","status":"Unknown"},"sync":{"status":"Unknown"}}}
}
method: POST
path: ""Check trigger run
# argocd admin notifications trigger run NAME RESOURCE_NAME [flags]
argocd admin notifications trigger run on-sync-status-unknown podinfoIf you have the error failed to get api: secrets "argocd-notifications-secret" not found it's caused by searching the secrets (& the app) in the default namespace, use -n ... to specify the namespace.
Perform actions to generate webhook events:
# Trigger a sync
argocd app sync <app-name>
# Force a sync (might cause failures for testing)
argocd app sync <app-name> --force
# Delete an application (for testing removal)
argocd app delete <app-name>- Go to your webhook.site browser tab
- Click on each webhook request to view the full payload
- Copy the JSON body
- Save to
inputs/captured/directory with descriptive names
Example file naming:
inputs/captured/000_template_test.json- CLI template test outputinputs/captured/001_sync_running.json- Sync operation startedinputs/captured/012_sync_succeeded.json- Successful deploymentinputs/captured/015_sync_failed.json- Failed deploymentinputs/captured/018_health_degraded.json- Health issues detectedinputs/captured/021_app_deleted.json- Application deletion
Once you have real webhook payloads:
# Generate expected outputs (overwrites existing outputs)
cd transformers/argocd_notifications
../../target/debug/cdviz-collector transform \
--mode overwrite \
--directory . \
--config ./cdviz-collector.toml \
-t argocd_metadata,argocd_notifications \
--input ./inputs/captured \
--output ./outputs/captured
# Or use mise task for check mode
mise run test:transform:argocd_notificationsNote: Some inputs may not generate outputs (e.g., sync running events are skipped). This is expected behavior.
# Check transformation against expected outputs (validates CDEvents)
mise run test:transform:argocd_notifications
# Generate/overwrite expected outputs (after adding new inputs)
cd transformers/argocd_notifications
../../target/debug/cdviz-collector transform \
--mode overwrite \
--directory . \
--config ./cdviz-collector.toml \
-t argocd_metadata,argocd_notifications \
--input ./inputs/captured \
--output ./outputs/capturedConfigure collector to accept ArgoCD webhooks (see example in cdviz-collector.toml), then start:
mise run runSend test events:
# Using curl with a captured webhook
curl -X POST http://localhost:8080/webhook/000-argocd \
-H "Content-Type: application/json" \
-d @transformers/argocd_notifications/inputs/captured/012_sync_podinfo_2_2.jsonThe transformer is configured via cdviz-collector.toml with two chained transformers:
# Metadata transformer injects environment_id from ArgoCD destination
[transformers.argocd_metadata]
type = "vrl"
template = """
.metadata = object(.metadata) ?? {}
[{
"metadata": merge(.metadata, {
"environment_id": .body.app.spec.destination.server || "unknown",
}),
"headers": .headers,
"body": .body,
}]
"""
# Main transformer detects event type and transforms to CDEvents
[transformers]
argocd_notifications = { type = "vrl", template_file = "./to_v0_4.vrl"}Webhook source configuration (in main cdviz-collector.toml):
[sources.argocd_webhook]
enabled = true
transformer_refs = ["argocd_metadata", "argocd_notifications"]
[sources.argocd_webhook.extractor]
type = "webhook"
id = "000-argocd"
headers_to_keep = []
[sources.argocd_webhook.extractor.headers]
# Optional: Verify webhook authenticity with signature
# "authorization" = { type = "signature", signature_encoding = "base64", signature_on = "body", signature_prefix = "Bearer ", token = "changeme" }Key transformation rules (aligned with kubewatch transformer):
- Transformer chaining:
argocd_metadata→argocd_notifications - Event detection: VRL logic determines event type from payload fields
- context.id: Auto-generated by collector (set to "0")
- context.source:
/argocd(following kubewatch pattern) - subject.id:
namespace/app_nameformat - subject.content.artifactId: PURL for Helm chart or Git source
- environment.id: Injected by argocd_metadata from
app.spec.destination.server - customData.argocd: Preserves ArgoCD-specific operation, health, and sync details
-
Verify ArgoCD notifications controller is running:
kubectl get pods -n argocd | grep notifications -
Check notifications controller logs:
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-notifications-controller
-
Verify webhook service configuration:
kubectl get configmap argocd-notifications-cm -n argocd -o yaml
-
Check application annotations:
kubectl get app <app-name> -n argocd -o yaml | grep notifications
If webhook.site shows errors:
- Verify JSON syntax in templates using
{{toJson .app}} - Check for proper escaping of special characters
- Test templates with
argocd admin notifications template notifycommand
- ArgoCD Notifications Documentation
- ArgoCD Webhook Service
- ArgoCD CDEvents Integration Proposal
- CDEvents Specification
- webhook.site - Free webhook testing tool
The inputs/captured/ directory contains real ArgoCD webhook payloads captured during testing:
000_cli_template.json- Result ofargocd admin notifications template notify webhook-cdviz podinfo -n argocd001-006.json- Initial events after ArgoCD notifications configuration007-010.json- Events during failed deployment (PodSecurity violations)011-013.json- Events during failed deployment (value type errors)014-017.json- Events from successful deployment (Podinfo 3.3)018-020.json- Events from successful upgrade (Podinfo 4.2)021.json- Events from application deletion via Git022.json- Events from manual prune operation
Simplified ArgoCD Configuration: One template for all event types keeps ArgoCD configuration minimal and maintainable. Event type detection is centralized in VRL logic where it can be versioned, tested, and evolved independently.
Benefits:
- Easier to maintain: single source of truth for webhook payload format
- Simpler troubleshooting: all events have identical structure
- Flexible event detection: VRL can implement complex logic based on multiple fields
- Easier testing: one payload format to validate
ArgoCD Limitation: Unlike Kubewatch (which monitors Kubernetes resources directly), ArgoCD webhooks provide application-level state without detailed container/image information. The webhook payload includes:
- ✅ Application source (Helm chart, Git repository, revision)
- ✅ Sync operation state and results
- ✅ Overall health status
- ❌ Individual container images and digests
- ❌ Pod-level deployment details
Result: Transformer emits one CDEvent per ArgoCD Application (using Helm chart or Git repo as artifactId) rather than per-container events like Kubewatch.
Separation of Concerns:
argocd_metadata: Infrastructure-level metadata injection (environment_id from destination cluster)argocd_notifications: Business logic for event type detection and CDEvent generation
This pattern follows the Kubewatch transformer design and allows each transformer to focus on a single responsibility.