Skip to content

ingress-annotations-compat library

This library is a set of shared internal macros consumed by HAPTIC's native haptic-annotations library — its heaviest consumer, and the only annotation library on by default — and by the three vendor annotation libraries (haproxytech, haproxy-ingress, nginx-ingress) — it emits nothing on its own, and there is nothing in it to configure. Leave it enabled: the vendor libraries import its macros, so disabling it while any of them is enabled breaks their template compilation.

Overview

ingress-annotations-compat.yaml is a scaffold library at hierarchy level 2.5 that holds parameterized macros consumed by the three Ingress vendor annotation libraries. The scaffold either walks resources.ingresses.List() itself (for example, BuildAnnotationSSLPassthrough) or takes a typed *resources.ingresses.T parameter (for example, EmitAnnotationAccessControl) — that's correct, because all three vendor libraries process the same resource and differ only by annotation namespace.

The scaffold exists to concentrate behaviour that would otherwise be duplicated three times. Each vendor library still owns its annotation extraction (the keys differ per vendor: haproxy.org/*, haproxy-ingress.github.io/*, nginx.ingress.kubernetes.io/*); the shared output emission lives in this library.

Scope: Ingress only. A vendor library operating on a non-Ingress CRD (HTTPRoute, GRPCRoute, custom CRDs) doesn't use these macros — it writes its own equivalents.

Configuration

controller:
  templateLibraries:
    ingressAnnotationsCompat:
      enabled: true  # Default; required by haptic-annotations (on by default) and by haproxytech, haproxy-ingress, and nginx-ingress

Disabling the scaffold while haptic-annotations or any of the three vendor annotation libraries stays enabled produces an invalid configuration: their snippets import macros defined here, and the controller's template validation rejects the merged config when those imports don't resolve. Disable it only together with all three vendor libraries.

Hierarchy

Level 0:   base
Level 1:   ssl
Level 2:   ingress, gateway
Level 2.5: ingress-annotations-compat   <-- this library
Level 3:   haptic-annotations, haproxytech, haproxy-ingress, nginx-ingress

The level-3 libraries import macros from this scaffold; the scaffold knows about Ingress but not about any specific annotation vocabulary.

Available macros

BuildAnnotationSSLPassthrough

Loops over Ingress resources and registers SSL passthrough backends. Each Ingress with a truthy SSL-passthrough annotation contributes one backend per host rule.

Signature:

BuildAnnotationSSLPassthrough(
  sslData       map[string]any, // accumulator
  annotationKey string,         // e.g. "haproxy.org/ssl-passthrough"
  namePrefix    string,         // e.g. "ssl-passthrough-"
  firstSeenKey  string,         // e.g. "haproxytech_sslPassthrough_host"
)

Backend entry shape (don't change without updating ssl.yaml consumers):

{
  "name":        namePrefix + ns + "-" + name,
  "sni":         host,
  "namespace":   ns,
  "ingress":     name,
  "svcName":     <first path's service name>,
  "svcPort":     <first path's service port (int, resolved from port.number or port.name)>,
  "svcPortName": <first path's service port name ("" if the port is referenced by number)>,
}

The service fields (svcName, svcPort, svcPortName) are captured at scan time rather than re-fetched by the consumer's backends-*-ssl-passthrough snippet — a second GetSingle() lookup could disagree with the use_backend pass under concurrent Ingress deletion. The macro comment in the source has the full rationale.

Used by:

  • haproxytech/util-haproxytech-ssl-passthrough (annotation: haproxy.org/ssl-passthrough)
  • haproxy-ingress/util-haproxy-ingress-ssl-passthrough (annotation: haproxy-ingress.github.io/ssl-passthrough)
  • nginx-ingress/util-nginx-ingress-ssl-passthrough (annotation: nginx.ingress.kubernetes.io/ssl-passthrough)
  • haptic-annotations/util-haptic-ssl-passthrough (annotation: haproxy-haptic.org/ssl-passthrough)

The vendor library still owns the per-library ComputeIfAbsent cache key, so the data slots stay distinct.

EmitAnnotationAccessControl

Emits CIDR-based source-range access control: acl <name> src <cidrs> plus http-request deny if <host-match> [!]<acl>, scoped to the ingress's hosts.

Signature:

EmitAnnotationAccessControl(
  ingress         *resources.ingresses.T,
  allowAnnotation string,  // e.g. "nginx.ingress.kubernetes.io/whitelist-source-range"
  denyAnnotation  string,  // e.g. "nginx.ingress.kubernetes.io/denylist-source-range"
  aclPrefix       string,  // e.g. "ni" — produces ACL names like "ni_allowlist_<ns>_<name>"
  allowCommentFmt string,  // comment template, may contain {KEY} for "<ns>/<name>"
  denyCommentFmt  string,
)

Used by:

  • haproxytech/frontend-filters-200-haproxytech-access-control
  • haproxy-ingress/frontend-filters-610-haproxy-ingress-access-control
  • nginx-ingress/frontend-filters-700-nginx-ingress-access-control
  • haptic-annotations/frontend-filters-810-haptic-access-control (aclPrefix: "haptic")

Validation tests assert on the names of the generated ACLs (ni_allowlist_*, hi_allowlist_*, haproxytech_allowlist_*, haptic_allowlist_*), so each library passes its distinct aclPrefix.

WebhookRejectOrWarn

WebhookRejectOrWarn(resource, reason, message) (from the util-webhook-reject-or-warn snippet) is the shared way to reject a misconfigured watched resource. It branches on the renderMode global: under the admission webhook it fail()s (so the API server denies the proposed resource), and on a live reconcile or the daemon load gate it records a Warning Event against resource and returns, so one already-present bad resource can't abort the whole render. The vendor libraries and the native haptic-annotations library import it for per-resource routing/presentation validation.

Callers must skip the offending resource's output in the warn path ({% continue %} in the Ingress loop). Use it only where skipping the feature is safe to serve without — routing/presentation guards — and keep a plain fail() for security features (skipping those would be fail-open) and for guards where a clean skip isn't possible. The full decision rule lives in the chart development guide (charts/CLAUDE.md).

Other exported macros

The scaffold exports six more macros, all imported the same way:

Macro Signature What it does
RenderAnnotationSSLPassthroughBackends (cacheKey, firstSeenKey, commentLabel string) Emits the mode tcp backends for the entries a matching BuildAnnotationSSLPassthrough scan cached under cacheKey — the emission half of the passthrough pair
RegisterAnnotationHSTS (ingress *resources.ingresses.T, enabledAnn, maxAgeAnn, subdomainsAnn, preloadAnn, defaultMaxAge string) Registers per-host HSTS settings for the SSL library's global HSTS snippet to emit
EmitAnnotationCORS (ingress *resources.ingresses.T, prefix, enabledAnn, defaultMaxAge, commentLabel string) Emits the CORS response headers and preflight handling for one Ingress (the ingress-nginx model)
ValidateCidrList (rawList, annotation, key string) Rejects a malformed CIDR list before it reaches the config, naming the annotation and the resource
ValidateConfigValue (value, annotation, key string, allowSpaces bool) Rejects annotation values carrying characters that would break out of the rendered directive
WafGovernance (gov map[string]any) Applies the shared Web Application Firewall governance rules to an annotation-derived policy selection

Design rationale

Why the scaffold sits at level 2.5, which patterns were extracted, which were surveyed and deliberately left duplicated (cookie-based session affinity, backend timeouts, header manipulation), and the rename from annotation-compat to ingress-annotations-compat: see Architecture Decision Record (ADR) ADR-0003.

Adding a new macro

A new macro earns its keep when:

  1. Two or more vendor libraries already implement nearly the same emission logic.
  2. The differences can be expressed as a small fixed set of string parameters (annotation keys, naming prefixes, comment formats).
  3. Validation-test assertions can survive the move (or can be updated cheaply).

If any of those fail, leave the duplication in place. Forced abstractions over genuinely different behaviour are worse than direct duplication — the future reader has to follow the parameters back to figure out what each library actually does.

See also

Found a problem on this page? Report it or edit the page with the pencil icon above the title.