diff --git a/controllers/nginx/controller.go b/controllers/nginx/controller.go index 166fe484d..d1fb498f8 100644 --- a/controllers/nginx/controller.go +++ b/controllers/nginx/controller.go @@ -42,6 +42,7 @@ import ( "k8s.io/contrib/ingress/controllers/nginx/healthcheck" "k8s.io/contrib/ingress/controllers/nginx/nginx" + "k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite" ) const ( @@ -609,6 +610,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur for _, loc := range server.Locations { if loc.Path == rootLocation && nginxPath == rootLocation && loc.IsDefBackend { 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 continue } @@ -622,9 +629,15 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg nginx.NginxConfigur } 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{ Path: nginxPath, Upstream: *ups, + Redirect: *locRew, }) } } diff --git a/controllers/nginx/nginx/nginx.go b/controllers/nginx/nginx/nginx.go index e46b4d80c..7b2b58488 100644 --- a/controllers/nginx/nginx/nginx.go +++ b/controllers/nginx/nginx/nginx.go @@ -16,6 +16,10 @@ limitations under the License. package nginx +import ( + "k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite" +) + // IngressConfig describes an NGINX configuration type IngressConfig struct { Upstreams []*Upstream @@ -88,6 +92,7 @@ type Location struct { Path string IsDefBackend bool Upstream Upstream + Redirect rewrite.Redirect } // LocationByPath sorts location by path diff --git a/controllers/nginx/nginx/rewrite/main.go b/controllers/nginx/nginx/rewrite/main.go new file mode 100644 index 000000000..f2726ad91 --- /dev/null +++ b/controllers/nginx/nginx/rewrite/main.go @@ -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 +} diff --git a/controllers/nginx/nginx/rewrite/main_test.go b/controllers/nginx/nginx/rewrite/main_test.go new file mode 100644 index 000000000..6fc500c0b --- /dev/null +++ b/controllers/nginx/nginx/rewrite/main_test.go @@ -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) + } +} diff --git a/controllers/nginx/nginx/template.go b/controllers/nginx/nginx/template.go index ba8c36f8b..b1b0b1cfc 100644 --- a/controllers/nginx/nginx/template.go +++ b/controllers/nginx/nginx/template.go @@ -21,12 +21,17 @@ import ( "encoding/json" "fmt" "regexp" + "strings" "text/template" "github.com/fatih/structs" "github.com/golang/glog" ) +const ( + slash = "/" +) + var ( camelRegexp = regexp.MustCompile("[0-9A-Za-z]+") tmplPath = "/etc/nginx/template/nginx.tmpl" @@ -40,6 +45,8 @@ var ( return true }, + "buildLocation": buildLocation, + "buildProxyPass": buildProxyPass, } ) @@ -101,3 +108,60 @@ func toCamelCase(src string) string { } 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 '' ''; +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) +} diff --git a/controllers/nginx/nginx/template_test.go b/controllers/nginx/nginx/template_test.go new file mode 100644 index 000000000..945f12543 --- /dev/null +++ b/controllers/nginx/nginx/template_test.go @@ -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) + } + } +}