Skip to content

Commit e4ec195

Browse files
committed
BUG/MINOR: remove unused domainWildcard bucket to fix dropped routes
The `domainWildcard` bucket in `desiredBackendsMaps` was never applied to any HAProxy map file. As a result, HTTPRoutes combining wildcard hosts with exact path matches were silently dropped into this dead bucket. This commit removes the unused `domainWildcard` bucket, since wildcard-host matching is already handled upstream by the listener/listener-route map pipeline. The `resolveEntry` function is updated to appropriately keep these entries in the `exact` bucket instead. Additionally, unit tests are introduced for `resolveEntry` to verify path match and host kind bucket resolution logic to prevent future regressions.
1 parent 870b4c5 commit e4ec195

2 files changed

Lines changed: 125 additions & 13 deletions

File tree

k8s/gate/haproxy/routes.go

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,21 +229,21 @@ func (b *RouteMgrImpl) onValidHTTPRouteUpserted(origin maps.ResourceOrigin, rout
229229
return nil
230230
}
231231

232-
// desiredBackendsMaps groups the four (hostname, path) → backends maps that
233-
// correspond to the four HAProxy map files (exact, prefix, regex, domain-wildcard).
232+
// desiredBackendsMaps groups the three (hostname, path) → backends maps that
233+
// correspond to the HAProxy path-match map files (exact, prefix, regex).
234+
// Wildcard-host matching is handled upstream by the listener/listener-route
235+
// map pipeline, not by a dedicated bucket here.
234236
type desiredBackendsMaps struct {
235-
exact map[maps.EntryKey]map[string]*maps.WeightedValue
236-
prefix map[maps.EntryKey]map[string]*maps.WeightedValue
237-
regex map[maps.EntryKey]map[string]*maps.WeightedValue
238-
domainWildcard map[maps.EntryKey]map[string]*maps.WeightedValue
237+
exact map[maps.EntryKey]map[string]*maps.WeightedValue
238+
prefix map[maps.EntryKey]map[string]*maps.WeightedValue
239+
regex map[maps.EntryKey]map[string]*maps.WeightedValue
239240
}
240241

241242
func newDesiredBackendsMaps() desiredBackendsMaps {
242243
return desiredBackendsMaps{
243-
exact: map[maps.EntryKey]map[string]*maps.WeightedValue{},
244-
prefix: map[maps.EntryKey]map[string]*maps.WeightedValue{},
245-
regex: map[maps.EntryKey]map[string]*maps.WeightedValue{},
246-
domainWildcard: map[maps.EntryKey]map[string]*maps.WeightedValue{},
244+
exact: map[maps.EntryKey]map[string]*maps.WeightedValue{},
245+
prefix: map[maps.EntryKey]map[string]*maps.WeightedValue{},
246+
regex: map[maps.EntryKey]map[string]*maps.WeightedValue{},
247247
}
248248
}
249249

