Skip to content

Commit

Permalink
Merge pull request #1184 from aledbf/redirects
Browse files Browse the repository at this point in the history
Add support for temporal and permanent redirects
  • Loading branch information
aledbf authored Aug 20, 2017
2 parents 1e6b212 + ed68194 commit 7010627
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 33 deletions.
24 changes: 23 additions & 1 deletion controllers/nginx/pkg/cmd/controller/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,6 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
IP: svc.Spec.ClusterIP,
Port: port,
ProxyProtocol: false,

})
}

Expand All @@ -467,11 +466,33 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
// https://trac.nginx.org/nginx/ticket/631
var longestName int
var serverNameBytes int
redirectServers := make(map[string]string)
for _, srv := range ingressCfg.Servers {
if longestName < len(srv.Hostname) {
longestName = len(srv.Hostname)
}
serverNameBytes += len(srv.Hostname)
if srv.RedirectFromToWWW {
var n string
if strings.HasPrefix(srv.Hostname, "www.") {
n = strings.TrimLeft(srv.Hostname, "www.")
} else {
n = fmt.Sprintf("www.%v", srv.Hostname)
}
glog.V(3).Infof("creating redirect from %v to", srv.Hostname, n)
if _, ok := redirectServers[n]; !ok {
found := false
for _, esrv := range ingressCfg.Servers {
if esrv.Hostname == n {
found = true
break
}
}
if !found {
redirectServers[n] = srv.Hostname
}
}
}
}
if cfg.ServerNameHashBucketSize == 0 {
nameHashBucketSize := nginxHashBucketSize(longestName)
Expand Down Expand Up @@ -562,6 +583,7 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
CustomErrors: len(cfg.CustomHTTPErrors) > 0,
Cfg: cfg,
IsIPV6Enabled: n.isIPV6Enabled && !cfg.DisableIpv6,
RedirectServers: redirectServers,
}

// We need to extract the endpoints to be used in the fastcgi error handler
Expand Down
1 change: 1 addition & 0 deletions controllers/nginx/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,4 +428,5 @@ type TemplateConfig struct {
CustomErrors bool
Cfg Configuration
IsIPV6Enabled bool
RedirectServers map[string]string
}
21 changes: 10 additions & 11 deletions controllers/nginx/pkg/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,10 @@ import (
"strings"
text_template "text/template"

"k8s.io/apimachinery/pkg/util/sets"

"github.com/golang/glog"

"github.com/pborman/uuid"

"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/ingress/controllers/nginx/pkg/config"
"k8s.io/ingress/core/pkg/ingress"
ing_net "k8s.io/ingress/core/pkg/net"
Expand Down Expand Up @@ -148,7 +147,7 @@ var (
"formatIP": formatIP,
"buildNextUpstream": buildNextUpstream,
"serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} {
return struct { First, Second interface{} } { all, server }
return struct{ First, Second interface{} }{all, server}
},
}
)
Expand Down Expand Up @@ -197,7 +196,7 @@ func buildLocation(input interface{}) string {
}

