Configure EventListener automatic exposure rules

TOC

What This Document Helps You Do

This document helps you configure automatic external exposure of EventListeners through the automatic exposure feature. You will learn:

  • How to configure export rules to automatically expose EventListeners via Ingress
  • How to set up webhook URLs that will be displayed in the UI
  • How to configure different exposure strategies for different namespaces or environments
  • How to verify that EventListeners are properly exposed

When to use this: Use this feature when you want the system to automatically create Ingress resources for your EventListeners, eliminating the need to manually create and manage Ingress resources. This is especially useful in production environments where you need consistent webhook URL patterns.

Prerequisites: You should have cluster administrator privileges to configure TektonConfig resources, and you should have a basic understanding of Kubernetes Ingress and networking concepts.

Feature overview

The automatic exposure controller (internally named trigger-wrapper) reads the trigger-wrapper-config ConfigMap in the Tekton system namespace (default: tekton-pipelines). The export-rules defined in that ConfigMap determine which EventListeners should be exposed externally (Service, Ingress, etc.) and populate the EventListener/Trigger status with the generated endpoints.

INFO

Technical Note: The internal component name is trigger-wrapper, but you don't need to remember this. You only need to configure the export rules through TektonConfig, and the system will handle the rest automatically.

Configuration entry point

On Alauda Tekton, it is recommended to manage this configuration through the TektonConfig custom resource. Embed the rule definitions under spec.pipeline.options.configMaps.trigger-wrapper-config.data.config, for example:

apiVersion: operator.tekton.dev/v1alpha1
kind: TektonConfig
metadata:
  name: config
spec:
  pipeline:
    options:
      configMaps:
        trigger-wrapper-config:
          data:
            config: |
              export-rules:
                - name: test-webhooks
                  host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
                  ingressClass: nginx
                  urlPathPrefix: /triggers
                  externalHosts:
                    - "https://webhooks.example.com"
                    - "https://backup.webhooks.example.com"
                  namespaceSelector:
                    matchNames:
                      - "*"

The Operator synchronises this spec into the trigger-wrapper-config ConfigMap inside Tekton system namespace (default: tekton-pipelines). Once updated, the automatic exposure controller refreshes its cache and reconciles resources according to the new rules.

INFO

Note: The ConfigMap name trigger-wrapper-config is an internal technical name. You manage the configuration through TektonConfig as shown above, and don't need to directly interact with the ConfigMap.

Field reference

