Files
ingress/pkg/server/service/zap_backend.go
zooqueen 1d875dd71c rebrand: strip "traefik" branding from the ingress controller
Removes all "traefik" branding/paths/env/comments from the shipping
surface while keeping load-bearing config schema and external deps intact.

Paths / binary:
- cmd/traefik/ -> cmd/ingress/ (traefik.go->ingress.go, etc.); merged out
  the empty cmd/ingress/generate.go stub. Build refs updated in Dockerfile,
  Makefile, .goreleaser.yml.tmpl, script/crossbinary-default.sh, README.
- contrib/systemd/traefik.service -> ingress.service (Hanzo Ingress unit,
  /etc/ingress paths, hanzo-ingress binary).
- contrib/grafana/traefik*.json -> ingress*.json; metric queries
  traefik_* -> ingress_* (now match the engine's ingress_ prefix) + titles.
- script/gcg/traefik-*.toml -> ingress-*.toml; Owner/Repo -> hanzoai/ingress.

Code/config branding (comments + non-schema strings):
- doc.go, mount.go, client/router.go, zap_backend.go, forward_backend.go,
  register.go: "Traefik" -> "the ingress"/"Hanzo Ingress"; corrected the
  annotation example to ingress.kubernetes.io/.
- ingress.sample.{toml,yml}: header + log + entryPoint example.
- pkg/config/dynamic/fixtures/sample.toml comment.
- pkg/redactor/testdata example+expected: /etc/traefik/traefik.toml ->
  /etc/ingress/ingress.toml, TraefikLogsFile -> IngressLogsFile (both files
  kept identical so the redactor golden test stays green).
- .golangci.yml importas aliases traefik* -> ingress* and the stale
  crd/traefikio path -> crd/hanzoai; unused-var exclude ingressVersion.
- .dockerignore / .gitignore: drop stale /traefik artifacts.
- script/code-gen.sh: PROJECT_MODULE -> github.com/hanzoai/ingress,
  v1alpha1 path -> crd/hanzoai, CRD output -> hanzo.ai_*.yaml /
  01-ingress-crd.yml.
- script/deploy.sh: drop the upstream traefik-library-image push.
- Removed dead, disabled upstream docs-publish workflow
  (.github/workflows/documentation.yaml, pushed to traefik/doc).
- LLM.md: rewrote rebrand notes; documented load-bearing "traefik" that
  stays (external imports, hash-bound realm, upstream issue URL, license
  attribution) and vendored upstream docs/webui/changelog as out of scope.

Load-bearing "traefik" intentionally KEPT (not branding):
- external Go modules github.com/traefik/{oxy,yaegi,grpc-web};
- digest_auth_test.go htdigest realm "traefik" (invalidates the hashes);
- consul_test.go upstream issue URL #8092 (provenance citation);
- LICENSE.md + boilerplate.go.tmpl Containous/Traefik Labs copyright
  (MIT attribution; Hanzo AI appended);
- integration/ test fixtures (traefik/whoami images, rawdata-*.json
  golden entrypoint names, nomad traefik.* tags) — test-only, semantics-
  sensitive; out of scope for this pass.

Verify: `go build $(go list ./... | grep -v /webui)` passes; redactor /
config/dynamic / nomad / server/service tests pass. The webui go:embed
failure and the kubernetes/crd test (ingress.k8s.io group mismatch) are
pre-existing on main, not caused by this rebrand.
2026-06-21 15:55:45 -07:00

157 lines
5.2 KiB
Go

// Package service: zap_backend wires a ZAP-HTTP transport for selected
// upstream backends. When a backend's host:port matches the configured
// allowlist, requests are dialed via github.com/zap-proto/http instead of
// net/http. All other backends fall through to the wrapped RoundTripper
// unchanged. External (client-facing) TLS is unaffected.
//
// Configuration: env var INGRESS_ZAP_BACKENDS holds a comma-separated
// list of host:port targets, e.g.
//
// INGRESS_ZAP_BACKENDS=svc1.ns.svc:8080,svc2.ns.svc:8080
//
// IngressRoute manifests opt in by pointing the service at one of those
// host:port targets; no schema change is required. See
// docs/content/reference/dynamic-configuration/zap-backend.md for an
// example manifest.
package service
import (
"net/http"
"os"
"strings"
"sync"
zaphttp "github.com/zap-proto/http"
"github.com/rs/zerolog/log"
)
// zapBackendEnv is the env var that lists ZAP-HTTP backend host:port
// targets, comma-separated.
const zapBackendEnv = "INGRESS_ZAP_BACKENDS"
// zapBackendRoundTripper routes requests to a ZAP-HTTP transport when
// the request's destination host:port matches the allowlist; everything
// else is delegated to the wrapped RoundTripper.
type zapBackendRoundTripper struct {
next http.RoundTripper
allowlist map[string]struct{}
transports sync.Map // host:port -> *zaphttp.Transport
}
// newZapBackendRoundTripper wraps next. If the env var is empty, it
// returns next unchanged so the call site stays oblivious.
func newZapBackendRoundTripper(next http.RoundTripper) http.RoundTripper {
addrs := parseZapBackendList(os.Getenv(zapBackendEnv))
if len(addrs) == 0 {
return next
}
allow := make(map[string]struct{}, len(addrs))
for _, a := range addrs {
allow[a] = struct{}{}
}
return &zapBackendRoundTripper{next: next, allowlist: allow}
}
// parseZapBackendList splits a comma-separated list and trims whitespace.
// Empty entries are dropped.
func parseZapBackendList(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
out = append(out, p)
}
return out
}
// RoundTrip dispatches to ZAP-HTTP if the destination is in the
// allowlist, otherwise to the wrapped transport.
//
// Match strategy:
//
// 1. Exact host:port match against allowlist entries. Works when
// IngressRoute backends point at a stable in-cluster address that
// the ingress does NOT resolve before dialing.
// 2. Port-only fallback: if any allowlist entry's port matches the
// request's port, route through ZAP-HTTP regardless of host.
// The ingress resolves Service DNS to a Pod IP before invoking the
// transport (req.URL.Host becomes 10.x.x.x:port at this layer);
// pinning the dial to (resolved-IP, port) lets the ZAP transport
// reach the same pod the regular transport would have. Operators
// who want stricter routing should use a per-port port number that
// no non-ZAP backend uses (we already do — :9999 isn't shared).
func (z *zapBackendRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
addr := backendAddr(req)
if _, ok := z.allowlist[addr]; ok {
log.Info().Str("addr", addr).Str("path", req.URL.Path).
Msg("[ZAP-BACKEND] dialing via zap-http (exact match)")
t := z.transportFor(addr)
return t.RoundTrip(req)
}
if z.matchPort(addr) {
log.Info().Str("addr", addr).Str("path", req.URL.Path).
Msg("[ZAP-BACKEND] dialing via zap-http (port match)")
t := z.transportFor(addr)
return t.RoundTrip(req)
}
return z.next.RoundTrip(req)
}
// matchPort returns true when the request's port matches the port of
// any allowlist entry. This is the resolved-pod-IP fallback described
// in RoundTrip.
func (z *zapBackendRoundTripper) matchPort(addr string) bool {
_, port, ok := splitHostPort(addr)
if !ok {
return false
}
for entry := range z.allowlist {
_, p, ok := splitHostPort(entry)
if ok && p == port {
return true
}
}
return false
}
// splitHostPort is strings.LastIndex over ":" — host:port carriers
// here never have brackets or schemes, so a hand-rolled split is fine
// and avoids net.SplitHostPort's empty-port behaviour.
func splitHostPort(addr string) (host, port string, ok bool) {
i := strings.LastIndex(addr, ":")
if i < 0 {
return addr, "", false
}
return addr[:i], addr[i+1:], true
}
// transportFor returns a per-addr ZAP transport, lazily created.
// zaphttp.Transport pins to a single peer (it ignores req.URL.Host),
// so we keep one per backend address.
func (z *zapBackendRoundTripper) transportFor(addr string) *zaphttp.Transport {
if v, ok := z.transports.Load(addr); ok {
return v.(*zaphttp.Transport)
}
t := zaphttp.NewTransport(addr)
actual, _ := z.transports.LoadOrStore(addr, t)
return actual.(*zaphttp.Transport)
}
// backendAddr returns the upstream host:port for the request. The ingress
// rewrites req.URL to point at the chosen backend before invoking the
// transport, so URL.Host is the source of truth. Falls back to req.Host
// if URL.Host is empty (defensive; this is not expected in practice).
func backendAddr(req *http.Request) string {
if req.URL != nil && req.URL.Host != "" {
return req.URL.Host
}
return req.Host
}