0

I'm trying to have a rule listening to a specific path containing a dollar sign like this:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: metadata-ingress
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/use-regex: "false"
spec:
  ingressClassName: public
  tls:
  - hosts:
    - mydomain.com
  rules:
  - host: mydomain.com
    http:
      paths:
      - path: /api/v2/$metadata
        pathType: Prefix
        backend:
          service:
            name: busybox
            port:
              number: 8280

I don't want any url rewrite or anything fancy, just want this specific path to be caught and forwarded to this service.

Without the "$" it works.

I thought disabling regex with use-regex: "false" would fix it, but no.

I also tried using the url encoded value for $ : %24metadata but it doesn't help either.

I also tried to use "exact" instead of "prefix" as the pathType but no.

Dunge
  • 532
  • 3
  • 19

1 Answers1

1

I can't reproduce your problem, but I thought I walk through my test setup and you can tell me if anything is different. For the purpose of testing different paths, I have two deployments using the traefik/whoami image (this just provides a useful endpoint that shows us -- among other things -- the hostname and path involved in the request).

That looks like:

apiVersion: v1
kind: Service
metadata:
  labels:
    app: example
    component: app1
  name: example-app1
spec:
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app: example
    component: app1

---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: example
    component: app2
  name: example-app2
spec:
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app: example
    component: app2

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: example
    component: app1
  name: example-app1
spec:
  selector:
    matchLabels:
      app: example
      component: app1
  template:
    metadata:
      labels:
        app: example
        component: app1
    spec:
      containers:
      - image: docker.io/traefik/whoami:latest
        name: whoami
        ports:
        - containerPort: 80
          name: http

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: example
    component: app2
  name: example-app2
spec:
  selector:
    matchLabels:
      app: example
      component: app2
  template:
    metadata:
      labels:
        app: example
        component: app2
    spec:
      containers:
      - image: docker.io/traefik/whoami:latest
        name: whoami
        ports:
        - containerPort: 80
          name: http

I've also deployed the following Ingress resource, which looks mostly like yours, except I've added a second paths config so that we can compare requests that match /api/v2/$metadata vs those that do not:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: house
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: example
spec:
  ingressClassName: nginx
  rules:
  - host: example.apps.infra.house
    http:
      paths:
      - backend:
          service:
            name: example-app1
            port:
              name: http
        path: /
        pathType: Prefix
      - backend:
          service:
            name: example-app2
            port:
              name: http
        path: /api/v2/$metadata
        pathType: Prefix
  tls:
  - hosts:
    - example.apps.infra.house
    secretName: example-cert

With these resources in place, a request to https://example.apps.infra.house/ goes to app1:

$ curl -s https://example.apps.infra.house/ | grep Hostname
Hostname: example-app1-596fcf48bd-dqhvc

Whereas a request to https://example.apps.infra.house/api/v2/$metadata goes to app2:

$ curl -s https://example.apps.infra.house/api/v2/\$metadata | grep Hostname
Hostname: example-app2-8675dc9b45-6hg7l

So that all seems to work.


We can, if we are so inclined, examine the nginx configuration that results from that Ingress. On my system, the nginx ingress controller runs in the nginx-ingress namespace:

$ kubectl -n nginx-ingress get deploy
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx-controller   1/1     1            1           8d

The configuration lives in /etc/nginx/nginx.conf in the container. We can cat the file to stdout and look for the relevant directives:

$ kubectl -n nginx-ingress exec deploy/ingress-nginx-controller cat /etc/nginx/nginx.conf
...
                location /api/v2/$metadata/ {
                ...
                }
...

Based on your comment, the following seems to work:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/rewrite-target: "/$2"
    cert-manager.io/cluster-issuer: house
spec:
  ingressClassName: nginx
  tls:
    - hosts:
      - example.apps.infra.house
      secretName: example-cert
  rules:
  - host: example.apps.infra.house
    http:
      paths:
      - path: /app1(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: example-app1
            port:
              name: http

      # Note the use of single quotes (') here; this is
      # important; using double quotes we would need to
      # write `\\$` instead of `\$`.
      - path: '/api/v2/\$metadata'
        pathType: Prefix
        backend:
          service:
            name: example-app2
            port:
              name: http

The resulting location directives look like:

location ~* "^/api/v2/\$metadata" {
...
}
location ~* "^/app1(/|$)(.*)" {
...
}

And a request for the $metadata path succeeds.

larsks
  • 277,717
  • 41
  • 399
  • 399
  • Thank you very much for that detailed answer. I fiddled around my problem a bit more and it seems that the issue occurs only if I have a second ingress resource that have the `nginx.ingress.kubernetes.io/rewrite-target` annotation. Even if the rules doesn't matches, it gets prioritized even for the route of my other ingress with the $ sign. – Dunge Feb 10 '23 at 04:44
  • 1
    I checked a diff of the resulting `nginx.conf`. As soon as ANY ingress resource have the annotation, no matter what path it haves as a rule or where it redirect, ALL the "location" parts of the nginx config get replaced from `location = /...` to `location ~* "^/..."` – Dunge Feb 10 '23 at 05:02
  • Are your ingress resources using the same hostname? I've posted an update to the answer that I think resolves the problem. – larsks Feb 10 '23 at 12:38
  • Thank you very much again. It works by using single quotes and escaping the $ with a \. Is this pattern documented somewhere? Also, don't you think it's a bit weird that rules in an ingress that do not use a certain annotation get influenced by a different external one that do? It feels a bit like a bug to me, especially that they are separate entries in the nginx.conf file. I can see it causing problem in a situation like someone installing a new helm chart including its own ingress resource in a cluster and it would break a completely different one and end up causing confusion. – Dunge Feb 10 '23 at 23:05
  • Re: "Is this pattern documented somewhere?", the `\ ` character is standard syntax for escaping regex metacharacters like `^`, `$`, etc. And the use of single quotes vs double quotes in YAML is part of the YAML spec (https://yaml.org/spec/1.2.2/#732-single-quoted-style). I agree that the behavior of the nginx operator seems a bit surprising. – larsks Feb 10 '23 at 23:47