Add external authentication using auth_request
This commit is contained in:
parent
25bf00a1fc
commit
541928e27d
13 changed files with 537 additions and 40 deletions
|
@ -37,17 +37,18 @@ The following annotations are supported:
|
|||
|
||||
|Name |type|
|
||||
|---------------------------|------|
|
||||
|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI|
|
||||
|[ingress.kubernetes.io/add-base-url](#rewrite)|true or false|
|
||||
|[ingress.kubernetes.io/auth-realm](#authentication)|string|
|
||||
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|
||||
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|
||||
|[ingress.kubernetes.io/auth-url](#external-authentication)|string|
|
||||
|[ingress.kubernetes.io/limit-connections](#rate-limiting)|number|
|
||||
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
|
||||
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|
||||
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|
||||
|[ingress.kubernetes.io/auth-realm](#authentication)|string|
|
||||
|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI|
|
||||
|[ingress.kubernetes.io/secure-backends](#secure-backends)|true or false|
|
||||
|[ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false|
|
||||
|[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/secure-backends](#secure-backends)|true or false|
|
||||
|[ingress.kubernetes.io/whitelist-source-range](#whitelist-source-range)|CIDR|
|
||||
|
||||
|
||||
|
@ -119,6 +120,18 @@ ingress.kubernetes.io/auth-realm:"realm string"
|
|||
Please check the [auth](examples/auth/README.md) example
|
||||
|
||||
|
||||
### 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.
|
||||
Additionally is possible to set `ingress.kubernetes.io/auth-method` to specify the HTTP method to use (GET or POST) and `ingress.kubernetes.io/auth-send-body` to true or false (default).
|
||||
|
||||
```
|
||||
ingress.kubernetes.io/auth-url:"URL to the authentication service"
|
||||
```
|
||||
|
||||
Please check the [external-auth](examples/external-auth/README.md) example
|
||||
|
||||
|
||||
### Rewrite
|
||||
|
||||
In some scenarios the exposed URL in the backend service differs from the specified path in the Ingress rule. Without a rewrite any request will return 404.
|
||||
|
|
|
@ -42,6 +42,7 @@ import (
|
|||
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/authreq"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/cors"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/healthcheck"
|
||||
|
@ -723,6 +724,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
|
|||
glog.V(3).Infof("error reading CORS annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
|
||||
}
|
||||
|
||||
ra, err := authreq.ParseAnnotations(ing)
|
||||
glog.V(3).Infof("nginx auth request %v", ra)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("error reading auth request annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
|
||||
}
|
||||
|
||||
host := rule.Host
|
||||
if host == "" {
|
||||
host = defServerName
|
||||
|
@ -756,6 +763,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
|
|||
loc.SecureUpstream = secUpstream
|
||||
loc.Whitelist = *wl
|
||||
loc.EnableCORS = eCORS
|
||||
loc.ExternalAuthURL = ra
|
||||
|
||||
addLoc = false
|
||||
continue
|
||||
|
@ -771,14 +779,15 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
|
|||
|
||||
if addLoc {
|
||||
server.Locations = append(server.Locations, &ingress.Location{
|
||||
Path: nginxPath,
|
||||
Upstream: *ups,
|
||||
Auth: *nginxAuth,
|
||||
RateLimit: *rl,
|
||||
Redirect: *locRew,
|
||||
SecureUpstream: secUpstream,
|
||||
Whitelist: *wl,
|
||||
EnableCORS: eCORS,
|
||||
Path: nginxPath,
|
||||
Upstream: *ups,
|
||||
Auth: *nginxAuth,
|
||||
RateLimit: *rl,
|
||||
Redirect: *locRew,
|
||||
SecureUpstream: secUpstream,
|
||||
Whitelist: *wl,
|
||||
EnableCORS: eCORS,
|
||||
ExternalAuthURL: ra,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
148
controllers/nginx/examples/external-auth/README.md
Normal file
148
controllers/nginx/examples/external-auth/README.md
Normal file
|
@ -0,0 +1,148 @@
|
|||
# External authentication
|
||||
|
||||
### Example 1:
|
||||
|
||||
Use an external service (Basic Auth) located in `https://httpbin.org`
|
||||
|
||||
```
|
||||
$ kubectl create -f ingress.yaml
|
||||
ingress "external-auth" created
|
||||
$ kubectl get ing external-auth
|
||||
NAME HOSTS ADDRESS PORTS AGE
|
||||
external-auth external-auth-01.sample.com 172.17.4.99 80 13s
|
||||
$ kubectl get ing external-auth -o yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
ingress.kubernetes.io/auth-url: https://httpbin.org/basic-auth/user/passwd
|
||||
creationTimestamp: 2016-10-03T13:50:35Z
|
||||
generation: 1
|
||||
name: external-auth
|
||||
namespace: default
|
||||
resourceVersion: "2068378"
|
||||
selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/external-auth
|
||||
uid: 5c388f1d-8970-11e6-9004-080027d2dc94
|
||||
spec:
|
||||
rules:
|
||||
- host: external-auth-01.sample.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: echoheaders
|
||||
servicePort: 80
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 172.17.4.99
|
||||
$
|
||||
```
|
||||
|
||||
Test 1: no username/password (expect code 401)
|
||||
```
|
||||
$ curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com'
|
||||
* Rebuilt URL to: http://172.17.4.99/
|
||||
* Trying 172.17.4.99...
|
||||
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0)
|
||||
> GET / HTTP/1.1
|
||||
> Host: external-auth-01.sample.com
|
||||
> User-Agent: curl/7.50.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 401 Unauthorized
|
||||
< Server: nginx/1.11.3
|
||||
< Date: Mon, 03 Oct 2016 14:52:08 GMT
|
||||
< Content-Type: text/html
|
||||
< Content-Length: 195
|
||||
< Connection: keep-alive
|
||||
< WWW-Authenticate: Basic realm="Fake Realm"
|
||||
<
|
||||
<html>
|
||||
<head><title>401 Authorization Required</title></head>
|
||||
<body bgcolor="white">
|
||||
<center><h1>401 Authorization Required</h1></center>
|
||||
<hr><center>nginx/1.11.3</center>
|
||||
</body>
|
||||
</html>
|
||||
* Connection #0 to host 172.17.4.99 left intact
|
||||
```
|
||||
|
||||
Test 2: valid username/password (expect code 200)
|
||||
```
|
||||
$ curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com' -u 'user:passwd'
|
||||
* Rebuilt URL to: http://172.17.4.99/
|
||||
* Trying 172.17.4.99...
|
||||
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0)
|
||||
* Server auth using Basic with user 'user'
|
||||
> GET / HTTP/1.1
|
||||
> Host: external-auth-01.sample.com
|
||||
> Authorization: Basic dXNlcjpwYXNzd2Q=
|
||||
> User-Agent: curl/7.50.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 200 OK
|
||||
< Server: nginx/1.11.3
|
||||
< Date: Mon, 03 Oct 2016 14:52:50 GMT
|
||||
< Content-Type: text/plain
|
||||
< Transfer-Encoding: chunked
|
||||
< Connection: keep-alive
|
||||
<
|
||||
CLIENT VALUES:
|
||||
client_address=10.2.60.2
|
||||
command=GET
|
||||
real path=/
|
||||
query=nil
|
||||
request_version=1.1
|
||||
request_uri=http://external-auth-01.sample.com:8080/
|
||||
|
||||
SERVER VALUES:
|
||||
server_version=nginx: 1.9.11 - lua: 10001
|
||||
|
||||
HEADERS RECEIVED:
|
||||
accept=*/*
|
||||
authorization=Basic dXNlcjpwYXNzd2Q=
|
||||
connection=close
|
||||
host=external-auth-01.sample.com
|
||||
user-agent=curl/7.50.1
|
||||
x-forwarded-for=10.2.60.1
|
||||
x-forwarded-host=external-auth-01.sample.com
|
||||
x-forwarded-port=80
|
||||
x-forwarded-proto=http
|
||||
x-real-ip=10.2.60.1
|
||||
BODY:
|
||||
* Connection #0 to host 172.17.4.99 left intact
|
||||
-no body in request-
|
||||
```
|
||||
|
||||
Test 3: invalid username/password (expect code 401)
|
||||
```
|
||||
curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com' -u 'user:user'
|
||||
* Rebuilt URL to: http://172.17.4.99/
|
||||
* Trying 172.17.4.99...
|
||||
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0)
|
||||
* Server auth using Basic with user 'user'
|
||||
> GET / HTTP/1.1
|
||||
> Host: external-auth-01.sample.com
|
||||
> Authorization: Basic dXNlcjp1c2Vy
|
||||
> User-Agent: curl/7.50.1
|
||||
> Accept: */*
|
||||
>
|
||||
< HTTP/1.1 401 Unauthorized
|
||||
< Server: nginx/1.11.3
|
||||
< Date: Mon, 03 Oct 2016 14:53:04 GMT
|
||||
< Content-Type: text/html
|
||||
< Content-Length: 195
|
||||
< Connection: keep-alive
|
||||
* Authentication problem. Ignoring this.
|
||||
< WWW-Authenticate: Basic realm="Fake Realm"
|
||||
<
|
||||
<html>
|
||||
<head><title>401 Authorization Required</title></head>
|
||||
<body bgcolor="white">
|
||||
<center><h1>401 Authorization Required</h1></center>
|
||||
<hr><center>nginx/1.11.3</center>
|
||||
</body>
|
||||
</html>
|
||||
* Connection #0 to host 172.17.4.99 left intact
|
||||
```
|
15
controllers/nginx/examples/external-auth/ingress.yaml
Normal file
15
controllers/nginx/examples/external-auth/ingress.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
ingress.kubernetes.io/auth-url: "https://httpbin.org/basic-auth/user/passwd"
|
||||
name: external-auth
|
||||
spec:
|
||||
rules:
|
||||
- host: external-auth-01.sample.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: echoheaders
|
||||
servicePort: 80
|
||||
path: /
|
|
@ -95,14 +95,16 @@ func main() {
|
|||
glog.Fatalf("Please specify --default-backend-service")
|
||||
}
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
kubeClient, err := unversioned.NewInCluster()
|
||||
if err != nil {
|
||||
glog.Fatalf("error connecting to the client: %v", err)
|
||||
}
|
||||
kubeClient, err := unversioned.New(config)
|
||||
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to create client: %v", err)
|
||||
config, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
glog.Fatalf("error configuring the client: %v", err)
|
||||
}
|
||||
kubeClient, err = unversioned.New(config)
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
runtimePodInfo, err := getPodDetails(kubeClient)
|
||||
|
|
|
@ -90,6 +90,7 @@ http {
|
|||
|
||||
{{ if not (empty .defResolver) }}# Custom dns resolver.
|
||||
resolver {{ .defResolver }} valid=30s;
|
||||
resolver_timeout 10s;
|
||||
{{ end }}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
|
@ -183,27 +184,47 @@ http {
|
|||
server {
|
||||
server_name {{ $server.Name }};
|
||||
listen 80{{ if $cfg.useProxyProtocol }} proxy_protocol{{ end }};
|
||||
{{ if $server.SSL }}listen 443 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.enableSpdy }}spdy{{ end }} {{ if $cfg.useHttp2 }}http2{{ end }};
|
||||
{{- if $server.SSL }}listen 443 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.enableSpdy }}spdy{{ end }} {{ if $cfg.useHttp2 }}http2{{ 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.SSLCertificateKey }};
|
||||
{{- end }}
|
||||
|
||||
{{ if (and $server.SSL $cfg.hsts) -}}
|
||||
{{- if (and $server.SSL $cfg.hsts) }}
|
||||
more_set_headers "Strict-Transport-Security: max-age={{ $cfg.hstsMaxAge }}{{ if $cfg.hstsIncludeSubdomains }}; includeSubDomains{{ end }}; preload";
|
||||
{{- end }}
|
||||
|
||||
{{ if $cfg.enableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ 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 $authPath) }}
|
||||
location = {{ $authPath }} {
|
||||
internal;
|
||||
{{ if not $location.ExternalAuthURL.SendBody }}
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
{{ end -}}
|
||||
{{ if not (empty $location.ExternalAuthURL.Method) }}
|
||||
proxy_method {{ $location.ExternalAuthURL.Method }};
|
||||
{{ end -}}
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass_request_headers on;
|
||||
proxy_pass {{ $location.ExternalAuthURL.URL }};
|
||||
}
|
||||
{{ end }}
|
||||
location {{ $path }} {
|
||||
{{ if gt (len $location.Whitelist.CIDR) 0 }}
|
||||
{{- range $ip := $location.Whitelist.CIDR }}
|
||||
allow {{ $ip }};{{ end }}
|
||||
deny all;
|
||||
{{ end -}}
|
||||
{{ if not (empty $authPath) }}
|
||||
# this location requires authentication
|
||||
auth_request {{ $authPath }};
|
||||
{{ end }}
|
||||
|
||||
{{ if (and $server.SSL $location.Redirect.SSLRedirect) -}}
|
||||
# enforce ssl on server side
|
||||
|
@ -272,7 +293,7 @@ http {
|
|||
|
||||
{{ if eq $server.Name "_" }}
|
||||
# health checks in cloud providers require the use of port 80
|
||||
location {{ $cfg.healthzUrl }} {
|
||||
location {{ $cfg.HealthzURL }} {
|
||||
access_log off;
|
||||
return 200;
|
||||
}
|
||||
|
|
138
controllers/nginx/nginx/authreq/main.go
Normal file
138
controllers/nginx/nginx/authreq/main.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2015 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 authreq
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
)
|
||||
|
||||
const (
|
||||
// external URL that provides the authentication
|
||||
authURL = "ingress.kubernetes.io/auth-url"
|
||||
authMethod = "ingress.kubernetes.io/auth-method"
|
||||
authBody = "ingress.kubernetes.io/auth-send-body"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingAnnotations is returned when the ingress rule
|
||||
// does not contain annotations related with authentication
|
||||
ErrMissingAnnotations = errors.New("missing authentication annotations")
|
||||
)
|
||||
|
||||
// Auth returns external authentication configuration for an Ingress rule
|
||||
type Auth struct {
|
||||
URL string
|
||||
Method string
|
||||
SendBody bool
|
||||
}
|
||||
|
||||
type ingAnnotations map[string]string
|
||||
|
||||
func (a ingAnnotations) url() (string, error) {
|
||||
val, ok := a[authURL]
|
||||
if !ok {
|
||||
return "", ErrMissingAnnotations
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (a ingAnnotations) method() string {
|
||||
val, ok := a[authMethod]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func (a ingAnnotations) sendBody() bool {
|
||||
val, ok := a[authBody]
|
||||
if ok {
|
||||
if b, err := strconv.ParseBool(val); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
|
||||
)
|
||||
|
||||
func validMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to use an external URL as source for authentication
|
||||
func ParseAnnotations(ing *extensions.Ingress) (Auth, error) {
|
||||
if ing.GetAnnotations() == nil {
|
||||
return Auth{}, ErrMissingAnnotations
|
||||
}
|
||||
|
||||
str, err := ingAnnotations(ing.GetAnnotations()).url()
|
||||
if err != nil {
|
||||
return Auth{}, err
|
||||
}
|
||||
if str == "" {
|
||||
return Auth{}, fmt.Errorf("an empty string is not a valid URL")
|
||||
}
|
||||
|
||||
ur, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return Auth{}, err
|
||||
}
|
||||
if ur.Scheme == "" {
|
||||
return Auth{}, fmt.Errorf("url scheme is empty")
|
||||
}
|
||||
if ur.Host == "" {
|
||||
return Auth{}, fmt.Errorf("url host is empty")
|
||||
}
|
||||
|
||||
if strings.Index(ur.Host, "..") != -1 {
|
||||
return Auth{}, fmt.Errorf("invalid url host")
|
||||
}
|
||||
|
||||
m := ingAnnotations(ing.GetAnnotations()).method()
|
||||
if len(m) != 0 && !validMethod(m) {
|
||||
return Auth{}, fmt.Errorf("invalid HTTP method")
|
||||
}
|
||||
|
||||
sb := ingAnnotations(ing.GetAnnotations()).sendBody()
|
||||
|
||||
return Auth{
|
||||
URL: str,
|
||||
Method: m,
|
||||
SendBody: sb,
|
||||
}, nil
|
||||
}
|
114
controllers/nginx/nginx/authreq/main_test.go
Normal file
114
controllers/nginx/nginx/authreq/main_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
Copyright 2015 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 authreq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/extensions"
|
||||
"k8s.io/kubernetes/pkg/util/intstr"
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
_, err := ingAnnotations(ing.GetAnnotations()).url()
|
||||
if err == nil {
|
||||
t.Error("Expected a validation error")
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
tests := []struct {
|
||||
title string
|
||||
url string
|
||||
method string
|
||||
sendBody bool
|
||||
expErr bool
|
||||
}{
|
||||
{"empty", "", "", false, true},
|
||||
{"no scheme", "bar", "", false, true},
|
||||
{"invalid host", "http://", "", false, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "", false, true},
|
||||
{"valid URL", "http://bar.foo.com/external-auth", "", false, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "POST", true, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "GET", true, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
data[authURL] = test.url
|
||||
data[authBody] = fmt.Sprintf("%v", test.sendBody)
|
||||
data[authMethod] = fmt.Sprintf("%v", test.method)
|
||||
|
||||
u, err := ParseAnnotations(ing)
|
||||
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if u.URL != test.url {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL)
|
||||
}
|
||||
if u.Method != test.method {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
|
||||
}
|
||||
if u.SendBody != test.sendBody {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.sendBody, u.SendBody)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ package ingress
|
|||
|
||||
import (
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/authreq"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/ipwhitelist"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
|
||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
||||
|
@ -93,15 +94,16 @@ func (c ServerByName) Less(i, j int) bool {
|
|||
|
||||
// Location describes an NGINX location
|
||||
type Location struct {
|
||||
Path string
|
||||
IsDefBackend bool
|
||||
Upstream Upstream
|
||||
Auth auth.Nginx
|
||||
RateLimit ratelimit.RateLimit
|
||||
Redirect rewrite.Redirect
|
||||
SecureUpstream bool
|
||||
Whitelist ipwhitelist.SourceRange
|
||||
EnableCORS bool
|
||||
Path string
|
||||
IsDefBackend bool
|
||||
Upstream Upstream
|
||||
Auth auth.Nginx
|
||||
RateLimit ratelimit.RateLimit
|
||||
Redirect rewrite.Redirect
|
||||
SecureUpstream bool
|
||||
Whitelist ipwhitelist.SourceRange
|
||||
EnableCORS bool
|
||||
ExternalAuthURL authreq.Auth
|
||||
}
|
||||
|
||||
// LocationByPath sorts location by path
|
||||
|
|
|
@ -71,17 +71,21 @@ func NewManager(kubeClient *client.Client) *Manager {
|
|||
ngx := &Manager{
|
||||
ConfigFile: "/etc/nginx/nginx.conf",
|
||||
defCfg: config.NewDefault(),
|
||||
defResolver: strings.Join(getDNSServers(), " "),
|
||||
reloadLock: &sync.Mutex{},
|
||||
reloadRateLimiter: flowcontrol.NewTokenBucketRateLimiter(0.1, 1),
|
||||
}
|
||||
|
||||
res, err := getDNSServers()
|
||||
if err != nil {
|
||||
glog.Warningf("error reading nameservers: %v", err)
|
||||
}
|
||||
ngx.defResolver = strings.Join(res, " ")
|
||||
|
||||
ngx.createCertsDir(config.SSLDirectory)
|
||||
|
||||
ngx.sslDHParam = ngx.SearchDHParamFile(config.SSLDirectory)
|
||||
|
||||
var onChange func()
|
||||
|
||||
onChange = func() {
|
||||
template, err := ngx_template.NewTemplate(tmplPath, onChange)
|
||||
if err != nil {
|
||||
|
|
|
@ -18,6 +18,7 @@ package template
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
@ -49,6 +50,7 @@ var (
|
|||
return true
|
||||
},
|
||||
"buildLocation": buildLocation,
|
||||
"buildAuthLocation": buildAuthLocation,
|
||||
"buildProxyPass": buildProxyPass,
|
||||
"buildRateLimitZones": buildRateLimitZones,
|
||||
"buildRateLimit": buildRateLimit,
|
||||
|
@ -193,6 +195,22 @@ func buildLocation(input interface{}) string {
|
|||
return path
|
||||
}
|
||||
|
||||
func buildAuthLocation(input interface{}) string {
|
||||
location, ok := input.(*ingress.Location)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
if location.ExternalAuthURL.URL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
str := base64.URLEncoding.EncodeToString([]byte(location.Path))
|
||||
// avoid locations containing the = char
|
||||
str = strings.Replace(str, "=", "", -1)
|
||||
return fmt.Sprintf("/_external-auth-%v", str)
|
||||
}
|
||||
|
||||
// buildProxyPass produces the proxy pass string, if the ingress has redirects
|
||||
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
|
||||
// If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will
|
||||
|
|
|
@ -38,15 +38,14 @@ const (
|
|||
)
|
||||
|
||||
// getDNSServers returns the list of nameservers located in the file /etc/resolv.conf
|
||||
func getDNSServers() []string {
|
||||
func getDNSServers() ([]string, error) {
|
||||
var nameservers []string
|
||||
file, err := ioutil.ReadFile("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return []string{}
|
||||
return nameservers, err
|
||||
}
|
||||
|
||||
// Lines of the form "nameserver 1.2.3.4" accumulate.
|
||||
nameservers := []string{}
|
||||
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for l := range lines {
|
||||
trimmed := strings.TrimSpace(lines[l])
|
||||
|
@ -63,7 +62,7 @@ func getDNSServers() []string {
|
|||
}
|
||||
|
||||
glog.V(3).Infof("nameservers to use: %v", nameservers)
|
||||
return nameservers
|
||||
return nameservers, nil
|
||||
}
|
||||
|
||||
// getConfigKeyToStructKeyMap returns a map with the ConfigMapKey as key and the StructName as value.
|
||||
|
@ -143,6 +142,10 @@ func (ngx *Manager) ReadConfig(conf *api.ConfigMap) config.Configuration {
|
|||
|
||||
cfgDefault.CustomHTTPErrors = ngx.filterErrors(cErrors)
|
||||
cfgDefault.SkipAccessLogURLs = cSkipUrls
|
||||
// no custom resolver means use the system resolver
|
||||
if cfgDefault.Resolver == "" {
|
||||
cfgDefault.Resolver = ngx.defResolver
|
||||
}
|
||||
return cfgDefault
|
||||
}
|
||||
|
||||
|
|
|
@ -89,3 +89,13 @@ func TestManagerReadConfigStringNothing(t *testing.T) {
|
|||
t.Errorf("Failed to set string value true actual='%s' expected='%s'", configNginx.SSLSessionTimeout, exp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDNSServers(t *testing.T) {
|
||||
s, err := getDNSServers()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err)
|
||||
}
|
||||
if len(s) < 1 {
|
||||
t.Error("expected at least 1 nameserver in /etc/resolv.conf")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue