Add basic auth to kubernetes provider
This commit is contained in:
parent
dcc4d92983
commit
89da3b15a4
4 changed files with 267 additions and 0 deletions
|
@ -26,6 +26,7 @@ const resyncPeriod = time.Minute * 5
|
||||||
type Client interface {
|
type Client interface {
|
||||||
GetIngresses(namespaces Namespaces) []*v1beta1.Ingress
|
GetIngresses(namespaces Namespaces) []*v1beta1.Ingress
|
||||||
GetService(namespace, name string) (*v1.Service, bool, error)
|
GetService(namespace, name string) (*v1.Service, bool, error)
|
||||||
|
GetSecret(namespace, name string) (*v1.Secret, bool, error)
|
||||||
GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error)
|
GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error)
|
||||||
WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error)
|
WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error)
|
||||||
}
|
}
|
||||||
|
@ -34,10 +35,12 @@ type clientImpl struct {
|
||||||
ingController *cache.Controller
|
ingController *cache.Controller
|
||||||
svcController *cache.Controller
|
svcController *cache.Controller
|
||||||
epController *cache.Controller
|
epController *cache.Controller
|
||||||
|
secController *cache.Controller
|
||||||
|
|
||||||
ingStore cache.Store
|
ingStore cache.Store
|
||||||
svcStore cache.Store
|
svcStore cache.Store
|
||||||
epStore cache.Store
|
epStore cache.Store
|
||||||
|
secStore cache.Store
|
||||||
|
|
||||||
clientset *kubernetes.Clientset
|
clientset *kubernetes.Clientset
|
||||||
}
|
}
|
||||||
|
@ -154,6 +157,16 @@ func (c *clientImpl) GetService(namespace, name string) (*v1.Service, bool, erro
|
||||||
return service, exists, err
|
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
|
// WatchServices starts the watch of Provider Service resources and updates the corresponding store
|
||||||
func (c *clientImpl) WatchServices(watchCh chan<- interface{}, stopCh <-chan struct{}) {
|
func (c *clientImpl) WatchServices(watchCh chan<- interface{}, stopCh <-chan struct{}) {
|
||||||
source := cache.NewListWatchFromClient(
|
source := cache.NewListWatchFromClient(
|
||||||
|
@ -199,6 +212,21 @@ func (c *clientImpl) WatchEndpoints(watchCh chan<- interface{}, stopCh <-chan st
|
||||||
go c.epController.Run(stopCh)
|
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
|
// WatchAll returns events in the cluster and updates the stores via informer
|
||||||
// Filters ingresses by labelSelector
|
// Filters ingresses by labelSelector
|
||||||
func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error) {
|
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.WatchIngresses(kubeLabelSelector, eventCh, stopCh)
|
||||||
c.WatchServices(eventCh, stopCh)
|
c.WatchServices(eventCh, stopCh)
|
||||||
c.WatchEndpoints(eventCh, stopCh)
|
c.WatchEndpoints(eventCh, stopCh)
|
||||||
|
c.WatchSecrets(eventCh, stopCh)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(watchCh)
|
defer close(watchCh)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -16,6 +19,7 @@ import (
|
||||||
"github.com/containous/traefik/safe"
|
"github.com/containous/traefik/safe"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"k8s.io/client-go/pkg/api/v1"
|
"k8s.io/client-go/pkg/api/v1"
|
||||||
|
"k8s.io/client-go/pkg/apis/extensions/v1beta1"
|
||||||
"k8s.io/client-go/pkg/util/intstr"
|
"k8s.io/client-go/pkg/util/intstr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,6 +33,8 @@ const (
|
||||||
ruleTypePathPrefix = "PathPrefix"
|
ruleTypePathPrefix = "PathPrefix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const traefikDefaultRealm = "traefik"
|
||||||
|
|
||||||
// Provider holds configurations of the provider.
|
// Provider holds configurations of the provider.
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
provider.BaseProvider `mapstructure:",squash"`
|
provider.BaseProvider `mapstructure:",squash"`
|
||||||
|
@ -159,13 +165,21 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
default:
|
default:
|
||||||
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
|
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 {
|
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{
|
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
|
||||||
Backend: r.Host + pa.Path,
|
Backend: r.Host + pa.Path,
|
||||||
PassHostHeader: PassHostHeader,
|
PassHostHeader: PassHostHeader,
|
||||||
Routes: make(map[string]types.Route),
|
Routes: make(map[string]types.Route),
|
||||||
Priority: len(pa.Path),
|
Priority: len(pa.Path),
|
||||||
|
BasicAuth: basicAuthCreds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(r.Host) > 0 {
|
if len(r.Host) > 0 {
|
||||||
|
@ -278,6 +292,56 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
return &templateObjects, nil
|
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 {
|
func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
|
||||||
if len(endpointPorts) > 0 {
|
if len(endpointPorts) > 0 {
|
||||||
//name is optional if there is only one port
|
//name is optional if there is only one port
|
||||||
|
|
|
@ -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{
|
ObjectMeta: v1.ObjectMeta{
|
||||||
Namespace: "testing",
|
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{}
|
endpoints := []*v1.Endpoints{}
|
||||||
watchChan := make(chan interface{})
|
watchChan := make(chan interface{})
|
||||||
client := clientMock{
|
client := clientMock{
|
||||||
ingresses: ingresses,
|
ingresses: ingresses,
|
||||||
services: services,
|
services: services,
|
||||||
|
secrets: secrets,
|
||||||
endpoints: endpoints,
|
endpoints: endpoints,
|
||||||
watchChan: watchChan,
|
watchChan: watchChan,
|
||||||
}
|
}
|
||||||
|
@ -1558,6 +1600,19 @@ func TestIngressAnnotations(t *testing.T) {
|
||||||
Method: "wrr",
|
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{
|
Frontends: map[string]*types.Frontend{
|
||||||
"foo/bar": {
|
"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 {
|
type clientMock struct {
|
||||||
ingresses []*v1beta1.Ingress
|
ingresses []*v1beta1.Ingress
|
||||||
services []*v1.Service
|
services []*v1.Service
|
||||||
|
secrets []*v1.Secret
|
||||||
endpoints []*v1.Endpoints
|
endpoints []*v1.Endpoints
|
||||||
watchChan chan interface{}
|
watchChan chan interface{}
|
||||||
|
|
||||||
apiServiceError error
|
apiServiceError error
|
||||||
|
apiSecretError error
|
||||||
apiEndpointsError error
|
apiEndpointsError error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2064,6 +2222,19 @@ func (c clientMock) GetService(namespace, name string) (*v1.Service, bool, error
|
||||||
return nil, false, nil
|
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) {
|
func (c clientMock) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) {
|
||||||
if c.apiEndpointsError != nil {
|
if c.apiEndpointsError != nil {
|
||||||
return nil, false, c.apiEndpointsError
|
return nil, false, c.apiEndpointsError
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
backend = "{{$frontend.Backend}}"
|
backend = "{{$frontend.Backend}}"
|
||||||
priority = {{$frontend.Priority}}
|
priority = {{$frontend.Priority}}
|
||||||
passHostHeader = {{$frontend.PassHostHeader}}
|
passHostHeader = {{$frontend.PassHostHeader}}
|
||||||
|
basicAuth = [{{range $frontend.BasicAuth}}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
{{range $routeName, $route := $frontend.Routes}}
|
{{range $routeName, $route := $frontend.Routes}}
|
||||||
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
|
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
|
||||||
rule = "{{$route.Rule}}"
|
rule = "{{$route.Rule}}"
|
||||||
|
|
Loading…
Reference in a new issue