Skip to content

Commit 3589039

Browse files
committed
BUG/MINOR: controller: Fix wildcard host matching with route-acl
When an Ingress resource used a wildcard host (e.g., `*.example.com`) in combination with the `haproxy.org/route-acl` service annotation, the controller would generate an incorrect HAProxy ACL. It used an exact string match (`-m str`) on the literal value `*.example.com`, which would fail to match any intended subdomains. This patch modifies the route generation logic to inspect the hostname. If the host begins with an asterisk ('*'), it now correctly generates an ACL using a suffix match (`-m end`) and removes the leading asterisk from the hostname string. For non-wildcard hosts, the original behavior of using an exact string match (`-m str`) is preserved. Fixes: #734
1 parent bd3d957 commit 3589039

File tree

4 files changed

+271
-1
lines changed

4 files changed

+271
-1
lines changed

.aspell.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,5 @@ allowed:
5656
- svc
5757
- frontent
5858
- pprof
59+
- hostname
60+
- str

deploy/tests/tnr/routeacl/suite_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,215 @@ func (suite *UseBackendSuite) UseBackendFixture() (eventChan chan k8ssync.SyncDa
208208
<-controllerHasWorked
209209
return eventChan
210210
}
211+
212+
func (suite *UseBackendSuite) NonWildcardHostFixture() (eventChan chan k8ssync.SyncDataEvent) {
213+
var osArgs utils.OSArgs
214+
os.Args = []string{os.Args[0], "-e", "-t", "--config-dir=" + suite.test.TempDir}
215+
parser := flags.NewParser(&osArgs, flags.IgnoreUnknown)
216+
_, errParsing := parser.Parse() //nolint:ifshort
217+
if errParsing != nil {
218+
suite.T().Fatal(errParsing)
219+
}
220+
221+
s := store.NewK8sStore(osArgs)
222+
223+
haproxyEnv := env.Env{
224+
CfgDir: suite.test.TempDir,
225+
Proxies: env.Proxies{
226+
FrontHTTP: "http",
227+
FrontHTTPS: "https",
228+
FrontSSL: "ssl",
229+
BackSSL: "ssl-backend",
230+
},
231+
}
232+
233+
eventChan = make(chan k8ssync.SyncDataEvent, watch.DefaultChanSize*6)
234+
controller := c.NewBuilder().
235+
WithHaproxyCfgFile([]byte(haproxyConfig)).
236+
WithEventChan(eventChan).
237+
WithStore(s).
238+
WithHaproxyEnv(haproxyEnv).
239+
WithUpdateStatusManager(&updateStatusManager{}).
240+
WithArgs(osArgs).Build()
241+
242+
go controller.Start()
243+
244+
// Now sending store events for test setup
245+
ns := store.Namespace{Name: "ns", Status: store.ADDED}
246+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: ns.Name, Data: &ns}
247+
248+
endpoints := &store.Endpoints{
249+
SliceName: "api-service",
250+
Service: "api-service",
251+
Namespace: ns.Name,
252+
Ports: map[string]*store.PortEndpoints{
253+
"https": {
254+
Port: int64(3001),
255+
Addresses: map[string]struct{}{"10.244.0.11": {}},
256+
},
257+
},
258+
Status: store.ADDED,
259+
}
260+
261+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.ENDPOINTS, Namespace: endpoints.Namespace, Data: endpoints}
262+
263+
service := &store.Service{
264+
Name: "api-service",
265+
Namespace: ns.Name,
266+
Annotations: map[string]string{"route-acl": "path_reg path-in-bug-repro$"},
267+
Ports: []store.ServicePort{
268+
{
269+
Name: "https",
270+
Protocol: "TCP",
271+
Port: 8443,
272+
Status: store.ADDED,
273+
},
274+
},
275+
Status: store.ADDED,
276+
}
277+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.SERVICE, Namespace: service.Namespace, Data: service}
278+
279+
ingressClass := &store.IngressClass{
280+
Name: "haproxy",
281+
Controller: "haproxy.org/ingress-controller",
282+
Status: store.ADDED,
283+
}
284+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: ingressClass}
285+
286+
prefixPathType := networkingv1.PathTypePrefix
287+
ingress := &store.Ingress{
288+
IngressCore: store.IngressCore{
289+
APIVersion: store.NETWORKINGV1,
290+
Name: "api-ingress",
291+
Namespace: ns.Name,
292+
Class: "haproxy",
293+
Rules: map[string]*store.IngressRule{
294+
"api.example.local": {
295+
Host: "api.example.local", // Explicitly set the Host field
296+
Paths: map[string]*store.IngressPath{
297+
string(prefixPathType) + "-/": {
298+
Path: "/",
299+
PathTypeMatch: string(prefixPathType),
300+
SvcNamespace: service.Namespace,
301+
SvcPortString: "https",
302+
SvcName: service.Name,
303+
},
304+
},
305+
},
306+
},
307+
},
308+
Status: store.ADDED,
309+
}
310+
311+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS, Namespace: ingress.Namespace, Data: ingress}
312+
controllerHasWorked := make(chan struct{})
313+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.COMMAND, EventProcessed: controllerHasWorked}
314+
<-controllerHasWorked
315+
return eventChan
316+
}
317+
318+
func (suite *UseBackendSuite) WildcardHostFixture() (eventChan chan k8ssync.SyncDataEvent) {
319+
var osArgs utils.OSArgs
320+
os.Args = []string{os.Args[0], "-e", "-t", "--config-dir=" + suite.test.TempDir}
321+
parser := flags.NewParser(&osArgs, flags.IgnoreUnknown)
322+
_, errParsing := parser.Parse() //nolint:ifshort
323+
if errParsing != nil {
324+
suite.T().Fatal(errParsing)
325+
}
326+
327+
s := store.NewK8sStore(osArgs)
328+
329+
haproxyEnv := env.Env{
330+
CfgDir: suite.test.TempDir,
331+
Proxies: env.Proxies{
332+
FrontHTTP: "http",
333+
FrontHTTPS: "https",
334+
FrontSSL: "ssl",
335+
BackSSL: "ssl-backend",
336+
},
337+
}
338+
339+
eventChan = make(chan k8ssync.SyncDataEvent, watch.DefaultChanSize*6)
340+
controller := c.NewBuilder().
341+
WithHaproxyCfgFile([]byte(haproxyConfig)).
342+
WithEventChan(eventChan).
343+
WithStore(s).
344+
WithHaproxyEnv(haproxyEnv).
345+
WithUpdateStatusManager(&updateStatusManager{}).
346+
WithArgs(osArgs).Build()
347+
348+
go controller.Start()
349+
350+
// Now sending store events for test setup
351+
ns := store.Namespace{Name: "ns", Status: store.ADDED}
352+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: ns.Name, Data: &ns}
353+
354+
endpoints := &store.Endpoints{
355+
SliceName: "wildcard-service",
356+
Service: "wildcard-service",
357+
Namespace: ns.Name,
358+
Ports: map[string]*store.PortEndpoints{
359+
"https": {
360+
Port: int64(3001),
361+
Addresses: map[string]struct{}{"10.244.0.10": {}},
362+
},
363+
},
364+
Status: store.ADDED,
365+
}
366+
367+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.ENDPOINTS, Namespace: endpoints.Namespace, Data: endpoints}
368+
369+
service := &store.Service{
370+
Name: "wildcard-service",
371+
Namespace: ns.Name,
372+
Annotations: map[string]string{"route-acl": "path_reg path-in-bug-repro$"},
373+
Ports: []store.ServicePort{
374+
{
375+
Name: "https",
376+
Protocol: "TCP",
377+
Port: 8443,
378+
Status: store.ADDED,
379+
},
380+
},
381+
Status: store.ADDED,
382+
}
383+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.SERVICE, Namespace: service.Namespace, Data: service}
384+
385+
ingressClass := &store.IngressClass{
386+
Name: "haproxy",
387+
Controller: "haproxy.org/ingress-controller",
388+
Status: store.ADDED,
389+
}
390+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: ingressClass}
391+
392+
prefixPathType := networkingv1.PathTypePrefix
393+
ingress := &store.Ingress{
394+
IngressCore: store.IngressCore{
395+
APIVersion: store.NETWORKINGV1,
396+
Name: "wildcard-ingress",
397+
Namespace: ns.Name,
398+
Class: "haproxy",
399+
Rules: map[string]*store.IngressRule{
400+
"*.example.local": {
401+
Host: "*.example.local", // Explicitly set the Host field
402+
Paths: map[string]*store.IngressPath{
403+
string(prefixPathType) + "-/": {
404+
Path: "/",
405+
PathTypeMatch: string(prefixPathType),
406+
SvcNamespace: service.Namespace,
407+
SvcPortString: "https",
408+
SvcName: service.Name,
409+
},
410+
},
411+
},
412+
},
413+
},
414+
Status: store.ADDED,
415+
}
416+
417+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS, Namespace: ingress.Namespace, Data: ingress}
418+
controllerHasWorked := make(chan struct{})
419+
eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.COMMAND, EventProcessed: controllerHasWorked}
420+
<-controllerHasWorked
421+
return eventChan
422+
}

