diff --git a/docs/annotations.md b/docs/annotations.md index 480dd6ef46..5e94ed0b9e 100644 --- a/docs/annotations.md +++ b/docs/annotations.md @@ -51,6 +51,7 @@ Key: | `add-base-url` | Add `` tag to HTML. | | nginx | `base-url-scheme` | Specify the scheme of the `` tags. | | nginx | `preserve-host` | Whether to pass the client request host (`true`) or the origin hostname (`false`) in the HTTP Host field. | | trafficserver +| `x-forwarded-prefix` | Add the non-standard `X-Forwarded-Prefix` header to the request with the value of the matched location. | | nginx ## CORS Related | Name | Meaning | Default | Controller diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 2bbd8814d1..b6064de6b0 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -47,6 +47,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/upstreamhashby" "k8s.io/ingress-nginx/internal/ingress/annotations/upstreamvhost" "k8s.io/ingress-nginx/internal/ingress/annotations/vtsfilterkey" + "k8s.io/ingress-nginx/internal/ingress/annotations/xforwardedprefix" "k8s.io/ingress-nginx/internal/ingress/errors" "k8s.io/ingress-nginx/internal/ingress/resolver" ) @@ -81,6 +82,7 @@ type Ingress struct { UpstreamVhost string VtsFilterKey string Whitelist ipwhitelist.SourceRange + XForwardedPrefix bool } // Extractor defines the annotation parsers to be used in the extraction of annotations @@ -115,6 +117,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "UpstreamVhost": upstreamvhost.NewParser(cfg), "VtsFilterKey": vtsfilterkey.NewParser(cfg), "Whitelist": ipwhitelist.NewParser(cfg), + "XForwardedPrefix": xforwardedprefix.NewParser(cfg), }, } } diff --git a/internal/ingress/annotations/xforwardedprefix/main.go b/internal/ingress/annotations/xforwardedprefix/main.go new file mode 100644 index 0000000000..e25beebe39 --- /dev/null +++ b/internal/ingress/annotations/xforwardedprefix/main.go @@ -0,0 +1,39 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package xforwardedprefix + +import ( + extensions "k8s.io/api/extensions/v1beta1" + + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +type xforwardedprefix struct { + r resolver.Resolver +} + +// NewParser creates a new xforwardedprefix annotation parser +func NewParser(r resolver.Resolver) parser.IngressAnnotation { + return xforwardedprefix{r} +} + +// Parse parses the annotations contained in the ingress rule +// used to add an x-forwarded-prefix header to the request +func (cbbs xforwardedprefix) Parse(ing *extensions.Ingress) (interface{}, error) { + return parser.GetBoolAnnotation("x-forwarded-prefix", ing) +} diff --git a/internal/ingress/annotations/xforwardedprefix/main_test.go b/internal/ingress/annotations/xforwardedprefix/main_test.go new file mode 100644 index 0000000000..95a4d1f7cb --- /dev/null +++ b/internal/ingress/annotations/xforwardedprefix/main_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package xforwardedprefix + +import ( + "testing" + + api "k8s.io/api/core/v1" + extensions "k8s.io/api/extensions/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +func TestParse(t *testing.T) { + annotation := parser.GetAnnotationWithPrefix("x-forwarded-prefix") + ap := NewParser(&resolver.Mock{}) + if ap == nil { + t.Fatalf("expected a parser.IngressAnnotation but returned nil") + } + + testCases := []struct { + annotations map[string]string + expected bool + }{ + {map[string]string{annotation: "true"}, true}, + {map[string]string{annotation: "1"}, true}, + {map[string]string{annotation: ""}, false}, + {map[string]string{}, false}, + {nil, false}, + } + + ing := &extensions.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: extensions.IngressSpec{}, + } + + for _, testCase := range testCases { + ing.SetAnnotations(testCase.annotations) + result, _ := ap.Parse(ing) + if result != testCase.expected { + t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) + } + } +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 1e0fb2d996..64cb5959da 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -473,6 +473,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([] loc.VtsFilterKey = anns.VtsFilterKey loc.Whitelist = anns.Whitelist loc.Denied = anns.Denied + loc.XForwardedPrefix = anns.XForwardedPrefix if loc.Redirect.FromToWWW { server.RedirectFromToWWW = true @@ -503,6 +504,7 @@ func (n *NGINXController) getBackendServers(ingresses []*extensions.Ingress) ([] VtsFilterKey: anns.VtsFilterKey, Whitelist: anns.Whitelist, Denied: anns.Denied, + XForwardedPrefix: anns.XForwardedPrefix, } if loc.Redirect.FromToWWW { diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index 08ff249927..fe72dd6ee8 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -324,20 +324,25 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string { } } + xForwardedPrefix := "" + if location.XForwardedPrefix { + xForwardedPrefix = fmt.Sprintf(`proxy_set_header X-Forwarded-Prefix "%s"; + `, path) + } if location.Rewrite.Target == slash { // special case redirect to / // ie /something to / return fmt.Sprintf(` rewrite %s(.*) /$1 break; rewrite %s / break; - proxy_pass %s://%s; - %v`, path, location.Path, proto, upstreamName, abu) + %vproxy_pass %s://%s; + %v`, path, location.Path, xForwardedPrefix, proto, upstreamName, abu) } return fmt.Sprintf(` rewrite %s(.*) %s/$1 break; - proxy_pass %s://%s; - %v`, path, location.Rewrite.Target, proto, upstreamName, abu) + %vproxy_pass %s://%s; + %v`, path, location.Rewrite.Target, xForwardedPrefix, proto, upstreamName, abu) } // default proxy_pass diff --git a/internal/ingress/controller/template/template_test.go b/internal/ingress/controller/template/template_test.go index 1e4c9b75bf..9a28b69ef4 100644 --- a/internal/ingress/controller/template/template_test.go +++ b/internal/ingress/controller/template/template_test.go @@ -36,64 +36,70 @@ import ( var ( // TODO: add tests for secure endpoints tmplFuncTestcases = map[string]struct { - Path string - Target string - Location string - ProxyPass string - AddBaseURL bool - BaseURLScheme string - Sticky bool + Path string + Target string + Location string + ProxyPass string + AddBaseURL bool + BaseURLScheme string + Sticky bool + XForwardedPrefix bool }{ - "invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false, "", false}, + "invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false, "", false, false}, "redirect / to /jenkins": {"/", "/jenkins", "~* /", ` rewrite /(.*) /jenkins/$1 break; proxy_pass http://upstream-name; - `, false, "", false}, + `, false, "", false, false}, "redirect /something to /": {"/something", "/", `~* ^/something\/?(?.*)`, ` rewrite /something/(.*) /$1 break; rewrite /something / break; proxy_pass http://upstream-name; - `, false, "", false}, + `, false, "", false, false}, "redirect /end-with-slash/ to /not-root": {"/end-with-slash/", "/not-root", "~* ^/end-with-slash/(?.*)", ` rewrite /end-with-slash/(.*) /not-root/$1 break; proxy_pass http://upstream-name; - `, false, "", false}, + `, false, "", false, false}, "redirect /something-complex to /not-root": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?.*)`, ` rewrite /something-complex/(.*) /not-root/$1 break; proxy_pass http://upstream-name; - `, false, "", false}, + `, false, "", false, false}, "redirect / to /jenkins and rewrite": {"/", "/jenkins", "~* /", ` rewrite /(.*) /jenkins/$1 break; proxy_pass http://upstream-name; subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1' ro; - `, true, "", false}, + `, true, "", false, false}, "redirect /something to / and rewrite": {"/something", "/", `~* ^/something\/?(?.*)`, ` rewrite /something/(.*) /$1 break; rewrite /something / break; proxy_pass http://upstream-name; subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1' ro; - `, true, "", false}, + `, true, "", false, false}, "redirect /end-with-slash/ to /not-root and rewrite": {"/end-with-slash/", "/not-root", `~* ^/end-with-slash/(?.*)`, ` rewrite /end-with-slash/(.*) /not-root/$1 break; proxy_pass http://upstream-name; subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1' ro; - `, true, "", false}, + `, true, "", false, false}, "redirect /something-complex to /not-root and rewrite": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?.*)`, ` rewrite /something-complex/(.*) /not-root/$1 break; proxy_pass http://upstream-name; subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1' ro; - `, true, "", false}, + `, true, "", false, false}, "redirect /something to / and rewrite with specific scheme": {"/something", "/", `~* ^/something\/?(?.*)`, ` rewrite /something/(.*) /$1 break; rewrite /something / break; proxy_pass http://upstream-name; subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1' ro; - `, true, "http", false}, + `, true, "http", false, false}, "redirect / to /something with sticky enabled": {"/", "/something", `~* /`, ` rewrite /(.*) /something/$1 break; proxy_pass http://sticky-upstream-name; - `, false, "http", true}, + `, false, "http", true, false}, + "add the X-Forwarded-Prefix header": {"/there", "/something", `~* ^/there\/?(?.*)`, ` + rewrite /there/(.*) /something/$1 break; + proxy_set_header X-Forwarded-Prefix "/there/"; + proxy_pass http://sticky-upstream-name; + `, false, "http", true, true}, } ) @@ -136,9 +142,10 @@ func TestBuildProxyPass(t *testing.T) { for k, tc := range tmplFuncTestcases { loc := &ingress.Location{ - Path: tc.Path, - Rewrite: rewrite.Config{Target: tc.Target, AddBaseURL: tc.AddBaseURL, BaseURLScheme: tc.BaseURLScheme}, - Backend: defaultBackend, + Path: tc.Path, + Rewrite: rewrite.Config{Target: tc.Target, AddBaseURL: tc.AddBaseURL, BaseURLScheme: tc.BaseURLScheme}, + Backend: defaultBackend, + XForwardedPrefix: tc.XForwardedPrefix, } backends := []*ingress.Backend{} diff --git a/internal/ingress/types.go b/internal/ingress/types.go index d9da68b2fa..be4d03a541 100644 --- a/internal/ingress/types.go +++ b/internal/ingress/types.go @@ -259,6 +259,10 @@ type Location struct { // DefaultBackend allows the use of a custom default backend for this location. // +optional DefaultBackend *apiv1.Service `json:"defaultBackend,omitempty"` + // XForwardedPrefix allows to add a header X-Forwarded-Prefix to the request with the + // original location. + // +optional + XForwardedPrefix bool `json:"xForwardedPrefix,omitempty"` } // SSLPassthroughBackend describes a SSL upstream server configured diff --git a/internal/ingress/types_equals.go b/internal/ingress/types_equals.go index c86e33e7a4..a5902646a8 100644 --- a/internal/ingress/types_equals.go +++ b/internal/ingress/types_equals.go @@ -367,6 +367,9 @@ func (l1 *Location) Equal(l2 *Location) bool { if l1.UpstreamVhost != l2.UpstreamVhost { return false } + if l1.XForwardedPrefix != l2.XForwardedPrefix { + return false + } return true }