Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Rewrite-Target Annotation Edge Case #3078

Merged
merged 3 commits into from
Sep 14, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions internal/ingress/controller/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ var (
"buildOpentracing": buildOpentracing,
"proxySetHeader": proxySetHeader,
"buildInfluxDB": buildInfluxDB,
"atLeastOneNeedsRewrite": atLeastOneNeedsRewrite,
}
)

Expand Down Expand Up @@ -287,17 +288,40 @@ func buildResolvers(res interface{}, disableIpv6 interface{}) string {
return strings.Join(r, " ") + ";"
}

func needsRewrite(location *ingress.Location) bool {
if len(location.Rewrite.Target) > 0 && location.Rewrite.Target != location.Path {
return true
}
return false
}

// atLeastOneNeedsRewrite checks if the nginx.ingress.kubernetes.io/rewrite-target annotation is used on the '/' path
func atLeastOneNeedsRewrite(input interface{}) bool {
locations, ok := input.([]*ingress.Location)
if !ok {
glog.Errorf("expected an '[]*ingress.Location' type but %T was returned", input)
return false
}

for _, location := range locations {
if needsRewrite(location) {
return true
}
}
return false
}

// buildLocation produces the location string, if the ingress has redirects
// (specified through the nginx.ingress.kubernetes.io/rewrite-target annotation)
func buildLocation(input interface{}) string {
func buildLocation(input interface{}, rewrite bool) string {
location, ok := input.(*ingress.Location)
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return slash
}

path := location.Path
if len(location.Rewrite.Target) > 0 && location.Rewrite.Target != path {
if needsRewrite(location) {
if path == slash {
return fmt.Sprintf("~* %s", path)
}
Expand All @@ -310,6 +334,12 @@ func buildLocation(input interface{}) string {
return fmt.Sprintf(`~* ^%s%s`, path, baseuri)
}

if rewrite == true {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: you could do if rewrite { since rewrite is boolean.

if path == slash {
return path
}
return fmt.Sprintf(`^~ %s`, path)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed internally, this is not necessary every time there's rewrite annotation used, rather it is necessary only when the rewrite annotation is used on / path. If rewrite annotation is used i.e on /foo, then the generated location would like ~* ^/foo\/?(?<baseuri>.*), which will not match things like /.well-known/acme/challenge.

Given this, do you think we should simplify this further and apply ^~ only when there's an annotation on / path?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically, the conflict would occur with any leading portion of /.well-known/acme/challenge

eg. /.well-known

right?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and that expands further to any subpath of a path that does NOT use rewrite... so likely simpler logic this way... ie. just check that rewrite is used instead of looking for conflicting paths

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're saying this as well, the conflict you're describing can happen without rewrite target annotation which is not what we are trying to fix (I'd even argue it is fine as it is).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has the potential to break exact path matching if the ingress with the rewrite-target had a regex path that matched the exact path of another ingress (without the rewrite-target annotation). Example:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    ingress.kubernetes.io/rewrite-target: "/new/backend"
  name: example
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: example
          servicePort: 80
        path: "/apps/.*"
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: example
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: example
          servicePort: 80
        path: "/app/myapp"

A request to /app/myapp would match the the location ~* /app/.* instead of the exact path due to NGINX location modifier logic.

}
return path
}

Expand Down
2 changes: 1 addition & 1 deletion internal/ingress/controller/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ func TestBuildLocation(t *testing.T) {
Rewrite: rewrite.Config{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
}

newLoc := buildLocation(loc)
newLoc := buildLocation(loc, tc.Path != tc.Target)
if tc.Location != newLoc {
t.Errorf("%s: expected '%v' but returned %v", k, tc.Location, newLoc)
}
Expand Down
6 changes: 4 additions & 2 deletions rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,9 @@ http {

{{/* build the maps that will be use to validate the Whitelist */}}
{{ range $server := $servers }}
{{ $usesRewrite := atLeastOneNeedsRewrite $server.Locations }}
{{ range $location := $server.Locations }}
{{ $path := buildLocation $location }}
{{ $path := buildLocation $location $usesRewrite }}

{{ if isLocationAllowed $location }}
{{ if gt (len $location.Whitelist.CIDR) 0 }}
Expand Down Expand Up @@ -818,8 +819,9 @@ stream {
{{ $server.ServerSnippet }}
{{ end }}

{{ $usesRewrite := atLeastOneNeedsRewrite $server.Locations }}
{{ range $location := $server.Locations }}
{{ $path := buildLocation $location }}
{{ $path := buildLocation $location $usesRewrite }}
{{ $proxySetHeader := proxySetHeader $location }}
{{ $authPath := buildAuthLocation $location }}

Expand Down
53 changes: 53 additions & 0 deletions test/e2e/annotations/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,57 @@ var _ = framework.IngressNginxDescribe("Annotations - Rewrite", func() {
Expect(logs).To(ContainSubstring(`"(?i)/something$" matches "/something", client:`))
Expect(logs).To(ContainSubstring(`rewritten data: "/", args: "",`))
})

It("should use correct longest path match", func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which one of these tests is actually failing without your changes? Can we avoid introducing more tests? I think we concluded that there's only once case we are fixing, why not test just that (which is given ingress with path=/ and rewrite annotation, when I create another ingress for the same host with /.well-known/acme/challenge requests to /.well-known/acme/challenge goes to the service targeted by /)?

Copy link
Contributor Author

@zrdaley zrdaley Sep 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the important test for that case. The other can has been be removed.

host := "rewrite.bar.com"
expectBodyRequestURI := fmt.Sprintf("request_uri=http://%v:8080/.well-known/acme/challenge", host)
annotations := map[string]string{}
rewriteAnnotations := map[string]string{
"nginx.ingress.kubernetes.io/rewrite-target": "/new/backend",
}

By("creating a regular ingress definition")
ing := framework.NewSingleIngress("kube-lego", "/.well-known/acme/challenge", host, f.IngressController.Namespace, "http-svc", 80, &annotations)
_, err := f.EnsureIngress(ing)
Expect(err).NotTo(HaveOccurred())
Expect(ing).NotTo(BeNil())

err = f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "/.well-known/acme/challenge")
})
Expect(err).NotTo(HaveOccurred())

By("making a request to the non-rewritten location")
resp, body, errs := gorequest.New().
Get(f.IngressController.HTTPURL+"/.well-known/acme/challenge").
Set("Host", host).
End()

Expect(len(errs)).Should(Equal(0))
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
Expect(body).Should(ContainSubstring(expectBodyRequestURI))

By(`creating an ingress definition with the rewrite-target annotation set on the "/" location`)
rewriteIng := framework.NewSingleIngress("rewrite-index", "/", host, f.IngressController.Namespace, "http-svc", 80, &rewriteAnnotations)
_, err = f.EnsureIngress(rewriteIng)
Expect(err).NotTo(HaveOccurred())
Expect(rewriteIng).NotTo(BeNil())

err = f.WaitForNginxServer(host,
func(server string) bool {
return strings.Contains(server, "location ~* / {")
})
Expect(err).NotTo(HaveOccurred())

By("making a second request to the non-rewritten location")
resp, body, errs = gorequest.New().
Get(f.IngressController.HTTPURL+"/.well-known/acme/challenge").
Set("Host", host).
End()

Expect(len(errs)).Should(Equal(0))
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
Expect(body).Should(ContainSubstring(expectBodyRequestURI))
})
})