Skip to content

Commit 11cd38e

Browse files
SF97Gopher Bot
authored andcommitted
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 28d08ac commit 11cd38e

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
@@ -65,3 +65,5 @@ allowed:
6565
- frontent
6666
- pprof
6767
- preload
68+
- hostname
69+
- str

deploy/tests/tnr/routeacl/suite_test.go

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

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
@@ -105,7 +105,13 @@ func AddHostPathRoute(route Route, mapFiles maps.Maps) error {
105105
func AddCustomRoute(route Route, routeACLAnn string, api api.HAProxyClient) (err error) {
106106
var routeCond string
107107
if route.Host != "" {
108-
routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host)
108+
if route.Host[0] == '*' {
109+
// Wildcard host - use suffix matching
110+
routeCond = fmt.Sprintf("{ var(txn.host) -m end %s } ", route.Host[1:])
111+
} else {
112+
// Regular host - use string matching
113+
routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host)
114+
}
109115
}
110116
if route.Path.Path != "" {
111117
if route.Path.PathTypeMatch == store.PATH_TYPE_EXACT {

0 commit comments

Comments
 (0)