2016-02-08 20:57:32 +00:00
|
|
|
package provider
|
|
|
|
|
|
|
|
import (
|
2017-03-07 12:09:11 +00:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2016-06-22 16:31:14 +00:00
|
|
|
"reflect"
|
2016-05-18 16:30:42 +00:00
|
|
|
"strconv"
|
2016-04-25 14:56:06 +00:00
|
|
|
"strings"
|
2016-02-08 20:57:32 +00:00
|
|
|
"text/template"
|
|
|
|
"time"
|
2016-09-12 19:06:21 +00:00
|
|
|
|
2016-12-30 08:21:13 +00:00
|
|
|
"github.com/cenk/backoff"
|
|
|
|
"github.com/containous/traefik/job"
|
2016-11-11 22:50:20 +00:00
|
|
|
"github.com/containous/traefik/log"
|
|
|
|
"github.com/containous/traefik/provider/k8s"
|
|
|
|
"github.com/containous/traefik/safe"
|
|
|
|
"github.com/containous/traefik/types"
|
2017-04-07 10:49:53 +00:00
|
|
|
"k8s.io/client-go/pkg/api/v1"
|
|
|
|
"k8s.io/client-go/pkg/util/intstr"
|
2016-02-08 20:57:32 +00:00
|
|
|
)
|
|
|
|
|
2016-08-16 17:13:18 +00:00
|
|
|
var _ Provider = (*Kubernetes)(nil)
|
|
|
|
|
2017-02-06 23:04:30 +00:00
|
|
|
const (
|
|
|
|
annotationFrontendRuleType = "traefik.frontend.rule.type"
|
|
|
|
ruleTypePathPrefixStrip = "PathPrefixStrip"
|
|
|
|
ruleTypePathStrip = "PathStrip"
|
|
|
|
ruleTypePath = "Path"
|
|
|
|
ruleTypePathPrefix = "PathPrefix"
|
|
|
|
)
|
|
|
|
|
2016-02-08 20:57:32 +00:00
|
|
|
// Kubernetes holds configurations of the Kubernetes provider.
|
|
|
|
type Kubernetes struct {
|
2016-06-24 07:58:42 +00:00
|
|
|
BaseProvider `mapstructure:",squash"`
|
2017-03-07 12:09:11 +00:00
|
|
|
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)"`
|
|
|
|
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)"`
|
|
|
|
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)"`
|
2016-11-11 22:50:20 +00:00
|
|
|
DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers"`
|
|
|
|
Namespaces k8s.Namespaces `description:"Kubernetes namespaces"`
|
|
|
|
LabelSelector string `description:"Kubernetes api label selector to use"`
|
2016-06-22 16:31:14 +00:00
|
|
|
lastConfiguration safe.Safe
|
2016-02-08 20:57:32 +00:00
|
|
|
}
|
|
|
|
|
2016-11-11 22:50:20 +00:00
|
|
|
func (provider *Kubernetes) newK8sClient() (k8s.Client, error) {
|
2017-03-07 12:09:11 +00:00
|
|
|
withEndpoint := ""
|
2016-11-11 22:50:20 +00:00
|
|
|
if provider.Endpoint != "" {
|
2017-03-07 12:09:11 +00:00
|
|
|
withEndpoint = fmt.Sprintf(" with endpoint %v", provider.Endpoint)
|
2016-02-08 20:57:32 +00:00
|
|
|
}
|
2017-03-07 12:09:11 +00:00
|
|
|
|
|
|
|
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
|
|
|
|
log.Infof("Creating in-cluster Kubernetes client%s\n", withEndpoint)
|
|
|
|
return k8s.NewInClusterClient(provider.Endpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Infof("Creating cluster-external Kubernetes client%s\n", withEndpoint)
|
|
|
|
return k8s.NewExternalClusterClient(provider.Endpoint, provider.Token, provider.CertAuthFilePath)
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Provide allows the provider to provide configurations to traefik
|
|
|
|
// using the given configuration channel.
|
2016-11-09 18:27:04 +00:00
|
|
|
func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
2016-11-11 22:50:20 +00:00
|
|
|
k8sClient, err := provider.newK8sClient()
|
2016-02-08 20:57:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-05-30 13:05:58 +00:00
|
|
|
provider.Constraints = append(provider.Constraints, constraints...)
|
2016-02-08 20:57:32 +00:00
|
|
|
|
|
|
|
pool.Go(func(stop chan bool) {
|
|
|
|
operation := func() error {
|
|
|
|
for {
|
2016-12-03 20:20:39 +00:00
|
|
|
stopWatch := make(chan struct{}, 1)
|
2016-05-19 18:09:01 +00:00
|
|
|
defer close(stopWatch)
|
2016-08-19 09:09:54 +00:00
|
|
|
log.Debugf("Using label selector: '%s'", provider.LabelSelector)
|
2016-11-11 22:50:20 +00:00
|
|
|
eventsChan, err := k8sClient.WatchAll(provider.LabelSelector, stopWatch)
|
2016-04-25 14:56:06 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Error watching kubernetes events: %v", err)
|
2016-05-19 18:09:01 +00:00
|
|
|
timer := time.NewTimer(1 * time.Second)
|
|
|
|
select {
|
|
|
|
case <-timer.C:
|
|
|
|
return err
|
|
|
|
case <-stop:
|
|
|
|
return nil
|
|
|
|
}
|
2016-04-25 14:56:06 +00:00
|
|
|
}
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-stop:
|
|
|
|
return nil
|
|
|
|
case event := <-eventsChan:
|
2016-05-19 18:09:01 +00:00
|
|
|
log.Debugf("Received event from kubernetes %+v", event)
|
2016-04-25 14:56:06 +00:00
|
|
|
templateObjects, err := provider.loadIngresses(k8sClient)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-06-22 16:31:14 +00:00
|
|
|
if reflect.DeepEqual(provider.lastConfiguration.Get(), templateObjects) {
|
2016-11-22 21:02:51 +00:00
|
|
|
log.Debugf("Skipping event from kubernetes %+v", event)
|
2016-06-22 16:31:14 +00:00
|
|
|
} else {
|
|
|
|
provider.lastConfiguration.Set(templateObjects)
|
|
|
|
configurationChan <- types.ConfigMessage{
|
|
|
|
ProviderName: "kubernetes",
|
|
|
|
Configuration: provider.loadConfig(*templateObjects),
|
|
|
|
}
|
2016-04-25 14:56:06 +00:00
|
|
|
}
|
2016-02-08 20:57:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
notify := func(err error, time time.Duration) {
|
|
|
|
log.Errorf("Kubernetes connection error %+v, retrying in %s", err, time)
|
|
|
|
}
|
2016-12-08 12:32:12 +00:00
|
|
|
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
2016-02-08 20:57:32 +00:00
|
|
|
if err != nil {
|
2016-08-19 08:36:54 +00:00
|
|
|
log.Errorf("Cannot connect to Kubernetes server %+v", err)
|
2016-02-08 20:57:32 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-04-20 11:26:51 +00:00
|
|
|
func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configuration, error) {
|
2016-11-11 22:50:20 +00:00
|
|
|
ingresses := k8sClient.GetIngresses(provider.Namespaces)
|
|
|
|
|
2016-04-19 17:23:08 +00:00
|
|
|
templateObjects := types.Configuration{
|
|
|
|
map[string]*types.Backend{},
|
|
|
|
map[string]*types.Frontend{},
|
|
|
|
}
|
|
|
|
for _, i := range ingresses {
|
2017-03-03 19:30:22 +00:00
|
|
|
ingressClass := i.Annotations["kubernetes.io/ingress.class"]
|
|
|
|
|
|
|
|
if !shouldProcessIngress(ingressClass) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2016-04-19 17:23:08 +00:00
|
|
|
for _, r := range i.Spec.Rules {
|
2016-12-08 12:32:52 +00:00
|
|
|
if r.HTTP == nil {
|
|
|
|
log.Warnf("Error in ingress: HTTP is nil")
|
|
|
|
continue
|
|
|
|
}
|
2016-04-19 17:23:08 +00:00
|
|
|
for _, pa := range r.HTTP.Paths {
|
|
|
|
if _, exists := templateObjects.Backends[r.Host+pa.Path]; !exists {
|
|
|
|
templateObjects.Backends[r.Host+pa.Path] = &types.Backend{
|
|
|
|
Servers: make(map[string]types.Server),
|
2017-01-25 13:11:00 +00:00
|
|
|
LoadBalancer: &types.LoadBalancer{
|
|
|
|
Sticky: false,
|
|
|
|
Method: "wrr",
|
|
|
|
},
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
2017-02-10 11:05:59 +00:00
|
|
|
|
|
|
|
PassHostHeader := provider.getPassHostHeader()
|
|
|
|
|
|
|
|
passHostHeaderAnnotation := i.Annotations["traefik.frontend.passHostHeader"]
|
|
|
|
switch passHostHeaderAnnotation {
|
|
|
|
case "true":
|
|
|
|
PassHostHeader = true
|
|
|
|
case "false":
|
|
|
|
PassHostHeader = false
|
2017-02-14 19:52:54 +00:00
|
|
|
default:
|
|
|
|
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
|
2017-02-10 11:05:59 +00:00
|
|
|
}
|
|
|
|
|
2016-04-19 17:23:08 +00:00
|
|
|
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
|
|
|
|
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
|
2016-05-10 11:43:24 +00:00
|
|
|
Backend: r.Host + pa.Path,
|
|
|
|
PassHostHeader: PassHostHeader,
|
|
|
|
Routes: make(map[string]types.Route),
|
2016-08-02 23:48:53 +00:00
|
|
|
Priority: len(pa.Path),
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
2016-05-25 12:16:19 +00:00
|
|
|
if len(r.Host) > 0 {
|
2017-01-20 13:16:05 +00:00
|
|
|
rule := "Host:" + r.Host
|
|
|
|
|
|
|
|
if strings.Contains(r.Host, "*") {
|
|
|
|
rule = "HostRegexp:" + strings.Replace(r.Host, "*", "{subdomain:[A-Za-z0-9-_]+}", 1)
|
|
|
|
}
|
|
|
|
|
2016-05-25 12:16:19 +00:00
|
|
|
if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists {
|
|
|
|
templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{
|
2017-01-20 13:16:05 +00:00
|
|
|
Rule: rule,
|
2016-05-25 12:16:19 +00:00
|
|
|
}
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
2017-02-06 23:04:30 +00:00
|
|
|
|
2016-04-19 17:23:08 +00:00
|
|
|
if len(pa.Path) > 0 {
|
2017-02-06 23:04:30 +00:00
|
|
|
ruleType, unknown := getRuleTypeFromAnnotation(i.Annotations)
|
|
|
|
switch {
|
|
|
|
case unknown:
|
|
|
|
log.Warnf("Unknown RuleType '%s' for Ingress %s/%s, falling back to PathPrefix", ruleType, i.ObjectMeta.Namespace, i.ObjectMeta.Name)
|
|
|
|
fallthrough
|
|
|
|
case ruleType == "":
|
|
|
|
ruleType = ruleTypePathPrefix
|
2016-05-17 10:50:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{
|
|
|
|
Rule: ruleType + ":" + pa.Path,
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
2017-02-06 23:04:30 +00:00
|
|
|
|
2016-11-11 22:50:20 +00:00
|
|
|
service, exists, err := k8sClient.GetService(i.ObjectMeta.Namespace, pa.Backend.ServiceName)
|
2017-03-17 15:34:34 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", service.ObjectMeta.Namespace, pa.Backend.ServiceName, err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !exists {
|
|
|
|
log.Errorf("Service not found for %s/%s", service.ObjectMeta.Namespace, pa.Backend.ServiceName)
|
2016-04-25 14:56:06 +00:00
|
|
|
delete(templateObjects.Frontends, r.Host+pa.Path)
|
2016-05-25 23:53:51 +00:00
|
|
|
continue
|
2016-04-25 14:56:06 +00:00
|
|
|
}
|
2016-11-11 22:50:20 +00:00
|
|
|
|
2017-02-03 16:47:48 +00:00
|
|
|
if expression := service.Annotations["traefik.backend.circuitbreaker"]; expression != "" {
|
|
|
|
templateObjects.Backends[r.Host+pa.Path].CircuitBreaker = &types.CircuitBreaker{
|
|
|
|
Expression: expression,
|
|
|
|
}
|
|
|
|
}
|
2017-01-25 13:11:00 +00:00
|
|
|
if service.Annotations["traefik.backend.loadbalancer.method"] == "drr" {
|
|
|
|
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr"
|
|
|
|
}
|
|
|
|
if service.Annotations["traefik.backend.loadbalancer.sticky"] == "true" {
|
|
|
|
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = true
|
|
|
|
}
|
2017-02-03 16:47:48 +00:00
|
|
|
|
2016-05-25 23:53:51 +00:00
|
|
|
protocol := "http"
|
|
|
|
for _, port := range service.Spec.Ports {
|
|
|
|
if equalPorts(port, pa.Backend.ServicePort) {
|
|
|
|
if port.Port == 443 {
|
|
|
|
protocol = "https"
|
|
|
|
}
|
2017-02-10 01:25:38 +00:00
|
|
|
if service.Spec.Type == "ExternalName" {
|
|
|
|
url := protocol + "://" + service.Spec.ExternalName
|
|
|
|
name := url
|
|
|
|
|
|
|
|
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
|
|
|
|
URL: url,
|
2016-05-25 23:53:51 +00:00
|
|
|
Weight: 1,
|
2016-05-20 16:34:57 +00:00
|
|
|
}
|
2016-05-25 23:53:51 +00:00
|
|
|
} else {
|
2017-03-14 14:59:13 +00:00
|
|
|
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
2017-03-17 15:34:34 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err)
|
|
|
|
return nil, err
|
2017-03-14 14:59:13 +00:00
|
|
|
}
|
2017-02-14 22:57:09 +00:00
|
|
|
|
2017-03-17 15:34:34 +00:00
|
|
|
if !exists {
|
|
|
|
log.Errorf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
|
|
|
continue
|
|
|
|
}
|
2017-02-14 19:53:35 +00:00
|
|
|
|
2017-03-14 14:59:13 +00:00
|
|
|
if len(endpoints.Subsets) == 0 {
|
2017-03-17 15:34:34 +00:00
|
|
|
log.Warnf("Service endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
2017-03-14 14:59:13 +00:00
|
|
|
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{
|
|
|
|
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)),
|
|
|
|
Weight: 1,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for _, subset := range endpoints.Subsets {
|
|
|
|
for _, address := range subset.Addresses {
|
|
|
|
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
|
|
|
|
name := url
|
|
|
|
if address.TargetRef != nil && address.TargetRef.Name != "" {
|
|
|
|
name = address.TargetRef.Name
|
|
|
|
}
|
|
|
|
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
|
|
|
|
URL: url,
|
|
|
|
Weight: 1,
|
2016-05-20 16:34:57 +00:00
|
|
|
}
|
|
|
|
}
|
2016-04-20 11:26:51 +00:00
|
|
|
}
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
2017-03-14 14:59:13 +00:00
|
|
|
}
|
2016-05-25 23:53:51 +00:00
|
|
|
break
|
2016-04-19 17:23:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &templateObjects, nil
|
|
|
|
}
|
|
|
|
|
2016-11-11 22:50:20 +00:00
|
|
|
func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
|
2016-05-20 16:34:57 +00:00
|
|
|
if len(endpointPorts) > 0 {
|
|
|
|
//name is optional if there is only one port
|
|
|
|
port := endpointPorts[0]
|
|
|
|
for _, endpointPort := range endpointPorts {
|
|
|
|
if servicePort.Name == endpointPort.Name {
|
|
|
|
port = endpointPort
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return int(port.Port)
|
|
|
|
}
|
2016-11-11 22:50:20 +00:00
|
|
|
return int(servicePort.Port)
|
2016-05-20 16:34:57 +00:00
|
|
|
}
|
|
|
|
|
2016-11-11 22:50:20 +00:00
|
|
|
func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool {
|
|
|
|
if int(servicePort.Port) == ingressPort.IntValue() {
|
2016-05-18 16:30:42 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
if servicePort.Name != "" && servicePort.Name == ingressPort.String() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-03-03 19:30:22 +00:00
|
|
|
func shouldProcessIngress(ingressClass string) bool {
|
|
|
|
switch ingressClass {
|
|
|
|
case "", "traefik":
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-10 11:43:24 +00:00
|
|
|
func (provider *Kubernetes) getPassHostHeader() bool {
|
2016-05-03 14:52:14 +00:00
|
|
|
if provider.DisablePassHostHeaders {
|
2016-05-10 11:43:24 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-02-08 20:57:32 +00:00
|
|
|
func (provider *Kubernetes) loadConfig(templateObjects types.Configuration) *types.Configuration {
|
|
|
|
var FuncMap = template.FuncMap{}
|
|
|
|
configuration, err := provider.getConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
}
|
|
|
|
return configuration
|
|
|
|
}
|
2017-02-06 23:04:30 +00:00
|
|
|
|
|
|
|
func getRuleTypeFromAnnotation(annotations map[string]string) (ruleType string, unknown bool) {
|
|
|
|
ruleType = annotations[annotationFrontendRuleType]
|
|
|
|
for _, knownRuleType := range []string{
|
|
|
|
ruleTypePathPrefixStrip,
|
|
|
|
ruleTypePathStrip,
|
|
|
|
ruleTypePath,
|
|
|
|
ruleTypePathPrefix,
|
|
|
|
} {
|
|
|
|
if strings.ToLower(ruleType) == strings.ToLower(knownRuleType) {
|
|
|
|
return knownRuleType, false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ruleType != "" {
|
|
|
|
// Annotation is set but does not match anything we know.
|
|
|
|
unknown = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return ruleType, unknown
|
|
|
|
}
|