@@ -265,9 +265,8 @@ func (d *desiredBackendsMaps) resolveEntry(hostname string, match gatewayv1.HTTP
265265
selected := d.exact
266266
switch pathType {
267267
case gatewayv1.PathMatchExact:
268-
if isDomainWildcard(originalHostname) {
269-
selected = d.domainWildcard
270-
}
268+
// Wildcard-host + exact-path is routed via the listener/listener-route
269+
// map pipeline upstream; the path bucket remains d.exact here.
271270
case gatewayv1.PathMatchPathPrefix:
272271
if isDomainWildcard(originalHostname) {
273272
selected = d.regex

k8s/gate/haproxy/routes_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package haproxy
22

33
import (
44
"testing"
5+
6+
"github.com/haproxytech/haproxy-unified-gateway/k8s/gate/haproxy/storage/maps"
7+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
58
)
69

710
func TestSanitizeRegexp(t *testing.T) {
@@ -61,3 +64,113 @@ func TestSanitizeRegexp(t *testing.T) {
6164
})
6265
}
6366
}
67+
68+
// TestResolveEntry covers the pathMatch × hostKind combinations and guards
69+
// against regressions where wildcard-host + exact-path entries were silently
70+
// dropped into a dead `domainWildcard` bucket that was never applied to any
71+
// map file.
72+
func TestResolveEntry(t *testing.T) {
73+
ptr := func(s gatewayv1.PathMatchType) *gatewayv1.PathMatchType { return &s }
74+
strPtr := func(s string) *string { return &s }
75+
76+
type bucket int
77+
const (
78+
bucketExact bucket = iota
79+
bucketPrefix
80+
bucketRegex
81+
)
82+
83+
tests := []struct {
84+
name string
85+
hostname string
86+
pathType gatewayv1.PathMatchType
87+
path string
88+
wantBucket bucket
89+
wantKey maps.EntryKey
90+
}{
91+
{
92+
name: "exact-path exact-host",
93+
hostname: "example.com",
94+
pathType: gatewayv1.PathMatchExact,
95+
path: "/foo",
96+
wantBucket: bucketExact,
97+
wantKey: maps.EntryKey{Hostname: "example.com", Path: "/foo"},
98+
},
99+
{
100+
name: "exact-path wildcard-host falls through to exact bucket",
101+
hostname: "*.example.com",
102+
pathType: gatewayv1.PathMatchExact,
103+
path: "/foo",
104+
wantBucket: bucketExact,
105+
wantKey: maps.EntryKey{Hostname: ".example.com", Path: "/foo"},
106+
},
107+
{
108+
name: "prefix-path exact-host",
109+
hostname: "example.com",
110+
pathType: gatewayv1.PathMatchPathPrefix,
111+
path: "/foo",
112+
wantBucket: bucketPrefix,
113+
wantKey: maps.EntryKey{Hostname: "example.com", Path: "/foo"},
114+
},
115+
{
116+
name: "prefix-path wildcard-host",
117+
hostname: "*.example.com",
118+
pathType: gatewayv1.PathMatchPathPrefix,
119+
path: "/foo",
120+
wantBucket: bucketRegex,
121+
wantKey: maps.EntryKey{Hostname: `\.example\.com`, Path: "/foo.*"},
122+
},
123+
{
124+
name: "regex-path exact-host",
125+
hostname: "example.com",
126+
pathType: gatewayv1.PathMatchRegularExpression,
127+
path: "/foo",
128+
wantBucket: bucketRegex,
129+
wantKey: maps.EntryKey{Hostname: `^example\.com`, Path: "/foo"},
130+
},
131+
{
132+
name: "regex-path wildcard-host",
133+
hostname: "*.example.com",
134+
pathType: gatewayv1.PathMatchRegularExpression,
135+
path: "/foo",
136+
wantBucket: bucketRegex,
137+
wantKey: maps.EntryKey{Hostname: `\.example\.com`, Path: "/foo"},
138+
},
139+
}
140+
141+
for _, tt := range tests {
142+
t.Run(tt.name, func(t *testing.T) {
143+
d := newDesiredBackendsMaps()
144+
match := gatewayv1.HTTPRouteMatch{
145+
Path: &gatewayv1.HTTPPathMatch{
146+
Type: ptr(tt.pathType),
147+
Value: strPtr(tt.path),
148+
},
149+
}
150+
innerMap, key := d.resolveEntry(tt.hostname, match)
151+
152+
if key != tt.wantKey {
153+
t.Errorf("resolveEntry key = %+v, want %+v", key, tt.wantKey)
154+
}
155+
if innerMap == nil {
156+
t.Fatal("resolveEntry returned nil inner map")
157+
}
158+
159+
buckets := map[bucket]map[maps.EntryKey]map[string]*maps.WeightedValue{
160+
bucketExact: d.exact,
161+
bucketPrefix: d.prefix,
162+
bucketRegex: d.regex,
163+
}
164+
for b, m := range buckets {
165+
_, present := m[tt.wantKey]
166+
if b == tt.wantBucket {
167+
if !present {
168+
t.Errorf("key %+v missing from expected bucket %d", tt.wantKey, b)
169+
}
170+
} else if present {
171+
t.Errorf("key %+v leaked into unexpected bucket %d", tt.wantKey, b)
172+
}
173+
}
174+
})
175+
}
176+
}

0 commit comments

Comments
 (0)