Writing rewriters¶
An ingress rewriter translates controller-specific Ingress annotations into ingress entry dicts, consumed by whichever IngressProvider is configured in the distribution. Each rewriter targets a specific ingress controller — dispatched by ingressClassName or annotation prefix.
The gatekeepers of old each bore a different sigil, yet all opened the same threshold. The pilgrim need not know the sigil — only declare which gate he approaches, and the keeper shall answer in kind.
— Cultes des Goules, On the Many Gates (on good authority)
The contract¶
A rewriter class must have:
name— a string identifying the rewriter (e.g."haproxy","nginx"). Used for override matching: an external rewriter with the samenameas a built-in one replaces it.match(manifest, ctx)— returnTrueif this rewriter handles this Ingress manifest. Typically checksingressClassName(resolved throughingress_typesconfig) or annotation prefixes.rewrite(manifest, ctx)— convert one Ingress manifest to a list of entry dicts (see entry format below).priority(optional) — integer, default1000. Lower = checked earlier. External rewriters are always checked before built-in ones, regardless of priority. Priority only orders rewriters within the same pool (external or built-in).
from dekube import IngressRewriter, get_ingress_class, resolve_backend
class NginxRewriter(IngressRewriter):
name = "nginx"
def match(self, manifest, ctx):
ingress_types = ctx.config.get("ingress_types") or {}
cls = get_ingress_class(manifest, ingress_types)
if cls == "nginx":
return True
annotations = manifest.get("metadata", {}).get("annotations", {})
return any(k.startswith("nginx.ingress.kubernetes.io/") for k in annotations)
def rewrite(self, manifest, ctx):
entries = []
for rule in (manifest.get("spec") or {}).get("rules") or []:
host = rule.get("host", "")
if not host:
continue
for path_entry in (rule.get("http") or {}).get("paths") or []:
backend = resolve_backend(path_entry, manifest, ctx)
entries.append({
"host": host,
"path": path_entry.get("path", "/"),
"upstream": backend["upstream"],
"scheme": "http",
})
return entries
Entry format¶
Each entry dict returned by rewrite() must have:
| Key | Type | Required | Description |
|---|---|---|---|
host |
str |
yes | The hostname (e.g. app.example.com) |
path |
str |
yes | The path (/ for catch-all, /api for specific) |
upstream |
str |
yes | The upstream address (host:port) |
scheme |
str |
yes | http or https |
server_ca_secret |
str |
no | Secret name containing CA cert for backend TLS |
server_sni |
str |
no | SNI server name for backend TLS |
strip_prefix |
str |
no | Path prefix to strip before proxying. On multi-path rules, scope it to the matching path — don't apply a global annotation blindly to every entry. |
extra_directives |
list[str] |
no | Provider-specific directive strings (see below) |
extra_directives¶
A list of provider-specific directive strings injected into the generated config. With the default CaddyProvider, these are raw Caddy directives inserted into the host block — after uri strip_prefix and before reverse_proxy, which places them in the right Caddy order for directives like header, rate_limit, basic_auth, forward_auth, request_body, etc. Other IngressProvider implementations may interpret these differently or ignore them.
entries.append({
"host": "app.example.com",
"path": "/",
"upstream": "app:8080",
"scheme": "http",
"extra_directives": [
"header X-Frame-Options DENY",
"rate_limit {remote.ip} 100r/m",
],
})
How dispatch works¶
When the IngressProvider processes Ingress manifests, each manifest is dispatched to the first matching rewriter:
- External rewriters are checked first (in priority order)
- Built-in rewriters are checked next
- If no rewriter matches, a warning is emitted and the manifest is skipped
The built-in HAProxyRewriter matches:
ingressClassName: haproxyor empty/absent class (acts as default fallback)- Any manifest with
haproxy.org/*annotations
Custom ingress class names (ingress_types)¶
When clusters use custom ingressClassName values (e.g. haproxy-controller-internal, nginx-external), add an ingress_types mapping in dekube.yaml to resolve them to canonical rewriter names:
ingress_types:
haproxy-controller-internal: haproxy
haproxy-controller-external: haproxy
nginx-internal: nginx
The mapping is applied before rewriter dispatch — rewriters see the canonical name. Without it, custom class names won't match any rewriter and the Ingress is skipped with a warning.
Inside your rewriter, use get_ingress_class(manifest, ctx.config.get("ingress_types") or {}) to get the resolved class name. Both get_ingress_class and resolve_backend are part of the public interface — import them from dekube.
For building a complete reverse proxy backend (not just an annotation translator), see Writing ingress providers.
Override mechanism¶
An external rewriter with the same name as a built-in one replaces it entirely. For example, a custom HAProxyRewriter with name = "haproxy" would replace the built-in HAProxy handling.
When an override occurs, dekube prints:
See Writing extensions for testing, repo structure, publishing, and available imports.