Each entry in export-rules represents a publishing strategy. Important fields:

  • name – rule name, also used when generating Service/Ingress names.

  • ingressClass (optional) – the Ingress controller to use, e.g. nginx, traefik.

  • host (optional) – hostname matched by the Ingress. Leave empty to accept all hosts. Important: When configuring a domain name, ensure DNS resolution is properly configured (or add to /etc/hosts for local testing). The domain must be resolvable from systems that will send webhooks.

  • externalHosts (optional) – What it does: Defines the webhook URLs that will be displayed to users in the UI and populated in EventListener's status.addresses field.

    Think of it as: The "public address" that external systems (like GitHub, GitLab) will use to send webhooks to your EventListener.

    How it works:

    • The controller combines each externalHosts URL with the generated path: ${externalHost}/<urlPathPrefix>/<eventlistener-namespace>/<eventlistener-name>
    • For example: externalHosts: ["https://webhooks.example.com"] + urlPathPrefix: /triggers → Final URL: https://webhooks.example.com/triggers/my-namespace/my-listener
    • These URLs appear in EventListener's status.addresses field, making it easy to copy and paste into GitHub/GitLab webhook configuration

    Common scenarios:

    ScenarioWhat to fill in externalHostsExample
    Standard domain with HTTPSYour public domain with https://"https://webhooks.example.com"
    LoadBalancer with custom portDomain/IP with port number"https://webhooks.example.com:8443"
    Multiple access pointsArray of backup URLs["https://primary.com", "https://backup.com"]
    IP-based access (no domain)HTTP with IP address"http://192.168.1.100" (but leave host field empty)
    ACP platformPlatform URL + cluster path"https://192.168.1.100/clusters-rewrite/test"

    What happens if you fill it wrong?

    • ✅ Ingress will still work correctly (this field doesn't affect Ingress creation)
    • ❌ Users will see incorrect webhook URLs in the UI
    • ❌ Copying the URL from status.addresses to GitHub/GitLab will fail
    • 🔧 Fix: Update the externalHosts value in TektonConfig and the controller will refresh EventListener status

    Key differences from host field:

    FieldPurposeUsed byExample
    hostIngress routing rule (HTTP Host header matching)Kubernetes Ingresswebhooks.example.com (domain only, no protocol)
    externalHostsUser-facing webhook URL for UI displayEventListener status.addresseshttps://webhooks.example.com (full URL with protocol)

    Rule of thumb:

    • If you can access your webhook at https://webhooks.example.com:8443/triggers/ns/el, then fill: externalHosts: ["https://webhooks.example.com:8443"]
    • The controller will append the path automatically

    Important notes:

    • Always include the protocol (http:// or https://)
    • Include custom ports if your LoadBalancer uses non-standard ports
    • Don't include the path in urlPathPrefix (like /triggers) - the controller appends it to the end of the externalHosts automatically
    • If unsure, leave it empty and check the actual accessible URL first, then update the configuration
  • urlPathPrefix (optional) – path prefix; defaults to /triggers. The final path rendered in the Ingress is ${urlPathPrefix}/${eventlistener-namespace}/${eventlistener-name}. Always start with / and avoid trailing slashes.

  • namespaceSelector.matchNames (optional) – namespaces allowed by this rule. Use "*" to target all namespaces.

  • labelSelector (optional) – Kubernetes LabelSelector used to filter EventListeners.

  • tls (optional) – TLS configuration for Ingress. Each entry specifies hosts (list of hostnames) and secretName (name of the TLS Secret containing the certificate).

  • annotations (optional) – additional annotations for Ingress. Useful for cert-manager, nginx-specific settings, etc. Controller-managed annotations (like nginx.ingress.kubernetes.io/rewrite-target) will be merged with user-provided annotations.

Namespace matching currently supports matchNames only. If you need label-based namespace selection, enumerate the namespaces explicitly.

Field Mapping to Ingress Resources

The following table shows how export rule fields map to the generated Ingress resource:

Export Rule FieldIngress Resource FieldDescription
namemetadata.nameThe Ingress resource name is set to the rule name
ingressClassspec.ingressClassNameSpecifies which Ingress controller should handle this Ingress
hostspec.rules[].hostHostname for the Ingress rule. If empty or "*", matches all hosts. Note: IP addresses are not supported as host values. If using an IP, leave host empty or configure a domain name that resolves to the IP.
urlPathPrefixspec.rules[].http.paths[].pathCombined with namespace and EventListener name to form the path: ${urlPathPrefix}/${namespace}/${eventlistener-name}
tlsspec.tlsTLS configuration for HTTPS. Each entry maps to spec.tls[].hosts and spec.tls[].secretName
annotationsmetadata.annotationsUser-provided annotations are merged with controller-managed annotations
namespaceSelectorN/AUsed to filter EventListeners, not directly mapped to Ingress
labelSelectorN/AUsed to filter EventListeners, not directly mapped to Ingress
externalHostsN/AUsed to populate EventListener status.addresses, not directly mapped to Ingress

Example Mapping:

Given this export rule:

export-rules:
  - name: test-webhooks
    host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    ingressClass: nginx
    urlPathPrefix: /triggers
    tls:
      - hosts:
          - webhooks.example.com
        secretName: webhooks-tls-secret
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt-prod"
WARNING

DNS Configuration: When using a domain name in the host field, ensure DNS records are configured to resolve the domain to your Ingress controller's IP. For local testing, you can add entries to /etc/hosts (Linux/Mac) or C:\Windows\System32\drivers\etc\hosts (Windows).

The generated Ingress will have:

  • metadata.name: test-webhooks
  • spec.ingressClassName: nginx
  • spec.rules[0].host: webhooks.example.com
  • spec.rules[0].http.paths[].path: /triggers/${namespace}/${eventlistener-name} (for each matching EventListener)
  • spec.tls[0].hosts: ["webhooks.example.com"]
  • spec.tls[0].secretName: webhooks-tls-secret
  • metadata.annotations: Includes cert-manager.io/cluster-issuer and controller-managed annotations

Request flow diagram

externalHosts tells external clients which URL to call. The Ingress still matches requests by host and ${urlPathPrefix}/${namespace}/${eventlistener}, and the backend Service receives exactly that path.

Configuration examples

Example 1: wildcard host with custom prefix

export-rules:
  - name: wildcard-host
    urlPathPrefix: /hooks/default
    ingressClass: nginx
    namespaceSelector:
      matchNames:
        - cicd

Result: the Ingress exposes /hooks/default/${namespace}/${eventlistener}. Because host is empty, any hostname will be accepted—ideal when an external gateway assigns the public domain.

Example 2: shared hostname and prefix

export-rules:
  - name: all-listeners
    host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /triggers
    ingressClass: nginx
    namespaceSelector:
      matchNames:
        - "*"

Result: every EventListener appears at https://webhooks.example.com/triggers/${namespace}/${eventlistener}; the backend sees the same path.

Example 3: environment-specific rules

export-rules:
  - name: staging-gitlab
    host: gitlab-staging.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /staging/gitlab
    namespaceSelector:
      matchNames:
        - staging-tools
    labelSelector:
      matchLabels:
        webhook-type: gitlab

  - name: prod-github
    host: github-prod.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /prod/github
    ingressClass: traefik
    namespaceSelector:
      matchNames:
        - prod-tools
    labelSelector:
      matchLabels:
        webhook-type: github

Result:

  • GitLab webhooks: https://gitlab-staging.example.com/staging/gitlab/${namespace}/${eventlistener}
  • GitHub webhooks: https://github-prod.example.com/prod/github/${namespace}/${eventlistener}

Example 4: team-scoped publishing with default prefix

export-rules:
  - name: team-a
    urlPathPrefix: /triggers
    namespaceSelector:
      matchNames:
        - team-a

Result: only EventListeners in team-a are exposed, at /triggers/team-a/${eventlistener}.

Example 5: multiple external endpoints and adjusted prefix

export-rules:
  - name: multi-endpoints
    host: webhook.internal.local  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /internal/hooks
    externalHosts:
      - https://webhooks.example.com/hooks/
      - https://backup.example.com/api/hooks
    namespaceSelector:
      matchNames:
        - ci-tools

Result:

  • Ingress serves webhook.internal.local/internal/hooks/${namespace}/${eventlistener} internally.
  • Externally you can publish https://webhooks.example.com/hooks/internal/hooks/${namespace}/${eventlistener} and https://backup.example.com/api/hooks/internal/hooks/${namespace}/${eventlistener}.
  • The backend Service always receives /internal/hooks/${namespace}/${eventlistener}.

Example 6: Configuring TLS/HTTPS

Option A: Manual TLS with pre-created Secret

  1. Create a TLS Secret containing your certificate:

    kubectl create secret tls webhooks-tls-secret \
      --cert=path/to/cert.pem \
      --key=path/to/key.pem \
      -n tekton-pipelines
  2. Configure your export rule with TLS:

    export-rules:
      - name: secure-webhooks
        host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
        urlPathPrefix: /triggers
        ingressClass: nginx
        tls:
          - hosts:
              - webhooks.example.com
            secretName: webhooks-tls-secret
        namespaceSelector:
          matchNames:
            - "*"

The controller will automatically configure the Ingress with TLS using the specified Secret.

Option B: Using cert-manager for automatic certificate management

Configure your export rule with cert-manager annotations:

export-rules:
  - name: cert-manager-webhooks
    host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /triggers
    ingressClass: nginx
    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt-prod"
      # Optional: additional nginx SSL settings
      nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
    namespaceSelector:
      matchNames:
        - "*"

cert-manager will automatically:

  • Create a Certificate resource
  • Obtain a certificate from Let's Encrypt (or your configured issuer)
  • Create a TLS Secret
  • Update the Ingress with TLS configuration

Option C: Combined TLS and annotations

You can combine manual TLS configuration with additional annotations:

export-rules:
  - name: custom-tls-with-annotations
    host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
    urlPathPrefix: /triggers
    ingressClass: nginx
    tls:
      - hosts:
          - webhooks.example.com
        secretName: custom-tls-secret
    annotations:
      nginx.ingress.kubernetes.io/ssl-protocols: "TLSv1.2 TLSv1.3"
      nginx.ingress.kubernetes.io/ssl-ciphers: "HIGH:!aNULL:!MD5"
    namespaceSelector:
      matchNames:
        - "*"

Note: When both tls and cert-manager annotations are configured, the tls configuration takes precedence. For automatic certificate management, use cert-manager annotations without tls configuration.

Configuration workflow

  1. Edit the TektonConfig resource (see Configuration entry point).
  2. Apply the changes: kubectl apply -f tektonconfig.yaml.
  3. Wait for the Operator to propagate the ConfigMap; the automatic exposure controller will then reconcile new resources automatically.

Verification and troubleshooting

Verify ConfigMap content

Check that the ConfigMap contains the expected configuration:

kubectl get configmap trigger-wrapper-config -n tekton-pipelines -o yaml

Expected output (normal):

apiVersion: v1
kind: ConfigMap
metadata:
  name: trigger-wrapper-config
  namespace: tekton-pipelines
data:
  config: |
    export-rules:
      - name: test-webhooks
        host: webhooks.example.com  # Ensure DNS resolution is configured (or add to /etc/hosts for testing)
        ingressClass: nginx
        urlPathPrefix: /triggers
        externalHosts:
          - "https://webhooks.example.com"
          - "https://backup.webhooks.example.com"
        namespaceSelector:
          matchNames:
            - "*"

What to check:

  • The ConfigMap exists and contains the config key
  • The export-rules array matches your TektonConfig specification
  • YAML syntax is valid (no parsing errors)

Verify Ingress objects

Check that Ingress resources are created for matching EventListeners:

# Replace <namespace> with the namespace where your EventListener is deployed
# For example, if your EventListener is in the 'tekton-triggers-demo' namespace:
kubectl get ingress -n tekton-triggers-demo

Important: The <namespace> in the command should be the namespace where your EventListener is deployed, not the system namespace (tekton-pipelines). The automatic exposure feature creates Ingress resources in the same namespace as the EventListener.

Expected output (normal):

NAME                              CLASS   HOSTS                    ADDRESS   PORTS   AGE
el-<eventlistener-name>           nginx   webhooks.example.com     ...        80      5m

What to check:

  • Ingress objects exist for EventListeners that match export rules
  • The HOSTS field matches the host specified in the export rule
  • The Ingress has an ADDRESS assigned (may take a few minutes)
  • If no Ingress appears, verify the namespace matches matchNames and EventListener labels match labelSelector

Check EventListener addresses

Verify that EventListener status contains the generated webhook addresses:

# Replace <el-name> with your EventListener name and <namespace> with the namespace where it's deployed
# For example, if your EventListener is named 'hello-listener' in the 'tekton-triggers-demo' namespace:
kubectl get eventlistener hello-listener -n tekton-triggers-demo \
  -o jsonpath='{.status.addresses}' | jq

Expected output (normal):

[
  {
    "url": "https://webhooks.example.com/triggers/<namespace>/<el-name>"
  },
  {
    "url": "https://backup.webhooks.example.com/triggers/<namespace>/<el-name>"
  }
]

What to check:

  • The addresses array contains URLs matching your externalHosts configuration
  • URLs follow the pattern: <externalHost>/<urlPathPrefix>/<eventlistener-namespace>/<eventlistener-name>
  • If addresses is empty or missing, the EventListener may not match any export rule

Inspect Trigger annotations

Check the export metadata stored in Trigger annotations:

# Replace <trigger-name> with your Trigger name and <namespace> with the namespace where it's deployed
# For example, if your Trigger is named 'my-trigger' in the 'tekton-triggers-demo' namespace:
kubectl get trigger my-trigger -n tekton-triggers-demo \
  -o jsonpath='{.metadata.annotations.triggers\.tekton\.dev/eventlistener-info}' | jq

Expected output (normal):

[
  {
    "name": "my-eventlistener",
    "namespace": "my-namespace",
    "endpoints": [
      "https://webhooks.example.com/triggers/my-namespace/my-eventlistener",
      "https://backup.webhooks.example.com/triggers/my-namespace/my-eventlistener"
    ],
    "relevance": {
      "score": 1000,
      "namespaceScore": 1000,
      "labelScore": 1000,
      "namespaceSelector": {
        "matchNames": ["my-namespace"]
      },
      "matchType": "direct"
    }
  }
]

What to check:

  • The annotation contains an array of EventListener information
  • Each entry includes name, namespace, endpoints, and relevance fields
  • The endpoints array matches the EventListener's status.addresses
  • The relevance.score indicates how well the EventListener matches the Trigger (higher is better)
  • If the annotation is missing, the Trigger may not have found any matching EventListeners

Troubleshooting tips

  • If a rule does not apply:

    • Verify that the namespace is listed in matchNames (or use "*" for all namespaces)
    • Check that EventListener labels satisfy the labelSelector requirements
    • Ensure the EventListener is in Ready state
  • Misconfigured label selectors:

    • Appear in controller logs as parsing errors
    • Check controller logs: kubectl logs -n tekton-pipelines -l app=tektoncd-enhancement-controller
  • Removing a rule:

    • Cascades deletion of the generated resources (Ingress, Service, etc.)
    • Setting export-rules to an empty array disables all external exposure
    • EventListener status.addresses will be cleared when no rules match
  • Using IP addresses instead of domain names:

    • Problem: Kubernetes Ingress resources do not support IP addresses as host values. If you configure host with an IP address (e.g., host: 192.168.1.100), the Ingress will fail to be created or will not work correctly.

    • Solution 1: Leave host empty or set it to "*" to accept all hosts. The Ingress will match requests regardless of the host header:

      export-rules:
        - name: ip-based-webhooks
          host: ""  # or omit the host field entirely
          urlPathPrefix: /triggers
          externalHosts:
            - "http://192.168.1.100"  # Use IP in externalHosts for client reference
          namespaceSelector:
            matchNames:
              - "*"
    • Solution 2: Configure a domain name that resolves to your IP address, then use that domain in the host field:

      1. Set up DNS resolution: Add an A record pointing your domain to the IP address (e.g., webhooks.example.com192.168.1.100)

      2. Configure the export rule with the domain name:

        export-rules:
          - name: domain-webhooks
            host: webhooks.example.com  # Domain that resolves to your IP. Ensure DNS resolution is configured (or add to /etc/hosts for testing)
            urlPathPrefix: /triggers
            externalHosts:
              - "http://192.168.1.100"  # Or use the domain: "http://webhooks.example.com"
            namespaceSelector:
              matchNames:
                - "*"
    • Note: externalHosts can contain IP addresses or URLs, as it's only used to populate EventListener status.addresses and doesn't affect Ingress creation. However, the Ingress itself must use a valid hostname (or be empty) in the host field.

By maintaining the ConfigMap through TektonConfig, you can flexibly control how Tekton EventListeners are exposed to external systems. Keep an eye on controller logs during updates to confirm that reconciliations complete successfully.