path := location.Path
if len(location.Redirect.Target) > 0 && location.Redirect.Target != path {
if len(location.Rewrite.Target) > 0 && location.Rewrite.Target != path {
if path == slash {
return fmt.Sprintf("~* %s", path)
}
Expand Down Expand Up @@ -290,25 +289,25 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
// defProxyPass returns the default proxy_pass, just the name of the upstream
defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, upstreamName)
// if the path in the ingress rule is equals to the target: no special rewrite
if path == location.Redirect.Target {
if path == location.Rewrite.Target {
return defProxyPass
}

if path != slash && !strings.HasSuffix(path, slash) {
path = fmt.Sprintf("%s/", path)
}

if len(location.Redirect.Target) > 0 {
if len(location.Rewrite.Target) > 0 {
abu := ""
if location.Redirect.AddBaseURL {
if location.Rewrite.AddBaseURL {
// path has a slash suffix, so that it can be connected with baseuri directly
bPath := fmt.Sprintf("%s%s", path, "$baseuri")
abu = fmt.Sprintf(`subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host%v">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host%v">' r;
`, bPath, bPath)
}

if location.Redirect.Target == slash {
if location.Rewrite.Target == slash {
// special case redirect to /
// ie /something to /
return fmt.Sprintf(`
Expand All @@ -321,7 +320,7 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
return fmt.Sprintf(`
rewrite %s(.*) %s/$1 break;
proxy_pass %s://%s;
%v`, path, location.Redirect.Target, proto, upstreamName, abu)
%v`, path, location.Rewrite.Target, proto, upstreamName, abu)
}

// default proxy_pass
Expand Down Expand Up @@ -502,4 +501,4 @@ func buildNextUpstream(input interface{}) string {
}

return strings.Join(nextUpstreamCodes, " ")
}
}
13 changes: 6 additions & 7 deletions controllers/nginx/pkg/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ package template

import (
"encoding/json"
"io/ioutil"
"os"
"path"
"reflect"
"strings"
"testing"

"io/ioutil"

"k8s.io/ingress/controllers/nginx/pkg/config"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
Expand Down Expand Up @@ -110,8 +109,8 @@ func TestFormatIP(t *testing.T) {
func TestBuildLocation(t *testing.T) {
for k, tc := range tmplFuncTestcases {
loc := &ingress.Location{
Path: tc.Path,
Redirect: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
Path: tc.Path,
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
}

newLoc := buildLocation(loc)
Expand All @@ -124,9 +123,9 @@ func TestBuildLocation(t *testing.T) {
func TestBuildProxyPass(t *testing.T) {
for k, tc := range tmplFuncTestcases {
loc := &ingress.Location{
Path: tc.Path,
Redirect: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
Backend: "upstream-name",
Path: tc.Path,
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
Backend: "upstream-name",
}

pp := buildProxyPass("", []*ingress.Backend{}, loc)
Expand Down
30 changes: 24 additions & 6 deletions controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@ http {
{{ $zone }}
{{ end }}

{{/* Build server redirects (from/to www) */}}
{{ range $hostname, $to := .RedirectServers }}
server {
listen 80{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen 442{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }} ssl;
{{ if $IsIPV6Enabled }}
listen [::]:80{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
listen [::]:442{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }};
{{ end }}
server_name {{ $hostname }};
return 301 $scheme://{{ $to }}$request_uri;
}
{{ end }}

{{ $backlogSize := .BacklogSize }}
{{ range $index, $server := $servers }}
server {
Expand Down Expand Up @@ -510,9 +524,9 @@ stream {
ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }};
{{ end }}

{{ if not (empty $location.Redirect.AppRoot)}}
{{ if not (empty $location.Rewrite.AppRoot)}}
if ($uri = /) {
return 302 {{ $location.Redirect.AppRoot }};
return 302 {{ $location.Rewrite.AppRoot }};
}
{{ end }}

Expand All @@ -536,7 +550,6 @@ stream {

client_max_body_size "{{ $location.Proxy.BodySize }}";


set $target {{ $location.ExternalAuth.URL }};
proxy_pass $target;
}
Expand All @@ -545,7 +558,7 @@ stream {
location {{ $path }} {
set $proxy_upstream_name "{{ buildUpstreamName $server.Hostname $all.Backends $location }}";

{{ if (or $location.Redirect.ForceSSLRedirect (and (not (empty $server.SSLCertificate)) $location.Redirect.SSLRedirect)) }}
{{ if (or $location.Rewrite.ForceSSLRedirect (and (not (empty $server.SSLCertificate)) $location.Rewrite.SSLRedirect)) }}
# enforce ssl on server side
if ($pass_access_scheme = http) {
return 301 https://$best_http_host$request_uri;
Expand Down Expand Up @@ -575,7 +588,6 @@ stream {
error_page 401 = {{ $location.ExternalAuth.SigninURL }}?rd=$request_uri;
{{ end }}


{{/* if the location contains a rate limit annotation, create one */}}
{{ $limits := buildRateLimit $location }}
{{ range $limit := $limits }}
Expand All @@ -596,6 +608,12 @@ stream {
{{ template "CORS" }}
{{ end }}

{{ if not (empty $location.Redirect.URL) }}
if ($uri ~* {{ $path }}) {
return {{ $location.Redirect.Code }} {{ $location.Redirect.URL }};
}
{{ end }}

client_max_body_size "{{ $location.Proxy.BodySize }}";

proxy_set_header Host $best_http_host;
Expand Down Expand Up @@ -644,7 +662,7 @@ stream {
proxy_next_upstream {{ buildNextUpstream $location.Proxy.NextUpstream }}{{ if $all.Cfg.RetryNonIdempotent }} non_idempotent{{ end }};

{{/* rewrite only works if the content is not compressed */}}
{{ if $location.Redirect.AddBaseURL }}
{{ if $location.Rewrite.AddBaseURL }}
proxy_set_header Accept-Encoding "";
{{ end }}

Expand Down
131 changes: 131 additions & 0 deletions core/pkg/ingress/annotations/redirect/redirect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
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 redirect

import (
"net/http"
"net/url"
"strings"

extensions "k8s.io/api/extensions/v1beta1"

"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/errors"
)

const (
permanent = "ingress.kubernetes.io/permanent-redirect"
temporal = "ingress.kubernetes.io/temporal-redirect"
www = "ingress.kubernetes.io/from-to-www-redirect"
)

// Redirect returns the redirect configuration for an Ingress rule
type Redirect struct {
URL string `json:"url"`
Code int `json:"code"`
FromToWWW bool `json:"fromToWWW"`
}

type redirect struct{}

// NewParser creates a new redirect annotation parser
func NewParser() parser.IngressAnnotation {
return redirect{}
}

// Parse parses the annotations contained in the ingress
// rule used to create a redirect in the paths defined in the rule.
// If the Ingress containes both annotations the execution order is
// temporal and then permanent
func (a redirect) Parse(ing *extensions.Ingress) (interface{}, error) {
r3w, _ := parser.GetBoolAnnotation(www, ing)

tr, err := parser.GetStringAnnotation(temporal, ing)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}

if tr != "" {
if err := isValidURL(tr); err != nil {
return nil, err
}

return &Redirect{
URL: tr,
Code: http.StatusFound,
FromToWWW: r3w,
}, nil
}

pr, err := parser.GetStringAnnotation(permanent, ing)
if err != nil && !errors.IsMissingAnnotations(err) {
return nil, err
}

if pr != "" {
if err := isValidURL(pr); err != nil {
return nil, err
}

return &Redirect{
URL: pr,
Code: http.StatusMovedPermanently,
FromToWWW: r3w,
}, nil
}

if r3w {
return &Redirect{
FromToWWW: r3w,
}, nil
}

return nil, errors.ErrMissingAnnotations
}

// Equal tests for equality between two Redirect types
func (r1 *Redirect) Equal(r2 *Redirect) bool {
if r1 == r2 {
return true
}
if r1 == nil || r2 == nil {
return false
}
if r1.URL != r2.URL {
return false
}
if r1.Code != r2.Code {
return false
}
if r1.FromToWWW != r2.FromToWWW {
return false
}
return true
}

func isValidURL(s string) error {
u, err := url.Parse(s)
if err != nil {
return err
}

if !strings.HasPrefix(u.Scheme, "http") {
return errors.Errorf("only http and https are valid protocols (%v)", u.Scheme)
}

return nil
}
Loading

0 comments on commit 7010627

Please sign in to comment.