WeightedRoundRobin load balancer

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
Julien Salleyron 2019-08-26 10:30:05 +02:00 committed by Traefiker Bot
parent 84de444325
commit 6fed76a687
44 changed files with 1612 additions and 833 deletions

View file

@ -3,7 +3,6 @@ package crd
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
"reflect"
@ -16,7 +15,6 @@ import (
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/job"
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/containous/traefik/v2/pkg/safe"
"github.com/containous/traefik/v2/pkg/tls"
corev1 "k8s.io/api/core/v1"
@ -136,159 +134,22 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
return nil
}
func checkStringQuoteValidity(value string) error {
_, err := strconv.Unquote(`"` + value + `"`)
return err
}
func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) {
service, exists, err := client.GetService(namespace, svc.Name)
if err != nil {
return nil, err
func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration {
tlsConfigs := make(map[string]*tls.CertAndStores)
conf := &dynamic.Configuration{
HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs),
TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs),
TLS: &dynamic.TLSConfiguration{
Certificates: getTLSConfig(tlsConfigs),
Options: buildTLSOptions(ctx, client),
},
}
if !exists {
return nil, errors.New("service not found")
for _, middleware := range client.GetMiddlewares() {
conf.HTTP.Middlewares[makeID(middleware.Namespace, middleware.Name)] = &middleware.Spec
}
var portSpec *corev1.ServicePort
for _, p := range service.Spec.Ports {
if svc.Port == p.Port {
portSpec = &p
break
}
}
if portSpec == nil {
return nil, errors.New("service port not found")
}
var servers []dynamic.TCPServer
if service.Spec.Type == corev1.ServiceTypeExternalName {
servers = append(servers, dynamic.TCPServer{
Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port),
})
} else {
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
if endpointsErr != nil {
return nil, endpointsErr
}
if !endpointsExists {
return nil, errors.New("endpoints not found")
}
if len(endpoints.Subsets) == 0 {
return nil, errors.New("subset not found")
}
var port int32
for _, subset := range endpoints.Subsets {
for _, p := range subset.Ports {
if portSpec.Name == p.Name {
port = p.Port
break
}
}
if port == 0 {
return nil, errors.New("cannot define a port")
}
for _, addr := range subset.Addresses {
servers = append(servers, dynamic.TCPServer{
Address: fmt.Sprintf("%s:%d", addr.IP, port),
})
}
}
}
return servers, nil
}
func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]dynamic.Server, error) {
strategy := svc.Strategy
if strategy == "" {
strategy = "RoundRobin"
}
if strategy != "RoundRobin" {
return nil, fmt.Errorf("load balancing strategy %v is not supported", strategy)
}
service, exists, err := client.GetService(namespace, svc.Name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.New("service not found")
}
var portSpec *corev1.ServicePort
for _, p := range service.Spec.Ports {
if svc.Port == p.Port {
portSpec = &p
break
}
}
if portSpec == nil {
return nil, errors.New("service port not found")
}
var servers []dynamic.Server
if service.Spec.Type == corev1.ServiceTypeExternalName {
servers = append(servers, dynamic.Server{
URL: fmt.Sprintf("http://%s:%d", service.Spec.ExternalName, portSpec.Port),
})
} else {
endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name)
if endpointsErr != nil {
return nil, endpointsErr
}
if !endpointsExists {
return nil, errors.New("endpoints not found")
}
if len(endpoints.Subsets) == 0 {
return nil, errors.New("subset not found")
}
var port int32
for _, subset := range endpoints.Subsets {
for _, p := range subset.Ports {
if portSpec.Name == p.Name {
port = p.Port
break
}
}
if port == 0 {
return nil, errors.New("cannot define a port")
}
protocol := "http"
switch svc.Scheme {
case "http", "https", "h2c":
protocol = svc.Scheme
case "":
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
protocol = "https"
}
default:
return nil, fmt.Errorf("invalid scheme %q specified", svc.Scheme)
}
for _, addr := range subset.Addresses {
servers = append(servers, dynamic.Server{
URL: fmt.Sprintf("%s://%s:%d", protocol, addr.IP, port),
})
}
}
}
return servers, nil
return conf
}
func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options {
@ -338,250 +199,9 @@ func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options
return tlsOptions
}
func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.HTTPConfiguration {
conf := &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
}
for _, ingressRoute := range client.GetIngressRoutes() {
logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRoute.Name), log.Str("namespace", ingressRoute.Namespace)))
// TODO keep the name ingressClass?
if !shouldProcessIngress(p.IngressClass, ingressRoute.Annotations[annotationKubernetesIngressClass]) {
continue
}
err := getTLSHTTP(ctx, ingressRoute, client, tlsConfigs)
if err != nil {
logger.Errorf("Error configuring TLS: %v", err)
}
ingressName := ingressRoute.Name
if len(ingressName) == 0 {
ingressName = ingressRoute.GenerateName
}
for _, route := range ingressRoute.Spec.Routes {
if route.Kind != "Rule" {
logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind)
continue
}
if len(route.Match) == 0 {
logger.Errorf("Empty match rule")
continue
}
if err := checkStringQuoteValidity(route.Match); err != nil {
logger.Errorf("Invalid syntax for match rule: %s", route.Match)
continue
}
var allServers []dynamic.Server
for _, service := range route.Services {
servers, err := loadServers(client, ingressRoute.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
WithField("servicePort", service.Port).
Errorf("Cannot create service: %v", err)
continue
}
allServers = append(allServers, servers...)
}
var mds []string
for _, mi := range route.Middlewares {
if strings.Contains(mi.Name, "@") {
if len(mi.Namespace) > 0 {
logger.
WithField(log.MiddlewareName, mi.Name).
Warnf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
}
ns := mi.Namespace
if len(ns) == 0 {
ns = ingressRoute.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
}
key, err := makeServiceKey(route.Match, ingressName)
if err != nil {
logger.Error(err)
continue
}
serviceName := makeID(ingressRoute.Namespace, key)
conf.Routers[serviceName] = &dynamic.Router{
Middlewares: mds,
Priority: route.Priority,
EntryPoints: ingressRoute.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
}
if ingressRoute.Spec.TLS != nil {
tlsConf := &dynamic.RouterTLSConfig{
CertResolver: ingressRoute.Spec.TLS.CertResolver,
}
if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRoute.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference, (i.e. not a cross-provider reference)
ns := ingressRoute.Spec.TLS.Options.Namespace
if !strings.Contains(tlsOptionsName, "@") {
if len(ns) == 0 {
ns = ingressRoute.Namespace
}
tlsOptionsName = makeID(ns, tlsOptionsName)
} else if len(ns) > 0 {
logger.
WithField("TLSoptions", ingressRoute.Spec.TLS.Options.Name).
Warnf("namespace %q is ignored in cross-provider context", ns)
}
tlsConf.Options = tlsOptionsName
}
conf.Routers[serviceName].TLS = tlsConf
}
conf.Services[serviceName] = &dynamic.Service{
LoadBalancer: &dynamic.LoadBalancerService{
Servers: allServers,
// TODO: support other strategies.
PassHostHeader: true,
},
}
}
}
return conf
}
func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client Client, tlsConfigs map[string]*tls.CertAndStores) *dynamic.TCPConfiguration {
conf := &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
}
for _, ingressRouteTCP := range client.GetIngressRouteTCPs() {
logger := log.FromContext(log.With(ctx, log.Str("ingress", ingressRouteTCP.Name), log.Str("namespace", ingressRouteTCP.Namespace)))
if !shouldProcessIngress(p.IngressClass, ingressRouteTCP.Annotations[annotationKubernetesIngressClass]) {
continue
}
if ingressRouteTCP.Spec.TLS != nil && !ingressRouteTCP.Spec.TLS.Passthrough {
err := getTLSTCP(ctx, ingressRouteTCP, client, tlsConfigs)
if err != nil {
logger.Errorf("Error configuring TLS: %v", err)
}
}
ingressName := ingressRouteTCP.Name
if len(ingressName) == 0 {
ingressName = ingressRouteTCP.GenerateName
}
for _, route := range ingressRouteTCP.Spec.Routes {
if len(route.Match) == 0 {
logger.Errorf("Empty match rule")
continue
}
if err := checkStringQuoteValidity(route.Match); err != nil {
logger.Errorf("Invalid syntax for match rule: %s", route.Match)
continue
}
var allServers []dynamic.TCPServer
for _, service := range route.Services {
servers, err := loadTCPServers(client, ingressRouteTCP.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
WithField("servicePort", service.Port).
Errorf("Cannot create service: %v", err)
continue
}
allServers = append(allServers, servers...)
}
key, e := makeServiceKey(route.Match, ingressName)
if e != nil {
logger.Error(e)
continue
}
serviceName := makeID(ingressRouteTCP.Namespace, key)
conf.Routers[serviceName] = &dynamic.TCPRouter{
EntryPoints: ingressRouteTCP.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
}
if ingressRouteTCP.Spec.TLS != nil {
conf.Routers[serviceName].TLS = &dynamic.RouterTCPTLSConfig{
Passthrough: ingressRouteTCP.Spec.TLS.Passthrough,
CertResolver: ingressRouteTCP.Spec.TLS.CertResolver,
}
if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 {
tlsOptionsName := ingressRouteTCP.Spec.TLS.Options.Name
// Is a Kubernetes CRD reference (i.e. not a cross-provider reference)
ns := ingressRouteTCP.Spec.TLS.Options.Namespace
if !strings.Contains(tlsOptionsName, "@") {
if len(ns) == 0 {
ns = ingressRouteTCP.Namespace
}
tlsOptionsName = makeID(ns, tlsOptionsName)
} else if len(ns) > 0 {
logger.
WithField("TLSoptions", ingressRouteTCP.Spec.TLS.Options.Name).
Warnf("namespace %q is ignored in cross-provider context", ns)
}
conf.Routers[serviceName].TLS.Options = tlsOptionsName
}
}
conf.Services[serviceName] = &dynamic.TCPService{
LoadBalancer: &dynamic.TCPLoadBalancerService{
Servers: allServers,
},
}
}
}
return conf
}
func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) *dynamic.Configuration {
tlsConfigs := make(map[string]*tls.CertAndStores)
conf := &dynamic.Configuration{
HTTP: p.loadIngressRouteConfiguration(ctx, client, tlsConfigs),
TCP: p.loadIngressRouteTCPConfiguration(ctx, client, tlsConfigs),
TLS: &dynamic.TLSConfiguration{
Certificates: getTLSConfig(tlsConfigs),
Options: buildTLSOptions(ctx, client),
},
}
for _, middleware := range client.GetMiddlewares() {
conf.HTTP.Middlewares[makeID(middleware.Namespace, middleware.Name)] = &middleware.Spec
}
return conf
func checkStringQuoteValidity(value string) error {
_, err := strconv.Unquote(`"` + value + `"`)
return err
}
func makeServiceKey(rule, ingressName string) (string, error) {
@ -608,50 +228,6 @@ func shouldProcessIngress(ingressClass string, ingressClassAnnotation string) bo
(len(ingressClass) == 0 && ingressClassAnnotation == traefikDefaultIngressClass)
}
func getTLSHTTP(ctx context.Context, ingressRoute *v1alpha1.IngressRoute, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
if ingressRoute.Spec.TLS == nil {
return nil
}
if ingressRoute.Spec.TLS.SecretName == "" {
log.FromContext(ctx).Debugf("Skipping TLS sub-section: No secret name provided")
return nil
}
configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace)
if err != nil {
return err
}
tlsConfigs[configKey] = tlsConf
}
return nil
}
func getTLSTCP(ctx context.Context, ingressRoute *v1alpha1.IngressRouteTCP, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error {
if ingressRoute.Spec.TLS == nil {
return nil
}
if ingressRoute.Spec.TLS.SecretName == "" {
log.FromContext(ctx).Debugf("Skipping TLS sub-section for TCP: No secret name provided")
return nil
}
configKey := ingressRoute.Namespace + "/" + ingressRoute.Spec.TLS.SecretName
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := getTLS(k8sClient, ingressRoute.Spec.TLS.SecretName, ingressRoute.Namespace)
if err != nil {
return err
}
tlsConfigs[configKey] = tlsConf
}
return nil
}
func getTLS(k8sClient Client, secretName, namespace string) (*tls.CertAndStores, error) {
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
if err != nil {