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
}