diff --git a/.aspell.yml b/.aspell.yml index fc6d9961..f9c41909 100644 --- a/.aspell.yml +++ b/.aspell.yml @@ -57,3 +57,5 @@ allowed: - frontent - pprof - preload + - hostname + - str diff --git a/deploy/tests/tnr/routeacl/suite_test.go b/deploy/tests/tnr/routeacl/suite_test.go index d955a749..0b8a65ef 100644 --- a/deploy/tests/tnr/routeacl/suite_test.go +++ b/deploy/tests/tnr/routeacl/suite_test.go @@ -208,3 +208,215 @@ func (suite *UseBackendSuite) UseBackendFixture() (eventChan chan k8ssync.SyncDa <-controllerHasWorked return eventChan } + +func (suite *UseBackendSuite) NonWildcardHostFixture() (eventChan chan k8ssync.SyncDataEvent) { + var osArgs utils.OSArgs + os.Args = []string{os.Args[0], "-e", "-t", "--config-dir=" + suite.test.TempDir} + parser := flags.NewParser(&osArgs, flags.IgnoreUnknown) + _, errParsing := parser.Parse() //nolint:ifshort + if errParsing != nil { + suite.T().Fatal(errParsing) + } + + s := store.NewK8sStore(osArgs) + + haproxyEnv := env.Env{ + CfgDir: suite.test.TempDir, + Proxies: env.Proxies{ + FrontHTTP: "http", + FrontHTTPS: "https", + FrontSSL: "ssl", + BackSSL: "ssl-backend", + }, + } + + eventChan = make(chan k8ssync.SyncDataEvent, watch.DefaultChanSize*6) + controller := c.NewBuilder(). + WithHaproxyCfgFile([]byte(haproxyConfig)). + WithEventChan(eventChan). + WithStore(s). + WithHaproxyEnv(haproxyEnv). + WithUpdateStatusManager(&updateStatusManager{}). + WithArgs(osArgs).Build() + + go controller.Start() + + // Now sending store events for test setup + ns := store.Namespace{Name: "ns", Status: store.ADDED} + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: ns.Name, Data: &ns} + + endpoints := &store.Endpoints{ + SliceName: "api-service", + Service: "api-service", + Namespace: ns.Name, + Ports: map[string]*store.PortEndpoints{ + "https": { + Port: int64(3001), + Addresses: map[string]struct{}{"10.244.0.11": {}}, + }, + }, + Status: store.ADDED, + } + + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.ENDPOINTS, Namespace: endpoints.Namespace, Data: endpoints} + + service := &store.Service{ + Name: "api-service", + Namespace: ns.Name, + Annotations: map[string]string{"route-acl": "path_reg path-in-bug-repro$"}, + Ports: []store.ServicePort{ + { + Name: "https", + Protocol: "TCP", + Port: 8443, + Status: store.ADDED, + }, + }, + Status: store.ADDED, + } + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.SERVICE, Namespace: service.Namespace, Data: service} + + ingressClass := &store.IngressClass{ + Name: "haproxy", + Controller: "haproxy.org/ingress-controller", + Status: store.ADDED, + } + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: ingressClass} + + prefixPathType := networkingv1.PathTypePrefix + ingress := &store.Ingress{ + IngressCore: store.IngressCore{ + APIVersion: store.NETWORKINGV1, + Name: "api-ingress", + Namespace: ns.Name, + Class: "haproxy", + Rules: map[string]*store.IngressRule{ + "api.example.local": { + Host: "api.example.local", // Explicitly set the Host field + Paths: map[string]*store.IngressPath{ + string(prefixPathType) + "-/": { + Path: "/", + PathTypeMatch: string(prefixPathType), + SvcNamespace: service.Namespace, + SvcPortString: "https", + SvcName: service.Name, + }, + }, + }, + }, + }, + Status: store.ADDED, + } + + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS, Namespace: ingress.Namespace, Data: ingress} + controllerHasWorked := make(chan struct{}) + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.COMMAND, EventProcessed: controllerHasWorked} + <-controllerHasWorked + return eventChan +} + +func (suite *UseBackendSuite) WildcardHostFixture() (eventChan chan k8ssync.SyncDataEvent) { + var osArgs utils.OSArgs + os.Args = []string{os.Args[0], "-e", "-t", "--config-dir=" + suite.test.TempDir} + parser := flags.NewParser(&osArgs, flags.IgnoreUnknown) + _, errParsing := parser.Parse() //nolint:ifshort + if errParsing != nil { + suite.T().Fatal(errParsing) + } + + s := store.NewK8sStore(osArgs) + + haproxyEnv := env.Env{ + CfgDir: suite.test.TempDir, + Proxies: env.Proxies{ + FrontHTTP: "http", + FrontHTTPS: "https", + FrontSSL: "ssl", + BackSSL: "ssl-backend", + }, + } + + eventChan = make(chan k8ssync.SyncDataEvent, watch.DefaultChanSize*6) + controller := c.NewBuilder(). + WithHaproxyCfgFile([]byte(haproxyConfig)). + WithEventChan(eventChan). + WithStore(s). + WithHaproxyEnv(haproxyEnv). + WithUpdateStatusManager(&updateStatusManager{}). + WithArgs(osArgs).Build() + + go controller.Start() + + // Now sending store events for test setup + ns := store.Namespace{Name: "ns", Status: store.ADDED} + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.NAMESPACE, Namespace: ns.Name, Data: &ns} + + endpoints := &store.Endpoints{ + SliceName: "wildcard-service", + Service: "wildcard-service", + Namespace: ns.Name, + Ports: map[string]*store.PortEndpoints{ + "https": { + Port: int64(3001), + Addresses: map[string]struct{}{"10.244.0.10": {}}, + }, + }, + Status: store.ADDED, + } + + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.ENDPOINTS, Namespace: endpoints.Namespace, Data: endpoints} + + service := &store.Service{ + Name: "wildcard-service", + Namespace: ns.Name, + Annotations: map[string]string{"route-acl": "path_reg path-in-bug-repro$"}, + Ports: []store.ServicePort{ + { + Name: "https", + Protocol: "TCP", + Port: 8443, + Status: store.ADDED, + }, + }, + Status: store.ADDED, + } + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.SERVICE, Namespace: service.Namespace, Data: service} + + ingressClass := &store.IngressClass{ + Name: "haproxy", + Controller: "haproxy.org/ingress-controller", + Status: store.ADDED, + } + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS_CLASS, Data: ingressClass} + + prefixPathType := networkingv1.PathTypePrefix + ingress := &store.Ingress{ + IngressCore: store.IngressCore{ + APIVersion: store.NETWORKINGV1, + Name: "wildcard-ingress", + Namespace: ns.Name, + Class: "haproxy", + Rules: map[string]*store.IngressRule{ + "*.example.local": { + Host: "*.example.local", // Explicitly set the Host field + Paths: map[string]*store.IngressPath{ + string(prefixPathType) + "-/": { + Path: "/", + PathTypeMatch: string(prefixPathType), + SvcNamespace: service.Namespace, + SvcPortString: "https", + SvcName: service.Name, + }, + }, + }, + }, + }, + Status: store.ADDED, + } + + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.INGRESS, Namespace: ingress.Namespace, Data: ingress} + controllerHasWorked := make(chan struct{}) + eventChan <- k8ssync.SyncDataEvent{SyncType: k8ssync.COMMAND, EventProcessed: controllerHasWorked} + <-controllerHasWorked + return eventChan +} diff --git a/deploy/tests/tnr/routeacl/usebackend_test.go b/deploy/tests/tnr/routeacl/usebackend_test.go index c41e60d4..d705dff1 100644 --- a/deploy/tests/tnr/routeacl/usebackend_test.go +++ b/deploy/tests/tnr/routeacl/usebackend_test.go @@ -32,3 +32,53 @@ func (suite *UseBackendSuite) TestUseBackend() { suite.Exactly(c, 2, "use_backend for route-acl is repeated %d times but expected 2", c) }) } + +func (suite *UseBackendSuite) TestNonWildcardHostWithRouteACL() { + // Test non-wildcard host first to ensure route-acl works + suite.NonWildcardHostFixture() + suite.Run("Non-wildcard host should use string matching (-m str) with route-acl", func() { + contents, err := os.ReadFile(filepath.Join(suite.test.TempDir, "haproxy.cfg")) + if err != nil { + suite.T().Error(err.Error()) + } + + // Check that -m str is used with non-wildcard hosts in route-acl + if !strings.Contains(string(contents), "var(txn.host) -m str api.example.local") { + suite.T().Error("Expected to find 'var(txn.host) -m str api.example.local' in HAProxy config") + } + + // Check that route-acl annotation is applied + if !strings.Contains(string(contents), "path_reg path-in-bug-repro$") { + suite.T().Error("Expected to find route-acl pattern 'path_reg path-in-bug-repro$' in HAProxy config") + } + }) +} + +func (suite *UseBackendSuite) TestWildcardHostWithRouteACL() { + // This test addresses https://github.com/haproxytech/kubernetes-ingress/issues/734 + suite.WildcardHostFixture() + suite.Run("Wildcard host should use suffix matching (-m end) with route-acl", func() { + contents, err := os.ReadFile(filepath.Join(suite.test.TempDir, "haproxy.cfg")) + if err != nil { + suite.T().Error(err.Error()) + } + + // Debug: Print the actual config to see what's generated + suite.T().Logf("Generated HAProxy config:\n%s", string(contents)) + + // Check that -m end is used with wildcard hosts in route-acl + if !strings.Contains(string(contents), "var(txn.host) -m end .example.local") { + suite.T().Error("Expected to find 'var(txn.host) -m end .example.local' in HAProxy config") + } + + // Check that the buggy -m str pattern is NOT used + if strings.Contains(string(contents), "var(txn.host) -m str *.example.local") { + suite.T().Error("Found buggy pattern 'var(txn.host) -m str *.example.local' in HAProxy config") + } + + // Check that route-acl annotation is applied + if !strings.Contains(string(contents), "path_reg path-in-bug-repro$") { + suite.T().Error("Expected to find route-acl pattern 'path_reg path-in-bug-repro$' in HAProxy config") + } + }) +} diff --git a/pkg/route/route.go b/pkg/route/route.go index 8c3f91d2..b3667851 100644 --- a/pkg/route/route.go +++ b/pkg/route/route.go @@ -104,7 +104,13 @@ func AddHostPathRoute(route Route, mapFiles maps.Maps) error { func AddCustomRoute(route Route, routeACLAnn string, api api.HAProxyClient) (err error) { var routeCond string if route.Host != "" { - routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host) + if route.Host[0] == '*' { + // Wildcard host - use suffix matching + routeCond = fmt.Sprintf("{ var(txn.host) -m end %s } ", route.Host[1:]) + } else { + // Regular host - use string matching + routeCond = fmt.Sprintf("{ var(txn.host) -m str %s } ", route.Host) + } } if route.Path.Path != "" { if route.Path.PathTypeMatch == store.PATH_TYPE_EXACT {