Repository: traefik/traefik (language: go)

Issue:
Traefik v3 breaks Kubernetes Ingress Prefix Path Regex
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

I upgraded from Traefik v2 to v3. Then I realized that all Kubernetes Ingress configurations which use a regex within as part of the prefix path (`pathType=Prefix`) break.


# Workaround

Add the following lines to the static config:

```yaml
core:
  defaultRuleSyntax: v2
```

# Suggestions

1. Revert to previous behavior for `pathType=Prefix`
2. Have an option for the Kubernetes Ingress provider to default to v2 behavior, by default
3. Have the Kubernetes Ingress provider to use `PathRegexp('^...')` insternally, instead
4. Add new `PathRegexp` value to `traefik.ingress.kubernetes.io/router.pathmatcher` annotation


### What did you see instead?

Kubernetes Ingress configurations which use a regex within as part of the Prefix path no longer work.

### What version of Traefik are you using?

v3.0.0

### What is your environment & configuration?

```yaml
providers:
  kubernetesIngress:
    ingressClass: traefik
    allowEmptyServices: true
    allowExternalNameServices: true
    throttleDuration: "3s"
```

Kubernetes Ingress Example:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: catalogservice-ingress
  namespace: uplift
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: https
    traefik.ingress.kubernetes.io/router.tls: "true"
spec:
  ingressClassName: traefik
  rules:
    - host: 'api.xxx'
      http:
        paths:
          - path: /xxx/v1/yyyy/{customer:[a-z0-9]+}/push/
            pathType: Prefix
            backend:
              service:
                name: catalogservice-service
                port:
                  name: http
```


### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:
Hey @fkollmann,
As it's explained in the [migration guide](https://doc.traefik.io/traefik/migration/v2-to-v3/#router-rule-matchers), `defaultRuleSyntax ` needs to be set to `v2` if you want to keep the v2 format by default.
This should be used temporarily, while migrating to the default v3 syntax.
If you set `defaultRuleSyntax` to be `v2` will Ingresses using `v3` syntax (whatever that may be, I haven't found how it differs yet) still work?
I'm trying to work out how you'd migrate without an outage.
I feel that this bug shouldn't have been closed?
I cannot get regex's working for an `Ingress` using Traefik v3
Using the example given for this issue:
```
          - path: /xxx/v1/yyyy/{customer:[a-z0-9]+}/push/
```
I've tried writing it two other ways and the v3 parser does not treat either as a regex:
```
          - path: /xxx/v1/yyyy/{[a-z0-9]+}/push/
```
```
          - path: /xxx/v1/yyyy/[a-z0-9]+/push/
```
Hello @jim-barber-he,

Could you confirm that the bug you described may be fixed by this [PR](https://github.com/traefik/traefik/pull/10689)?
Hi @nmengin,

Author of the mentioned PR here. I don't think my PR fixes this issue. Further more it might even make the workaround (`defaultRuleSyntax: v2`) impossible. Therefore I would remove my changes regarding ingress configuration from my PR.

Digging through the code it seems like that with the current implementation it is not possible to configure ingress paths with regex matching. [Only `Path(...)` and `PathPrefix(...)` are used internally](https://github.com/traefik/traefik/blob/8d2a2ff08f9ceff99fb743bfbf50816feed68520/pkg/provider/kubernetes/ingress/kubernetes.go#L696).

*Maybe* using path type `ImplementationSpecific` in combination with the annotation `traefik.ingress.kubernetes.io/router.pathmatcher` could be another workaround. But this feels kinda hacky and the documentation says only `Path` and `PathPrefix` should be used as path matchers.

To reiterate, it is not mentioned in the migration guide that it is no longer possible to use regex for routing in this use case. So I think this is a real bug and should be reopened. 

Disclaimer: Unfortunately I have no experience with Kubernetes ingress configuration. I purely went with what's in the code and might have missed something. So take this with a grain of salt.
Hello @HalloTschuess,

Thank you for your feedback.
I re-opened the issue to discuss it during the next triage with the other maintainers.

> So I think this is a real bug and should be reopened.

From what I understand in this issue, such behavior is not expected indeed.
I do not believe #10689 resolved the issue.  I am unable to have ingress configurations with regex paths without setting `defaultRuleSyntax: v2` using traefik 3.0.2.
Hi @fkollmann @jim-barber-he @HalloTschuess @corybolar,

We have discussed this issue among the maintainers.
This issue is a consequence of a major change in v3 to make it easier to use the rule matcher syntax. To do this, we have introduced a mechanism to switch the interpreted version of the rule matchers to make migration easier.

Unfortunately, in the case of the Kubernetes Ingress provider, where Traefik reinterprets the Kubernetes resources according to its dynamic configuration, there is no way to mitigate the disruptive change to the rule syntax of the router with the `Prefix` path type by fixing the version that the rule should be interpreted.

We understand that this is a real concern when dealing with the migration from v2 to v3, and we suggest the following:
- ~To modify the default syntax version according to the static configuration (revert #10689)~
- To allow control of the rule syntax version per Ingress with a new annotation
- To improve the documentation to address this specific case (document `traefik.ingress.kubernetes.io/router.pathmatcher' annotation to be used to force the matcher to be `PathRegex`).