deploy/tests/tnr/routeacl/usebackend_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,53 @@ func (suite *UseBackendSuite) TestUseBackend() {
3232
suite.Exactly(c, 2, "use_backend for route-acl is repeated %d times but expected 2", c)
3333
})
3434
}
35+
36+
func (suite *UseBackendSuite) TestNonWildcardHostWithRouteACL() {
37+
// Test non-wildcard host first to ensure route-acl works
38+
suite.NonWildcardHostFixture()
39+
suite.Run("Non-wildcard host should use string matching (-m str) with route-acl", func() {
40+
contents, err := os.ReadFile(filepath.Join(suite.test.TempDir, "haproxy.cfg"))
41+
if err != nil {
42+
suite.T().Error(err.Error())
43+
}
44+
45+
// Check that -m str is used with non-wildcard hosts in route-acl
46+
if !strings.Contains(string(contents), "var(txn.host) -m str api.example.local") {
47+
suite.T().Error("Expected to find 'var(txn.host) -m str api.example.local' in HAProxy config")
48+
}
49+
50+
// Check that route-acl annotation is applied
51+
if !strings.Contains(string(contents), "path_reg path-in-bug-repro$") {
52+
suite.T().Error("Expected to find route-acl pattern 'path_reg path-in-bug-repro$' in HAProxy config")
53+
}
54+
})
55+
}
56+
57+
func (suite *UseBackendSuite) TestWildcardHostWithRouteACL() {
58+
// This test addresses https://github.com/haproxytech/kubernetes-ingress/issues/734
59+
suite.WildcardHostFixture()
60+
suite.Run("Wildcard host should use suffix matching (-m end) with route-acl", func() {
61+
contents, err := os.ReadFile(filepath.Join(suite.test.TempDir, "haproxy.cfg"))
62+
if err != nil {
63+
suite.T().Error(err.Error())
64+
}
65+
66+
// Debug: Print the actual config to see what's generated
67+
suite.T().Logf("Generated HAProxy config:\n%s", string(contents))
68+
69+
// Check that -m end is used with wildcard hosts in route-acl
70+
if !strings.Contains(string(contents), "var(txn.host) -m end .example.local") {
71+
suite.T().Error("Expected to find 'var(txn.host) -m end .example.local' in HAProxy config")
72+
}
73+
74+
// Check that the buggy -m str pattern is NOT used
75+
if strings.Contains(string(contents), "var(txn.host) -m str *.example.local") {
76+
suite.T().Error("Found buggy pattern 'var(txn.host) -m str *.example.local' in HAProxy config")
77+
}
78+
79+
// Check that route-acl annotation is applied
80+
if !strings.Contains(string(contents), "path_reg path-in-bug-repro$") {
81+
suite.T().Error("Expected to find route-acl pattern 'path_reg path-in-bug-repro$' in HAProxy config")
82+
}
83+
})
84+
}

pkg/route/route.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ func AddHostPathRoute(route Route, mapFiles maps.Maps) error {
104104
func AddCustomRoute(route Route, routeACLAnn string, api api.HAProxyClient) (err error) {
105105
var routeCond string
106106
if route.Host != "" {
107-
routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host)
107+
if route.Host[0] == '*' {
108+
// Wildcard host - use suffix matching
109+
routeCond = fmt.Sprintf("{ var(txn.host) -m end %s } ", route.Host[1:])
110+
} else {
111+
// Regular host - use string matching
112+
routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host)
113+
}
108114
}
109115
if route.Path.Path != "" {
110116
if route.Path.PathTypeMatch == store.PATH_TYPE_EXACT {

0 commit comments

Comments
 (0)