Add basic auth to kubernetes provider

This commit is contained in:
Alex Peters 2017-04-23 16:17:20 +02:00 committed by Fernandez Ludovic
parent dcc4d92983
commit 89da3b15a4
4 changed files with 267 additions and 0 deletions

View file

@ -26,6 +26,7 @@ const resyncPeriod = time.Minute * 5
type Client interface {
GetIngresses(namespaces Namespaces) []*v1beta1.Ingress
GetService(namespace, name string) (*v1.Service, bool, error)
GetSecret(namespace, name string) (*v1.Secret, bool, error)
GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error)
WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error)
}
@ -34,10 +35,12 @@ type clientImpl struct {
ingController *cache.Controller
svcController *cache.Controller
epController *cache.Controller
secController *cache.Controller
ingStore cache.Store
svcStore cache.Store
epStore cache.Store
secStore cache.Store
clientset *kubernetes.Clientset
}
@ -154,6 +157,16 @@ func (c *clientImpl) GetService(namespace, name string) (*v1.Service, bool, erro
return service, exists, err
}
func (c *clientImpl) GetSecret(namespace, name string) (*v1.Secret, bool, error) {
var secret *v1.Secret
item, exists, err := c.secStore.GetByKey(namespace + "/" + name)
if err == nil && item != nil {
secret = item.(*v1.Secret)
}
return secret, exists, err
}
// WatchServices starts the watch of Provider Service resources and updates the corresponding store
func (c *clientImpl) WatchServices(watchCh chan<- interface{}, stopCh <-chan struct{}) {
source := cache.NewListWatchFromClient(
@ -199,6 +212,21 @@ func (c *clientImpl) WatchEndpoints(watchCh chan<- interface{}, stopCh <-chan st
go c.epController.Run(stopCh)
}
func (c *clientImpl) WatchSecrets(watchCh chan<- interface{}, stopCh <-chan struct{}) {
source := cache.NewListWatchFromClient(
c.clientset.CoreV1().RESTClient(),
"secrets",
api.NamespaceAll,
fields.Everything())
c.secStore, c.secController = cache.NewInformer(
source,
&v1.Endpoints{},
resyncPeriod,
newResourceEventHandlerFuncs(watchCh))
go c.secController.Run(stopCh)
}
// WatchAll returns events in the cluster and updates the stores via informer
// Filters ingresses by labelSelector
func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error) {
@ -213,6 +241,7 @@ func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-c
c.WatchIngresses(kubeLabelSelector, eventCh, stopCh)
c.WatchServices(eventCh, stopCh)
c.WatchEndpoints(eventCh, stopCh)
c.WatchSecrets(eventCh, stopCh)
go func() {
defer close(watchCh)

View file

@ -1,6 +1,9 @@
package kubernetes
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"reflect"
@ -16,6 +19,7 @@ import (
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"k8s.io/client-go/pkg/api/v1"
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
"k8s.io/client-go/pkg/util/intstr"
)
@ -29,6 +33,8 @@ const (
ruleTypePathPrefix = "PathPrefix"
)
const traefikDefaultRealm = "traefik"
// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash"`
@ -159,13 +165,21 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
default:
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
}
if realm := i.Annotations["ingress.kubernetes.io/auth-realm"]; realm != "" && realm != traefikDefaultRealm {
return nil, errors.New("no realm customization supported")
}
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil {
return nil, err
}
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path,
PassHostHeader: PassHostHeader,
Routes: make(map[string]types.Route),
Priority: len(pa.Path),
BasicAuth: basicAuthCreds,
}
}
if len(r.Host) > 0 {
@ -278,6 +292,56 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
return &templateObjects, nil
}
func handleBasicAuthConfig(i *v1beta1.Ingress, k8sClient Client) ([]string, error) {
authType, exists := i.Annotations["ingress.kubernetes.io/auth-type"]
if !exists {
return nil, nil
}
if strings.ToLower(authType) != "basic" {
return nil, fmt.Errorf("unsupported auth-type: %q", authType)
}
authSecret := i.Annotations["ingress.kubernetes.io/auth-secret"]
if authSecret == "" {
return nil, errors.New("auth-secret annotation must be set")
}
basicAuthCreds, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient)
if err != nil {
return nil, err
}
if len(basicAuthCreds) == 0 {
return nil, errors.New("secret file without credentials")
}
return basicAuthCreds, nil
}
func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]string, error) {
secret, ok, err := k8sClient.GetSecret(namespace, secretName)
switch { // keep order of case conditions
case err != nil:
return nil, fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err)
case !ok:
return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName)
case secret == nil:
return nil, errors.New("secret data must not be nil")
case len(secret.Data) != 1:
return nil, errors.New("secret must contain single element only")
default:
}
var firstSecret []byte
for _, v := range secret.Data {
firstSecret = v
break
}
creds := make([]string, 0)
scanner := bufio.NewScanner(bytes.NewReader(firstSecret))
for scanner.Scan() {
if cred := scanner.Text(); cred != "" {
creds = append(creds, cred)
}
}
return creds, nil
}
func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
if len(endpointPorts) > 0 {
//name is optional if there is only one port

View file

@ -1466,6 +1466,35 @@ func TestIngressAnnotations(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"ingress.kubernetes.io/auth-type": "basic",
"ingress.kubernetes.io/auth-secret": "mySecret",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "basic",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/auth",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
@ -1515,12 +1544,25 @@ func TestIngressAnnotations(t *testing.T) {
},
},
}
secrets := []*v1.Secret{
{
ObjectMeta: v1.ObjectMeta{
Name: "mySecret",
UID: "1",
Namespace: "testing",
},
Data: map[string][]byte{
"auth": []byte("myUser:myEncodedPW"),
},
},
}
endpoints := []*v1.Endpoints{}
watchChan := make(chan interface{})
client := clientMock{
ingresses: ingresses,
services: services,
secrets: secrets,
endpoints: endpoints,
watchChan: watchChan,
}
@ -1558,6 +1600,19 @@ func TestIngressAnnotations(t *testing.T) {
Method: "wrr",
},
},
"basic/auth": {
Servers: map[string]types.Server{
"http://example.com": {
URL: "http://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
},
Frontends: map[string]*types.Frontend{
"foo/bar": {
@ -1586,6 +1641,20 @@ func TestIngressAnnotations(t *testing.T) {
},
},
},
"basic/auth": {
Backend: "basic/auth",
PassHostHeader: true,
Priority: len("/auth"),
Routes: map[string]types.Route{
"/auth": {
Rule: "PathPrefix:/auth",
},
"basic": {
Rule: "Host:basic",
},
},
BasicAuth: []string{"myUser:myEncodedPW"},
},
},
}
@ -2030,13 +2099,102 @@ func TestMissingResources(t *testing.T) {
}
}
func TestBasicAuthInTemplate(t *testing.T) {
ingresses := []*v1beta1.Ingress{
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"ingress.kubernetes.io/auth-type": "basic",
"ingress.kubernetes.io/auth-secret": "mySecret",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "basic",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/auth",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
}
services := []*v1.Service{
{
ObjectMeta: v1.ObjectMeta{
Name: "service1",
UID: "1",
Namespace: "testing",
},
Spec: v1.ServiceSpec{
ClusterIP: "10.0.0.1",
Type: "ExternalName",
ExternalName: "example.com",
Ports: []v1.ServicePort{
{
Name: "http",
Port: 80,
},
},
},
},
}
secrets := []*v1.Secret{
{
ObjectMeta: v1.ObjectMeta{
Name: "mySecret",
UID: "1",
Namespace: "testing",
},
Data: map[string][]byte{
"auth": []byte("myUser:myEncodedPW"),
},
},
}
endpoints := []*v1.Endpoints{}
watchChan := make(chan interface{})
client := clientMock{
ingresses: ingresses,
services: services,
secrets: secrets,
endpoints: endpoints,
watchChan: watchChan,
}
provider := Provider{}
actual, err := provider.loadIngresses(client)
if err != nil {
t.Fatalf("error %+v", err)
}
actual = provider.loadConfig(*actual)
got := actual.Frontends["basic/auth"].BasicAuth
if !reflect.DeepEqual(got, []string{"myUser:myEncodedPW"}) {
t.Fatalf("unexpected credentials: %+v", got)
}
}
type clientMock struct {
ingresses []*v1beta1.Ingress
services []*v1.Service
secrets []*v1.Secret
endpoints []*v1.Endpoints
watchChan chan interface{}
apiServiceError error
apiSecretError error
apiEndpointsError error
}
@ -2064,6 +2222,19 @@ func (c clientMock) GetService(namespace, name string) (*v1.Service, bool, error
return nil, false, nil
}
func (c clientMock) GetSecret(namespace, name string) (*v1.Secret, bool, error) {
if c.apiSecretError != nil {
return nil, false, c.apiSecretError
}
for _, secret := range c.secrets {
if secret.Namespace == namespace && secret.Name == name {
return secret, true, nil
}
}
return nil, false, nil
}
func (c clientMock) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) {
if c.apiEndpointsError != nil {
return nil, false, c.apiEndpointsError

View file

@ -20,6 +20,9 @@
backend = "{{$frontend.Backend}}"
priority = {{$frontend.Priority}}
passHostHeader = {{$frontend.PassHostHeader}}
basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}",
{{end}}]
{{range $routeName, $route := $frontend.Routes}}
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
rule = "{{$route.Rule}}"