Hi @rtribotte @corybolar ,

I'm not quite sure why #10689 should be reverted. It addresses an entirely different issue (#10688) and was never intended to resolve this problem. As per my previous post, I even removed changes from that PR since it would have broken the "workaround" with `defaultRuleSyntax: v2`.

In the PR, only the `default-router` is explicitly set to v3, with the route hardcoded to `PathPrefix("/")`. If I didn't miss something these values are not relevant to other ingress routers. The ingress configuration version remains unchanged and can still be set to v2 using `defaultRuleSyntax: v2`, as @corybolar mentioned. Therefore I don't think there is a conflict with any new solution or config option.

But it is entirely possible that there is something I didn't take into account. In any case I still vote against reverting #10689 since it does not break anything that previously worked at least as far as I know.

The other two suggestions seem reasonable. In addition, it might also be helpful if the proposed new annotation will be mentioned in the syntax remediation section of the migration guide.

Feel free correct me if I'm wrong any statement made or if I misunderstood something.
Hello @HalloTschuess,

Thanks for your feedback, and sorry for the misleading wording.
By "revert #10689", I was implying removing the ingress part of the PR changes, so that the default syntax version (from the static configuration) could apply when building the router from an Ingress.
I hope it clarifies the plan.

@HalloTschuess my bad, I completely overlooked the Ingress changes done by #10689.
You are right, there is indeed nothing to revert.
I edited the plan in [my comment](https://github.com/traefik/traefik/issues/10672#issuecomment-2200246200) accordingly.

Fix patch:
diff --git a/docs/content/migration/v2-to-v3-details.md b/docs/content/migration/v2-to-v3-details.md
index e40ab39f32..abd8ae587e 100644
--- a/docs/content/migration/v2-to-v3-details.md
+++ b/docs/content/migration/v2-to-v3-details.md
@@ -541,6 +541,19 @@ it is now unsupported and would prevent Traefik to start.
 
 All Pilot related configuration should be removed from the static configuration.
 
+### Kubernetes Ingress Path Matching
+
+In v3, the Kubernetes Ingress default path matching does not support regexes anymore.
+
+#### Remediation
+
+Two levels of remediation are possible:
+
+- Interpret the default path matcher `PathPrefix` with v2 syntax.
+This can done globally for all routers with the [static configuration](#configure-the-default-syntax-in-static-configuration) or on a per-router basis by using the [traefik.ingress.kubernetes.io/router.rulesyntax](../routing/providers/kubernetes-ingress.md#annotations) annotation.
+
+- Adapt the path regex to be compatible with the Go regex syntax and change the default path matcher to use the `PathRegexp` matcher with the [`traefik.ingress.kubernetes.io/router.pathmatcher`](../routing/providers/kubernetes-ingress.md#annotations) annotation.
+
 ## Operations Changes
 
 ### Traefik RBAC Update
diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md
index 80bd7becbb..158543b464 100644
--- a/docs/content/routing/providers/kubernetes-ingress.md
+++ b/docs/content/routing/providers/kubernetes-ingress.md
@@ -229,10 +229,18 @@ which in turn will create the resulting routers, services, handlers, etc.
     traefik.ingress.kubernetes.io/router.priority: "42"
     ```
 
+??? info "`traefik.ingress.kubernetes.io/router.rulesyntax`"
+
+    See [rule syntax](../routers/index.md#rulesyntax) for more information.
+
+    ```yaml
+    traefik.ingress.kubernetes.io/router.rulesyntax: "v2"
+    ```
+
 ??? info "`traefik.ingress.kubernetes.io/router.pathmatcher`"
 
     Overrides the default router rule type used for a path.
-    Only path-related matcher name can be specified: `Path`, `PathPrefix`.
+    Only path-related matcher name should be specified: `Path`, `PathPrefix` or `PathRegexp`.
 
     Default `PathPrefix`
 
diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go
index 00d5d6649f..144dd2a460 100644
--- a/pkg/provider/kubernetes/ingress/annotations.go
+++ b/pkg/provider/kubernetes/ingress/annotations.go
@@ -26,6 +26,7 @@ type RouterIng struct {
 	EntryPoints []string                 `json:"entryPoints,omitempty"`
 	Middlewares []string                 `json:"middlewares,omitempty"`
 	Priority    int                      `json:"priority,omitempty"`
+	RuleSyntax  string                   `json:"ruleSyntax,omitempty"`
 	TLS         *dynamic.RouterTLSConfig `json:"tls,omitempty" label:"allowEmpty"`
 }
 
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml
index bf4679f305..3a57a63458 100644
--- a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml
+++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml
@@ -10,6 +10,7 @@ metadata:
     traefik.ingress.kubernetes.io/router.entrypoints: ep1,ep2
     traefik.ingress.kubernetes.io/router.middlewares: md1,md2
     traefik.ingress.kubernetes.io/router.priority: "42"
+    traefik.ingress.kubernetes.io/router.rulesyntax: "v2"
     traefik.ingress.kubernetes.io/router.tls: "true"
     traefik.ingress.kubernetes.io/router.tls.certresolver:   foobar
     traefik.ingress.kubernetes.io/router.tls.domains.0.main: domain.com
diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go
index 490271b8c9..982cc09e87 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes.go
@@ -760,6 +760,7 @@ func loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath, rtConfig *Rout
 	}
 
 	if rtConfig != nil && rtConfig.Router != nil {
+		rt.RuleSyntax = rtConfig.Router.RuleSyntax
 		rt.Priority = rtConfig.Router.Priority
 		rt.EntryPoints = rtConfig.Router.EntryPoints
 		rt.Middlewares = rtConfig.Router.Middlewares


Test patch:
diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go
index 6462618941..7746b2623a 100644
--- a/pkg/provider/kubernetes/ingress/annotations_test.go
+++ b/pkg/provider/kubernetes/ingress/annotations_test.go
@@ -24,6 +24,7 @@ func Test_parseRouterConfig(t *testing.T) {
 				"traefik.ingress.kubernetes.io/router.entrypoints":        "foobar,foobar",
 				"traefik.ingress.kubernetes.io/router.middlewares":        "foobar,foobar",
 				"traefik.ingress.kubernetes.io/router.priority":           "42",
+				"traefik.ingress.kubernetes.io/router.rulesyntax":         "foobar",
 				"traefik.ingress.kubernetes.io/router.tls":                "true",
 				"traefik.ingress.kubernetes.io/router.tls.certresolver":   "foobar",
 				"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
@@ -38,6 +39,7 @@ func Test_parseRouterConfig(t *testing.T) {
 					EntryPoints: []string{"foobar", "foobar"},
 					Middlewares: []string{"foobar", "foobar"},
 					Priority:    42,
+					RuleSyntax:  "foobar",
 					TLS: &dynamic.RouterTLSConfig{
 						CertResolver: "foobar",
 						Domains: []types.Domain{
@@ -180,6 +182,7 @@ func Test_convertAnnotations(t *testing.T) {
 				"traefik.ingress.kubernetes.io/router.entrypoints":        "foobar,foobar",
 				"traefik.ingress.kubernetes.io/router.middlewares":        "foobar,foobar",
 				"traefik.ingress.kubernetes.io/router.priority":           "42",
+				"traefik.ingress.kubernetes.io/router.rulesyntax":         "foobar",
 				"traefik.ingress.kubernetes.io/router.tls":                "true",
 				"traefik.ingress.kubernetes.io/router.tls.certresolver":   "foobar",
 				"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
@@ -194,6 +197,7 @@ func Test_convertAnnotations(t *testing.T) {
 				"traefik.router.entrypoints":         "foobar,foobar",
 				"traefik.router.middlewares":         "foobar,foobar",
 				"traefik.router.priority":            "42",
+				"traefik.router.rulesyntax":          "foobar",
 				"traefik.router.tls":                 "true",
 				"traefik.router.tls.certresolver":    "foobar",
 				"traefik.router.tls.domains[0].main": "foobar",
diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index 5a4e3dd2b7..9b736a454f 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go
@@ -96,6 +96,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
 							Service:     "testing-service1-80",
 							Middlewares: []string{"md1", "md2"},
 							Priority:    42,
+							RuleSyntax:  "v2",
 							TLS: &dynamic.RouterTLSConfig{
 								CertResolver: "foobar",
 								Domains: []types.Domain{