Add support for rewrite
This commit is contained in:
parent
6968e23f8d
commit
95e85b57e3
6 changed files with 371 additions and 0 deletions
|
@ -42,6 +42,7 @@ import (
|
||||||
|
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/healthcheck"
|
"k8s.io/contrib/ingress/controllers/nginx/healthcheck"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx"
|
"k8s.io/contrib/ingress/controllers/nginx/nginx"
|
||||||
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -609,6 +610,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur
|
||||||
for _, loc := range server.Locations {
|
for _, loc := range server.Locations {
|
||||||
if loc.Path == rootLocation && nginxPath == rootLocation && loc.IsDefBackend {
|
if loc.Path == rootLocation && nginxPath == rootLocation && loc.IsDefBackend {
|
||||||
loc.Upstream = *ups
|
loc.Upstream = *ups
|
||||||
|
locRew, err := rewrite.ParseAnnotations(ing)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(3).Infof("error parsing rewrite annotations for Ingress rule %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
|
||||||
|
}
|
||||||
|
loc.Redirect = *locRew
|
||||||
|
|
||||||
addLoc = false
|
addLoc = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -622,9 +629,15 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur
|
||||||
}
|
}
|
||||||
|
|
||||||
if addLoc {
|
if addLoc {
|
||||||
|
locRew, err := rewrite.ParseAnnotations(ing)
|
||||||
|
if err != nil {
|
||||||
|
glog.V(3).Infof("error parsing rewrite annotations for Ingress rule %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
server.Locations = append(server.Locations, &nginx.Location{
|
server.Locations = append(server.Locations, &nginx.Location{
|
||||||
Path: nginxPath,
|
Path: nginxPath,
|
||||||
Upstream: *ups,
|
Upstream: *ups,
|
||||||
|
Redirect: *locRew,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
package nginx
|
package nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
||||||
|
)
|
||||||
|
|
||||||
// IngressConfig describes an NGINX configuration
|
// IngressConfig describes an NGINX configuration
|
||||||
type IngressConfig struct {
|
type IngressConfig struct {
|
||||||
Upstreams []*Upstream
|
Upstreams []*Upstream
|
||||||
|
@ -88,6 +92,7 @@ type Location struct {
|
||||||
Path string
|
Path string
|
||||||
IsDefBackend bool
|
IsDefBackend bool
|
||||||
Upstream Upstream
|
Upstream Upstream
|
||||||
|
Redirect rewrite.Redirect
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocationByPath sorts location by path
|
// LocationByPath sorts location by path
|
||||||
|
|
84
controllers/nginx/nginx/rewrite/main.go
Normal file
84
controllers/nginx/nginx/rewrite/main.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 rewrite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rewrite = "ingress-nginx.kubernetes.io/rewrite-to"
|
||||||
|
fixUrls = "ingress-nginx.kubernetes.io/fix-urls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrMissingAnnotations is returned when the ingress rule
|
||||||
|
// does not contains annotations related with redirect or strip prefix
|
||||||
|
type ErrMissingAnnotations struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrMissingAnnotations) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect returns authentication configuration for an Ingress rule
|
||||||
|
type Redirect struct {
|
||||||
|
// To URI where the traffic must be redirected
|
||||||
|
To string
|
||||||
|
// Rewrite indicates if is required to change the
|
||||||
|
// links in the response from the upstream servers
|
||||||
|
Rewrite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ingAnnotations map[string]string
|
||||||
|
|
||||||
|
func (a ingAnnotations) rewrite() string {
|
||||||
|
val, ok := a[rewrite]
|
||||||
|
if ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ingAnnotations) fixUrls() bool {
|
||||||
|
val, ok := a[fixUrls]
|
||||||
|
if ok {
|
||||||
|
if b, err := strconv.ParseBool(val); err == nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAnnotations parses the annotations contained in the ingress
|
||||||
|
// rule used to rewrite the defined paths
|
||||||
|
func ParseAnnotations(ing *extensions.Ingress) (*Redirect, error) {
|
||||||
|
if ing.GetAnnotations() == nil {
|
||||||
|
return &Redirect{}, ErrMissingAnnotations{"no annotations present"}
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := ingAnnotations(ing.GetAnnotations()).rewrite()
|
||||||
|
rw := ingAnnotations(ing.GetAnnotations()).fixUrls()
|
||||||
|
return &Redirect{
|
||||||
|
To: rt,
|
||||||
|
Rewrite: rw,
|
||||||
|
}, nil
|
||||||
|
}
|
118
controllers/nginx/nginx/rewrite/main_test.go
Normal file
118
controllers/nginx/nginx/rewrite/main_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 rewrite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||||
|
"k8s.io/kubernetes/pkg/util/intstr"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defRoute = "/demo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildIngress() *extensions.Ingress {
|
||||||
|
defaultBackend := extensions.IngressBackend{
|
||||||
|
ServiceName: "default-backend",
|
||||||
|
ServicePort: intstr.FromInt(80),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &extensions.Ingress{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "foo",
|
||||||
|
Namespace: api.NamespaceDefault,
|
||||||
|
},
|
||||||
|
Spec: extensions.IngressSpec{
|
||||||
|
Backend: &extensions.IngressBackend{
|
||||||
|
ServiceName: "default-backend",
|
||||||
|
ServicePort: intstr.FromInt(80),
|
||||||
|
},
|
||||||
|
Rules: []extensions.IngressRule{
|
||||||
|
{
|
||||||
|
Host: "foo.bar.com",
|
||||||
|
IngressRuleValue: extensions.IngressRuleValue{
|
||||||
|
HTTP: &extensions.HTTPIngressRuleValue{
|
||||||
|
Paths: []extensions.HTTPIngressPath{
|
||||||
|
{
|
||||||
|
Path: "/foo",
|
||||||
|
Backend: defaultBackend,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnnotations(t *testing.T) {
|
||||||
|
ing := buildIngress()
|
||||||
|
|
||||||
|
r := ingAnnotations(ing.GetAnnotations()).rewrite()
|
||||||
|
if r != "" {
|
||||||
|
t.Error("Expected no redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
f := ingAnnotations(ing.GetAnnotations()).fixUrls()
|
||||||
|
if f != false {
|
||||||
|
t.Error("Expected false as fix-urls but %v was returend", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]string{}
|
||||||
|
data[rewrite] = defRoute
|
||||||
|
data[fixUrls] = "true"
|
||||||
|
ing.SetAnnotations(data)
|
||||||
|
|
||||||
|
r = ingAnnotations(ing.GetAnnotations()).rewrite()
|
||||||
|
if r != defRoute {
|
||||||
|
t.Error("Expected %v as rewrite but %v was returend", defRoute, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
f = ingAnnotations(ing.GetAnnotations()).fixUrls()
|
||||||
|
if f != true {
|
||||||
|
t.Error("Expected true as fix-urls but %v was returend", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithoutAnnotations(t *testing.T) {
|
||||||
|
ing := buildIngress()
|
||||||
|
_, err := ParseAnnotations(ing)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error with ingress without annotations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRedirect(t *testing.T) {
|
||||||
|
ing := buildIngress()
|
||||||
|
|
||||||
|
data := map[string]string{}
|
||||||
|
data[rewrite] = defRoute
|
||||||
|
ing.SetAnnotations(data)
|
||||||
|
|
||||||
|
redirect, err := ParseAnnotations(ing)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Uxpected error with ingress: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if redirect.To != defRoute {
|
||||||
|
t.Errorf("Expected %v as redirect but returned %s", defRoute, redirect.To)
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,12 +21,17 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/fatih/structs"
|
"github.com/fatih/structs"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
slash = "/"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
camelRegexp = regexp.MustCompile("[0-9A-Za-z]+")
|
camelRegexp = regexp.MustCompile("[0-9A-Za-z]+")
|
||||||
tmplPath = "/etc/nginx/template/nginx.tmpl"
|
tmplPath = "/etc/nginx/template/nginx.tmpl"
|
||||||
|
@ -40,6 +45,8 @@ var (
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
"buildLocation": buildLocation,
|
||||||
|
"buildProxyPass": buildProxyPass,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,3 +108,60 @@ func toCamelCase(src string) string {
|
||||||
}
|
}
|
||||||
return string(bytes.Join(chunks, nil))
|
return string(bytes.Join(chunks, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildLocation(input interface{}) string {
|
||||||
|
location, ok := input.(*Location)
|
||||||
|
if !ok {
|
||||||
|
return slash
|
||||||
|
}
|
||||||
|
|
||||||
|
path := location.Path
|
||||||
|
if len(location.Redirect.To) > 0 && location.Redirect.To != path {
|
||||||
|
// if path != slash && !strings.HasSuffix(path, slash) {
|
||||||
|
// path = fmt.Sprintf("%s/", path)
|
||||||
|
// }
|
||||||
|
return fmt.Sprintf("~* %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildProxyPass(input interface{}) string {
|
||||||
|
location, ok := input.(*Location)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
path := location.Path
|
||||||
|
|
||||||
|
if path == location.Redirect.To {
|
||||||
|
return fmt.Sprintf("proxy_pass http://%s;", location.Upstream.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if path != slash && !strings.HasSuffix(path, slash) {
|
||||||
|
path = fmt.Sprintf("%s/", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(location.Redirect.To) > 0 {
|
||||||
|
rc := ""
|
||||||
|
if location.Redirect.Rewrite {
|
||||||
|
rc = fmt.Sprintf(`sub_filter '<head(.*)>' '<head$1><base href="$scheme://$server_name%v">';
|
||||||
|
sub_filter_once off;`, location.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if location.Redirect.To == slash {
|
||||||
|
// special case redirect to /
|
||||||
|
// ie /something to /
|
||||||
|
return fmt.Sprintf(`rewrite %s(.*) /$1 break;
|
||||||
|
proxy_pass http://%s;
|
||||||
|
%v`, path, location.Upstream.Name, rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`rewrite %s(.*) %s/$1 break;
|
||||||
|
proxy_pass http://%s;
|
||||||
|
%v`, path, location.Redirect.To, location.Upstream.Name, rc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default proxy_pass
|
||||||
|
return fmt.Sprintf("proxy_pass http://%s;", location.Upstream.Name)
|
||||||
|
}
|
||||||
|
|
87
controllers/nginx/nginx/template_test.go
Normal file
87
controllers/nginx/nginx/template_test.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 nginx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tmplFuncTestcases = map[string]struct {
|
||||||
|
Path string
|
||||||
|
To string
|
||||||
|
Location string
|
||||||
|
ProxyPass string
|
||||||
|
Rewrite bool
|
||||||
|
}{
|
||||||
|
"invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false},
|
||||||
|
"redirect / to /jenkins": {"/", "/jenkins", "~* /",
|
||||||
|
`rewrite /(.*) /jenkins/$1 break;
|
||||||
|
proxy_pass http://upstream-name;
|
||||||
|
`, false},
|
||||||
|
"redirect /something to /": {"/something", "/", "~* /something/", `rewrite /something/(.*) /$1 break;
|
||||||
|
proxy_pass http://upstream-name;
|
||||||
|
`, 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},
|
||||||
|
"redirect / to /jenkins and rewrite": {"/", "/jenkins", "~* /",
|
||||||
|
`rewrite /(.*) /jenkins/$1 break;
|
||||||
|
proxy_pass http://upstream-name;
|
||||||
|
sub_filter "//$host/" "//$host/jenkins";
|
||||||
|
sub_filter_once off;`, true},
|
||||||
|
"redirect /something to / and rewrite": {"/something", "/", "~* /something/", `rewrite /something/(.*) /$1 break;
|
||||||
|
proxy_pass http://upstream-name;
|
||||||
|
sub_filter "//$host/something" "//$host/";
|
||||||
|
sub_filter_once off;`, true},
|
||||||
|
"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;
|
||||||
|
sub_filter "//$host/something-complex" "//$host/not-root";
|
||||||
|
sub_filter_once off;`, true},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildLocation(t *testing.T) {
|
||||||
|
for k, tc := range tmplFuncTestcases {
|
||||||
|
loc := &Location{
|
||||||
|
Path: tc.Path,
|
||||||
|
Redirect: rewrite.Redirect{tc.To, tc.Rewrite},
|
||||||
|
}
|
||||||
|
|
||||||
|
newLoc := buildLocation(loc)
|
||||||
|
if tc.Location != newLoc {
|
||||||
|
t.Errorf("%s: expected %v but returned %v", k, tc.Location, newLoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProxyPass(t *testing.T) {
|
||||||
|
for k, tc := range tmplFuncTestcases {
|
||||||
|
loc := &Location{
|
||||||
|
Path: tc.Path,
|
||||||
|
Redirect: rewrite.Redirect{tc.To, tc.Rewrite},
|
||||||
|
Upstream: Upstream{Name: "upstream-name"},
|
||||||
|
}
|
||||||
|
|
||||||
|
pp := buildProxyPass(loc)
|
||||||
|
if tc.ProxyPass != pp {
|
||||||
|
t.Errorf("%s: expected \n%v \nbut returned \n%v", k, tc.ProxyPass, pp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue