diff --git a/controllers/nginx/configuration.md b/controllers/nginx/configuration.md index e38a2d02b..2e03fab63 100644 --- a/controllers/nginx/configuration.md +++ b/controllers/nginx/configuration.md @@ -65,7 +65,7 @@ The following annotations are supported: |[ingress.kubernetes.io/upstream-max-fails](#custom-nginx-upstream-checks)|number| |[ingress.kubernetes.io/upstream-fail-timeout](#custom-nginx-upstream-checks)|number| |[ingress.kubernetes.io/whitelist-source-range](#whitelist-source-range)|CIDR| - +|[ingress.kubernetes.io/server-alias](#server-alias)|string| #### Custom NGINX template @@ -156,7 +156,7 @@ Please check the [tls-auth](/examples/auth/client-certs/nginx/README.md) example ### Configuration snippet -Using this annotion you can add additional configuration to the NGINX location. For example: +Using this annotation you can add additional configuration to the NGINX location. For example: ``` ingress.kubernetes.io/configuration-snippet: | @@ -168,6 +168,13 @@ ingress.kubernetes.io/configuration-snippet: | To enable Cross-Origin Resource Sharing (CORS) in an Ingress rule add the annotation `ingress.kubernetes.io/enable-cors: "true"`. This will add a section in the server location enabling this functionality. For more information please check https://enable-cors.org/server_nginx.html +### Server Alias + +To add Server Aliases to an Ingress rule add the annotation `ingress.kubernetes.io/server-alias: ""`. +This will create a server with the same configuration, but a different server_name as the provided host. + +For more information please see http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name + ### External Authentication To use an existing service that provides authentication the Ingress rule can be annotated with `ingress.kubernetes.io/auth-url` to indicate the URL where the HTTP request should be sent. diff --git a/controllers/nginx/pkg/cmd/controller/nginx.go b/controllers/nginx/pkg/cmd/controller/nginx.go index 5bc5f360c..189921c14 100644 --- a/controllers/nginx/pkg/cmd/controller/nginx.go +++ b/controllers/nginx/pkg/cmd/controller/nginx.go @@ -446,6 +446,7 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error { IP: svc.Spec.ClusterIP, Port: port, ProxyProtocol: false, + }) } diff --git a/controllers/nginx/pkg/template/template.go b/controllers/nginx/pkg/template/template.go index 4e25d044c..1c2a175b8 100644 --- a/controllers/nginx/pkg/template/template.go +++ b/controllers/nginx/pkg/template/template.go @@ -147,6 +147,9 @@ var ( "toLower": strings.ToLower, "formatIP": formatIP, "buildNextUpstream": buildNextUpstream, + "serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} { + return struct { First, Second interface{} } { all, server } + }, } ) @@ -499,4 +502,4 @@ func buildNextUpstream(input interface{}) string { } return strings.Join(nextUpstreamCodes, " ") -} +} \ No newline at end of file diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index e8d8a8f27..98147223e 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -1,4 +1,5 @@ {{ $all := . }} +{{ $servers := .Servers }} {{ $cfg := .Cfg }} {{ $IsIPV6Enabled := .IsIPV6Enabled }} {{ $healthzURI := .HealthzURI }} @@ -266,7 +267,7 @@ http { {{ end }} {{/* build the maps that will be use to validate the Whitelist */}} - {{ range $index, $server := .Servers }} + {{ range $index, $server := $servers }} {{ range $location := $server.Locations }} {{ $path := buildLocation $location }} @@ -285,215 +286,24 @@ http { {{/* build all the required rate limit zones. Each annotation requires a dedicated zone */}} {{/* 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states */}} - {{ range $zone := (buildRateLimitZones $cfg.LimitConnZoneVariable .Servers) }} + {{ range $zone := (buildRateLimitZones $cfg.LimitConnZoneVariable $servers) }} {{ $zone }} {{ end }} {{ $backlogSize := .BacklogSize }} - {{ range $index, $server := .Servers }} + {{ range $index, $server := $servers }} server { server_name {{ $server.Hostname }}; - listen 80{{ if $cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{end}}; - {{ if $IsIPV6Enabled }}listen [::]:80{{ if $cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{ end }};{{ end }} - set $proxy_upstream_name "-"; - - {{/* Listen on 442 because port 443 is used in the TLS sni server */}} - {{/* This listener must always have proxy_protocol enabled, because the SNI listener forwards on source IP info in it. */}} - {{ if not (empty $server.SSLCertificate) }}listen 442 proxy_protocol{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{end}} ssl {{ if $cfg.UseHTTP2 }}http2{{ end }}; - {{ if $IsIPV6Enabled }}{{ if not (empty $server.SSLCertificate) }}listen [::]:442 proxy_protocol{{ end }} {{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $backlogSize }}{{end}} ssl {{ if $cfg.UseHTTP2 }}http2{{ end }};{{ end }} - {{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}} - # PEM sha: {{ $server.SSLPemChecksum }} - ssl_certificate {{ $server.SSLCertificate }}; - ssl_certificate_key {{ $server.SSLCertificate }}; - {{ end }} - - {{ if (and (not (empty $server.SSLCertificate)) $cfg.HSTS) }} - more_set_headers "Strict-Transport-Security: max-age={{ $cfg.HSTSMaxAge }}{{ if $cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }};{{ if $cfg.HSTSPreload }} preload{{ end }}"; - {{ end }} - - {{ if $cfg.EnableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end }} - - {{ range $location := $server.Locations }} - {{ $path := buildLocation $location }} - {{ $authPath := buildAuthLocation $location }} - - {{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }} - # PEM sha: {{ $location.CertificateAuth.AuthSSLCert.PemSHA }} - ssl_client_certificate {{ $location.CertificateAuth.AuthSSLCert.CAFileName }}; - ssl_verify_client on; - ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }}; - {{ end }} - - {{ if not (empty $location.Redirect.AppRoot)}} - if ($uri = /) { - return 302 {{ $location.Redirect.AppRoot }}; - } - {{ end }} - - {{ if not (empty $authPath) }} - location = {{ $authPath }} { - internal; - set $proxy_upstream_name "internal"; - - {{ if not $location.ExternalAuth.SendBody }} - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - {{ end }} - {{ if not (empty $location.ExternalAuth.Method) }} - proxy_method {{ $location.ExternalAuth.Method }}; - proxy_set_header X-Original-URI $request_uri; - proxy_set_header X-Scheme $pass_access_scheme; - {{ end }} - proxy_pass_request_headers on; - proxy_set_header Host {{ $location.ExternalAuth.Host }}; - proxy_ssl_server_name on; - - client_max_body_size "{{ $location.Proxy.BodySize }}"; - - set $target {{ $location.ExternalAuth.URL }}; - proxy_pass $target; - } - {{ end }} - - location {{ $path }} { - set $proxy_upstream_name "{{ buildUpstreamName $server.Hostname $backends $location }}"; - - {{ if (or $location.Redirect.ForceSSLRedirect (and (not (empty $server.SSLCertificate)) $location.Redirect.SSLRedirect)) }} - # enforce ssl on server side - if ($pass_access_scheme = http) { - return 301 https://$best_http_host$request_uri; - } - {{ end }} - - {{ if isLocationAllowed $location }} - {{ if gt (len $location.Whitelist.CIDR) 0 }} - if ({{ buildDenyVariable (print $server.Hostname "_" $path) }}) { - return 403; - } - {{ end }} - - port_in_redirect {{ if $location.UsePortInRedirects }}on{{ else }}off{{ end }}; - - {{ if not (empty $authPath) }} - # this location requires authentication - auth_request {{ $authPath }}; - auth_request_set $auth_cookie $upstream_http_set_cookie; - add_header Set-Cookie $auth_cookie; - {{- range $idx, $line := buildAuthResponseHeaders $location }} - {{ $line }} - {{- end }} - {{ end }} - - {{ if not (empty $location.ExternalAuth.SigninURL) }} - 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 }} - {{ $limit }}{{ end }} - - {{ if $location.BasicDigestAuth.Secured }} - {{ if eq $location.BasicDigestAuth.Type "basic" }} - auth_basic "{{ $location.BasicDigestAuth.Realm }}"; - auth_basic_user_file {{ $location.BasicDigestAuth.File }}; - {{ else }} - auth_digest "{{ $location.BasicDigestAuth.Realm }}"; - auth_digest_user_file {{ $location.BasicDigestAuth.File }}; - {{ end }} - proxy_set_header Authorization ""; - {{ end }} - - {{ if $location.EnableCORS }} - {{ template "CORS" }} - {{ end }} - - client_max_body_size "{{ $location.Proxy.BodySize }}"; - - proxy_set_header Host $best_http_host; - - # Pass the extracted client certificate to the backend - {{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }} - proxy_set_header ssl-client-cert $ssl_client_cert; - {{ end }} - - # Allow websocket connections - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $connection_upgrade; - - proxy_set_header X-Real-IP $the_real_ip; - proxy_set_header X-Forwarded-For $the_real_ip; - proxy_set_header X-Forwarded-Host $best_http_host; - proxy_set_header X-Forwarded-Port $pass_port; - proxy_set_header X-Forwarded-Proto $pass_access_scheme; - proxy_set_header X-Original-URI $request_uri; - proxy_set_header X-Scheme $pass_access_scheme; - - # mitigate HTTPoxy Vulnerability - # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ - proxy_set_header Proxy ""; - - # Custom headers to proxied server - {{ range $k, $v := $proxyHeaders }} - proxy_set_header {{ $k }} "{{ $v }}"; - {{ end }} - - proxy_connect_timeout {{ $location.Proxy.ConnectTimeout }}s; - proxy_send_timeout {{ $location.Proxy.SendTimeout }}s; - proxy_read_timeout {{ $location.Proxy.ReadTimeout }}s; - - proxy_redirect off; - proxy_buffering off; - proxy_buffer_size "{{ $location.Proxy.BufferSize }}"; - proxy_buffers 4 "{{ $location.Proxy.BufferSize }}"; - - proxy_http_version 1.1; - - proxy_cookie_domain {{ $location.Proxy.CookieDomain }}; - proxy_cookie_path {{ $location.Proxy.CookiePath }}; - - # In case of errors try the next upstream server before returning an error - 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 }} - proxy_set_header Accept-Encoding ""; - {{ end }} - - {{/* Add any additional configuration defined */}} - {{ $location.ConfigurationSnippet }} - - {{ buildProxyPass $server.Hostname $backends $location }} - {{ else }} - #{{ $location.Denied }} - return 503; - {{ end }} - } - {{ end }} - - {{ if eq $server.Hostname "_" }} - # health checks in cloud providers require the use of port 80 - location {{ $healthzURI }} { - access_log off; - return 200; - } - - # this is required to avoid error if nginx is being monitored - # with an external software (like sysdig) - location /nginx_status { - allow 127.0.0.1; - {{ if $IsIPV6Enabled }}allow ::1;{{ end }} - deny all; - - access_log off; - stub_status on; - } - {{ end }} - + {{ template "SERVER" serverConfig $all $server }} {{ template "CUSTOM_ERRORS" $all }} } - + {{ if $server.Alias }} + server { + server_name {{ $server.Alias }}; + {{ template "SERVER" serverConfig $all $server }} + {{ template "CUSTOM_ERRORS" $all }} + } + {{ end }} {{ end }} # default server, used for NGINX healthcheck and access to nginx stats @@ -664,3 +474,209 @@ stream { add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; } {{ end }} + +{{/* definition of server-template to avoid repetitions with server-alias */}} +{{ define "SERVER" }} + {{ $all := .First }} + {{ $server := .Second }} + listen 80{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $all.BacklogSize }}{{end}}; + {{ if $all.IsIPV6Enabled }}listen [::]:80{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $all.BacklogSize }}{{ end }};{{ end }} + set $proxy_upstream_name "-"; + + {{/* Listen on 442 because port 443 is used in the TLS sni server */}} + {{/* This listener must always have proxy_protocol enabled, because the SNI listener forwards on source IP info in it. */}} + {{ if not (empty $server.SSLCertificate) }}listen 442 proxy_protocol{{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }}; + {{ if $all.IsIPV6Enabled }}{{ if not (empty $server.SSLCertificate) }}listen [::]:442 proxy_protocol{{ end }} {{ if eq $server.Hostname "_"}} default_server reuseport backlog={{ $all.BacklogSize }}{{end}} ssl {{ if $all.Cfg.UseHTTP2 }}http2{{ end }};{{ end }} + {{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}} + # PEM sha: {{ $server.SSLPemChecksum }} + ssl_certificate {{ $server.SSLCertificate }}; + ssl_certificate_key {{ $server.SSLCertificate }}; + {{ end }} + + {{ if (and (not (empty $server.SSLCertificate)) $all.Cfg.HSTS) }} + more_set_headers "Strict-Transport-Security: max-age={{ $all.Cfg.HSTSMaxAge }}{{ if $all.Cfg.HSTSIncludeSubdomains }}; includeSubDomains{{ end }};{{ if $all.Cfg.HSTSPreload }} preload{{ end }}"; + {{ end }} + + {{ if $all.Cfg.EnableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end }} + + {{ range $location := $server.Locations }} + {{ $path := buildLocation $location }} + {{ $authPath := buildAuthLocation $location }} + + {{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }} + # PEM sha: {{ $location.CertificateAuth.AuthSSLCert.PemSHA }} + ssl_client_certificate {{ $location.CertificateAuth.AuthSSLCert.CAFileName }}; + ssl_verify_client on; + ssl_verify_depth {{ $location.CertificateAuth.ValidationDepth }}; + {{ end }} + + {{ if not (empty $location.Redirect.AppRoot)}} + if ($uri = /) { + return 302 {{ $location.Redirect.AppRoot }}; + } + {{ end }} + + {{ if not (empty $authPath) }} + location = {{ $authPath }} { + internal; + set $proxy_upstream_name "internal"; + + {{ if not $location.ExternalAuth.SendBody }} + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + {{ end }} + {{ if not (empty $location.ExternalAuth.Method) }} + proxy_method {{ $location.ExternalAuth.Method }}; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Scheme $pass_access_scheme; + {{ end }} + proxy_pass_request_headers on; + proxy_set_header Host {{ $location.ExternalAuth.Host }}; + proxy_ssl_server_name on; + + client_max_body_size "{{ $location.Proxy.BodySize }}"; + + + set $target {{ $location.ExternalAuth.URL }}; + proxy_pass $target; + } + {{ end }} + + 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)) }} + # enforce ssl on server side + if ($pass_access_scheme = http) { + return 301 https://$best_http_host$request_uri; + } + {{ end }} + + {{ if isLocationAllowed $location }} + {{ if gt (len $location.Whitelist.CIDR) 0 }} + if ({{ buildDenyVariable (print .Hostname "_" $path) }}) { + return 403; + } + {{ end }} + + port_in_redirect {{ if $location.UsePortInRedirects }}on{{ else }}off{{ end }}; + + {{ if not (empty $authPath) }} + # this location requires authentication + auth_request {{ $authPath }}; + auth_request_set $auth_cookie $upstream_http_set_cookie; + add_header Set-Cookie $auth_cookie; + {{- range $idx, $line := buildAuthResponseHeaders $location }} + {{ $line }} + {{- end }} + {{ end }} + + {{ if not (empty $location.ExternalAuth.SigninURL) }} + 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 }} + {{ $limit }}{{ end }} + + {{ if $location.BasicDigestAuth.Secured }} + {{ if eq $location.BasicDigestAuth.Type "basic" }} + auth_basic "{{ $location.BasicDigestAuth.Realm }}"; + auth_basic_user_file {{ $location.BasicDigestAuth.File }}; + {{ else }} + auth_digest "{{ $location.BasicDigestAuth.Realm }}"; + auth_digest_user_file {{ $location.BasicDigestAuth.File }}; + {{ end }} + proxy_set_header Authorization ""; + {{ end }} + + {{ if $location.EnableCORS }} + {{ template "CORS" }} + {{ end }} + + client_max_body_size "{{ $location.Proxy.BodySize }}"; + + proxy_set_header Host $best_http_host; + + # Pass the extracted client certificate to the backend + {{ if not (empty $location.CertificateAuth.AuthSSLCert.CAFileName) }} + proxy_set_header ssl-client-cert $ssl_client_cert; + {{ end }} + + # Allow websocket connections + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + + proxy_set_header X-Real-IP $the_real_ip; + proxy_set_header X-Forwarded-For $the_real_ip; + proxy_set_header X-Forwarded-Host $best_http_host; + proxy_set_header X-Forwarded-Port $pass_port; + proxy_set_header X-Forwarded-Proto $pass_access_scheme; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Scheme $pass_access_scheme; + + # mitigate HTTPoxy Vulnerability + # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ + proxy_set_header Proxy ""; + + # Custom headers to proxied server + {{ range $k, $v := $all.ProxySetHeaders }} + proxy_set_header {{ $k }} "{{ $v }}"; + {{ end }} + + proxy_connect_timeout {{ $location.Proxy.ConnectTimeout }}s; + proxy_send_timeout {{ $location.Proxy.SendTimeout }}s; + proxy_read_timeout {{ $location.Proxy.ReadTimeout }}s; + + proxy_redirect off; + proxy_buffering off; + proxy_buffer_size "{{ $location.Proxy.BufferSize }}"; + proxy_buffers 4 "{{ $location.Proxy.BufferSize }}"; + + proxy_http_version 1.1; + + proxy_cookie_domain {{ $location.Proxy.CookieDomain }}; + proxy_cookie_path {{ $location.Proxy.CookiePath }}; + + # In case of errors try the next upstream server before returning an error + 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 }} + proxy_set_header Accept-Encoding ""; + {{ end }} + + {{/* Add any additional configuration defined */}} + {{ $location.ConfigurationSnippet }} + + {{ buildProxyPass $server.Hostname $all.Backends $location }} + {{ else }} + #{{ $location.Denied }} + return 503; + {{ end }} + } + {{ end }} + + {{ if eq $server.Hostname "_" }} + # health checks in cloud providers require the use of port 80 + location {{ $all.HealthzURI }} { + access_log off; + return 200; + } + + # this is required to avoid error if nginx is being monitored + # with an external software (like sysdig) + location /nginx_status { + allow 127.0.0.1; + {{ if $all.IsIPV6Enabled }}allow ::1;{{ end }} + deny all; + + access_log off; + stub_status on; + } + + {{ end }} + +{{ end }} diff --git a/core/pkg/ingress/annotations/alias/main.go b/core/pkg/ingress/annotations/alias/main.go new file mode 100644 index 000000000..ddfbab208 --- /dev/null +++ b/core/pkg/ingress/annotations/alias/main.go @@ -0,0 +1,41 @@ +/* +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 alias + +import ( + extensions "k8s.io/api/extensions/v1beta1" + + "k8s.io/ingress/core/pkg/ingress/annotations/parser" +) + +const ( + annotation = "ingress.kubernetes.io/server-alias" +) + +type alias struct { +} + +// NewParser creates a new Alias annotation parser +func NewParser() parser.IngressAnnotation { + return alias{} +} + +// Parse parses the annotations contained in the ingress rule +// used to add an alias to the provided hosts +func (a alias) Parse(ing *extensions.Ingress) (interface{}, error) { + return parser.GetStringAnnotation(annotation, ing) +} \ No newline at end of file diff --git a/core/pkg/ingress/annotations/alias/main_test.go b/core/pkg/ingress/annotations/alias/main_test.go new file mode 100644 index 000000000..de4fe17f5 --- /dev/null +++ b/core/pkg/ingress/annotations/alias/main_test.go @@ -0,0 +1,60 @@ +/* +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 alias + +import ( + "testing" + + api "k8s.io/api/core/v1" + extensions "k8s.io/api/extensions/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestParse(t *testing.T) { + ap := NewParser() + if ap == nil { + t.Fatalf("expected a parser.IngressAnnotation but returned nil") + } + + testCases := []struct { + annotations map[string]string + expected string + }{ + {map[string]string{annotation: "www.example.com"}, "www.example.com"}, + {map[string]string{annotation: "*.example.com www.example.*"}, "*.example.com www.example.*"}, + {map[string]string{annotation: `~^www\d+\.example\.com$`}, `~^www\d+\.example\.com$`}, + {map[string]string{annotation: ""}, ""}, + {map[string]string{}, ""}, + {nil, ""}, + } + + 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/core/pkg/ingress/controller/annotations.go b/core/pkg/ingress/controller/annotations.go index ee7a08757..01f1cab8b 100644 --- a/core/pkg/ingress/controller/annotations.go +++ b/core/pkg/ingress/controller/annotations.go @@ -37,6 +37,7 @@ import ( "k8s.io/ingress/core/pkg/ingress/annotations/sslpassthrough" "k8s.io/ingress/core/pkg/ingress/errors" "k8s.io/ingress/core/pkg/ingress/resolver" + "k8s.io/ingress/core/pkg/ingress/annotations/alias" ) type extractorConfig interface { @@ -69,6 +70,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor { "SessionAffinity": sessionaffinity.NewParser(), "SSLPassthrough": sslpassthrough.NewParser(), "ConfigurationSnippet": snippet.NewParser(), + "Alias": alias.NewParser(), }, } } @@ -107,6 +109,7 @@ const ( sslPassthrough = "SSLPassthrough" sessionAffinity = "SessionAffinity" serviceUpstream = "ServiceUpstream" + serverAlias = "Alias" ) func (e *annotationExtractor) ServiceUpstream(ing *extensions.Ingress) bool { @@ -133,6 +136,11 @@ func (e *annotationExtractor) SSLPassthrough(ing *extensions.Ingress) bool { return val.(bool) } +func (e *annotationExtractor) Alias(ing *extensions.Ingress) string { + val, _ := e.annotations[serverAlias].Parse(ing) + return val.(string) +} + func (e *annotationExtractor) SessionAffinity(ing *extensions.Ingress) *sessionaffinity.AffinityConfig { val, _ := e.annotations[sessionAffinity].Parse(ing) return val.(*sessionaffinity.AffinityConfig) diff --git a/core/pkg/ingress/controller/controller.go b/core/pkg/ingress/controller/controller.go index 0df14d35c..bac29f23f 100644 --- a/core/pkg/ingress/controller/controller.go +++ b/core/pkg/ingress/controller/controller.go @@ -610,6 +610,16 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress upstreams := ic.createUpstreams(ings) servers := ic.createServers(ings, upstreams) + // If a server has a hostname equivalent to a pre-existing alias, then we remove the alias + for _, server := range servers { + for j, alias := range servers { + if server.Hostname == alias.Alias { + glog.Warningf("There is a conflict with hostname '%v' and alias of `%v`.", server.Hostname, alias.Hostname) + servers[j].Alias = "" + } + } + } + for _, ingIf := range ings { ing := ingIf.(*extensions.Ingress) @@ -1066,19 +1076,25 @@ func (ic *GenericController) createServers(data []interface{}, } } - // configure default location and SSL + // configure default location, alias, and SSL for _, ingIf := range data { ing := ingIf.(*extensions.Ingress) if !class.IsValid(ing, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass) { continue } + // setup server-alias based on annotations + aliasAnnotation := ic.annotations.Alias(ing) + for _, rule := range ing.Spec.Rules { host := rule.Host if host == "" { host = defServerName } + // setup server aliases + servers[host].Alias = aliasAnnotation + // only add a certificate if the server does not have one previously configured if len(ing.Spec.TLS) == 0 || servers[host].SSLCertificate != "" { continue diff --git a/core/pkg/ingress/types.go b/core/pkg/ingress/types.go index 5d17fe0d6..00f5afa21 100644 --- a/core/pkg/ingress/types.go +++ b/core/pkg/ingress/types.go @@ -220,6 +220,8 @@ type Server struct { SSLPemChecksum string `json:"sslPemChecksum"` // Locations list of URIs configured in the server. Locations []*Location `json:"locations,omitempty"` + // return the alias of the server name + Alias string `json:"alias,omitempty"` } // Location describes an URI inside a server.