From 580a5c0be20bba55c6e1447f40464cc0f31d3b31 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Tue, 8 Aug 2017 18:27:20 -0400 Subject: [PATCH] Add support for temporal and permanent redirects --- controllers/nginx/pkg/template/template.go | 12 +- .../nginx/pkg/template/template_test.go | 10 +- .../rootfs/etc/nginx/template/nginx.tmpl | 15 ++- core/pkg/ingress/annotations/redirect/main.go | 117 ++++++++++++++++++ core/pkg/ingress/controller/annotations.go | 4 +- core/pkg/ingress/controller/util_test.go | 4 +- core/pkg/ingress/types.go | 8 +- core/pkg/ingress/types_equals.go | 3 + 8 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 core/pkg/ingress/annotations/redirect/main.go diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index 607fb6abc..306fd2ecc 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -194,7 +194,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) } @@ -287,7 +287,7 @@ 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 } @@ -295,9 +295,9 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string { 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 '' '' r; @@ -305,7 +305,7 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string { `, bPath, bPath) } - if location.Redirect.Target == slash { + if location.Rewrite.Target == slash { // special case redirect to / // ie /something to / return fmt.Sprintf(` @@ -318,7 +318,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, location.Backend, abu) + %v`, path, location.Rewrite.Target, proto, location.Backend, abu) } // default proxy_pass diff --git a/controllers/nginx/pkg/template/template_test.go b/controllers/nginx/pkg/template/template_test.go index ac8a1b5c7..ae9713afa 100644 --- a/controllers/nginx/pkg/template/template_test.go +++ b/controllers/nginx/pkg/template/template_test.go @@ -110,8 +110,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) @@ -124,9 +124,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) diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index a3953e38a..4ff9fa0d6 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -327,9 +327,15 @@ http { ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }}; {{ end }} - {{ if not (empty $location.Redirect.AppRoot)}} + {{ if not (empty $location.Redirect.URL) }} + location {{ $path }} { + return {{ $location.Redirect.Code }} {{ $location.Redirect.URL }}; + } + {{ else }} + + {{ if not (empty $location.Rewrite.AppRoot) }} if ($uri = /) { - return 302 {{ $location.Redirect.AppRoot }}; + return 302 {{ $location.Rewrite.AppRoot }}; } {{ end }} @@ -362,7 +368,7 @@ http { location {{ $path }} { set $proxy_upstream_name "{{ buildUpstreamName $server.Hostname $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; @@ -459,7 +465,7 @@ http { proxy_next_upstream {{ buildNextUpstream $location.Proxy.NextUpstream }}{{ if $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 }} @@ -473,6 +479,7 @@ http { {{ end }} } {{ end }} + {{ end }} {{ if eq $server.Hostname "_" }} # health checks in cloud providers require the use of port 80 diff --git a/core/pkg/ingress/annotations/redirect/main.go b/core/pkg/ingress/annotations/redirect/main.go new file mode 100644 index 000000000..6422a3427 --- /dev/null +++ b/core/pkg/ingress/annotations/redirect/main.go @@ -0,0 +1,117 @@ +/* +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" + + "github.com/pkg/errors" + extensions "k8s.io/api/extensions/v1beta1" + + "k8s.io/ingress/core/pkg/ingress/annotations/parser" +) + +const ( + permanent = "ingress.kubernetes.io/permanent-redirect" + temporal = "ingress.kubernetes.io/temporal-redirect" +) + +// Redirect returns the redirect configuration for an Ingress rule +type Redirect struct { + URL string `json:"url"` + Code int `json:"code"` +} + +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) { + tr, err := parser.GetStringAnnotation(temporal, ing) + if err != nil { + return nil, err + } + + if tr != "" { + if err := isValidURL(tr); err != nil { + return nil, err + } + + return &Redirect{ + URL: tr, + Code: http.StatusFound, + }, nil + } + + pr, err := parser.GetStringAnnotation(permanent, ing) + if err != nil { + return nil, err + } + + if pr != "" { + if err := isValidURL(pr); err != nil { + return nil, err + } + + return &Redirect{ + URL: pr, + Code: http.StatusMovedPermanently, + }, nil + } + + return nil, errors.New("ingress rule without redirect annotations") +} + +// 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 + } + + 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 http are valid protocols (%v)", u.Scheme) + } + + return nil +} diff --git a/core/pkg/ingress/controller/annotations.go b/core/pkg/ingress/controller/annotations.go index 7aadb9ddd..a607ae81c 100644 --- a/core/pkg/ingress/controller/annotations.go +++ b/core/pkg/ingress/controller/annotations.go @@ -29,6 +29,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/portinredirect" "k8s.io/ingress/core/pkg/ingress/annotations/proxy" "k8s.io/ingress/core/pkg/ingress/annotations/ratelimit" + "k8s.io/ingress/core/pkg/ingress/annotations/redirect" "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" "k8s.io/ingress/core/pkg/ingress/annotations/secureupstream" "k8s.io/ingress/core/pkg/ingress/annotations/serviceupstream" @@ -63,7 +64,8 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor { "UsePortInRedirects": portinredirect.NewParser(cfg), "Proxy": proxy.NewParser(cfg), "RateLimit": ratelimit.NewParser(), - "Redirect": rewrite.NewParser(cfg), + "Redirect": redirect.NewParser(), + "Rewrite": rewrite.NewParser(cfg), "SecureUpstream": secureupstream.NewParser(cfg), "ServiceUpstream": serviceupstream.NewParser(), "SessionAffinity": sessionaffinity.NewParser(), diff --git a/core/pkg/ingress/controller/util_test.go b/core/pkg/ingress/controller/util_test.go index 2aa6d5a20..75f1a25ed 100644 --- a/core/pkg/ingress/controller/util_test.go +++ b/core/pkg/ingress/controller/util_test.go @@ -27,6 +27,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist" "k8s.io/ingress/core/pkg/ingress/annotations/proxy" "k8s.io/ingress/core/pkg/ingress/annotations/ratelimit" + "k8s.io/ingress/core/pkg/ingress/annotations/redirect" "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" ) @@ -99,7 +100,8 @@ func TestMergeLocationAnnotations(t *testing.T) { "EnableCORS": true, "ExternalAuth": authreq.External{}, "RateLimit": ratelimit.RateLimit{}, - "Redirect": rewrite.Redirect{}, + "Redirect": redirect.Redirect{}, + "Rewrite": rewrite.Redirect{}, "Whitelist": ipwhitelist.SourceRange{}, "Proxy": proxy.Configuration{}, "CertificateAuth": authtls.AuthSSLConfig{}, diff --git a/core/pkg/ingress/types.go b/core/pkg/ingress/types.go index 28a30aa07..4e242011b 100644 --- a/core/pkg/ingress/types.go +++ b/core/pkg/ingress/types.go @@ -32,6 +32,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist" "k8s.io/ingress/core/pkg/ingress/annotations/proxy" "k8s.io/ingress/core/pkg/ingress/annotations/ratelimit" + "k8s.io/ingress/core/pkg/ingress/annotations/redirect" "k8s.io/ingress/core/pkg/ingress/annotations/rewrite" "k8s.io/ingress/core/pkg/ingress/defaults" "k8s.io/ingress/core/pkg/ingress/resolver" @@ -274,9 +275,12 @@ type Location struct { // The Redirect annotation precedes RateLimit // +optional RateLimit ratelimit.RateLimit `json:"rateLimit,omitempty"` - // Redirect describes the redirection this location. + // Redirect describes a temporal o permanent redirection this location. // +optional - Redirect rewrite.Redirect `json:"redirect,omitempty"` + Redirect redirect.Redirect `json:"redirect,omitempty"` + // Rewrite describes the redirection this location. + // +optional + Rewrite rewrite.Redirect `json:"rewrite,omitempty"` // Whitelist indicates only connections from certain client // addresses or networks are allowed. // +optional diff --git a/core/pkg/ingress/types_equals.go b/core/pkg/ingress/types_equals.go index c0c63d923..b892edd7a 100644 --- a/core/pkg/ingress/types_equals.go +++ b/core/pkg/ingress/types_equals.go @@ -362,6 +362,9 @@ func (l1 *Location) Equal(l2 *Location) bool { if !(&l1.Redirect).Equal(&l2.Redirect) { return false } + if !(&l1.Rewrite).Equal(&l2.Rewrite) { + return false + } if !(&l1.Whitelist).Equal(&l2.Whitelist) { return false }