2019-08-26 08:30:05 +00:00
package crd
import (
"context"
"errors"
"fmt"
2020-12-04 19:56:04 +00:00
"net"
"strconv"
2019-08-26 08:30:05 +00:00
"strings"
2020-09-16 13:46:04 +00:00
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/tls"
2019-08-26 08:30:05 +00:00
corev1 "k8s.io/api/core/v1"
2021-01-15 14:54:04 +00:00
"k8s.io/apimachinery/pkg/util/intstr"
2019-08-26 08:30:05 +00:00
)
2019-11-14 18:28:04 +00:00
const (
roundRobinStrategy = "RoundRobin"
httpsProtocol = "https"
httpProtocol = "http"
)
2019-08-26 08:30:05 +00:00
func ( p * Provider ) loadIngressRouteConfiguration ( ctx context . Context , client Client , tlsConfigs map [ string ] * tls . CertAndStores ) * dynamic . HTTPConfiguration {
conf := & dynamic . HTTPConfiguration {
2020-09-11 13:40:03 +00:00
Routers : map [ string ] * dynamic . Router { } ,
Middlewares : map [ string ] * dynamic . Middleware { } ,
Services : map [ string ] * dynamic . Service { } ,
ServersTransports : map [ string ] * dynamic . ServersTransport { } ,
2019-08-26 08:30:05 +00:00
}
for _ , ingressRoute := range client . GetIngressRoutes ( ) {
ctxRt := log . With ( ctx , log . Str ( "ingress" , ingressRoute . Name ) , log . Str ( "namespace" , ingressRoute . Namespace ) )
logger := log . FromContext ( ctxRt )
// 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
}
2021-07-13 10:54:09 +00:00
cb := configBuilder { client : client , allowCrossNamespace : p . AllowCrossNamespace , allowExternalNameServices : p . AllowExternalNameServices }
2020-12-10 13:58:04 +00:00
2019-08-26 08:30:05 +00:00
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
}
2019-11-14 18:28:04 +00:00
serviceKey , err := makeServiceKey ( route . Match , ingressName )
2019-08-26 08:30:05 +00:00
if err != nil {
logger . Error ( err )
continue
}
2020-12-10 13:58:04 +00:00
mds , err := p . makeMiddlewareKeys ( ctx , ingressRoute . Namespace , route . Middlewares )
if err != nil {
logger . Errorf ( "Failed to create middleware keys: %v" , err )
continue
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
normalized := provider . Normalize ( makeID ( ingressRoute . Namespace , serviceKey ) )
serviceName := normalized
if len ( route . Services ) > 1 {
spec := v1alpha1 . ServiceSpec {
Weighted : & v1alpha1 . WeightedRoundRobin {
Services : route . Services ,
} ,
}
errBuild := cb . buildServicesLB ( ctx , ingressRoute . Namespace , spec , serviceName , conf . Services )
if errBuild != nil {
2020-02-12 17:28:05 +00:00
logger . Error ( errBuild )
2019-11-14 18:28:04 +00:00
continue
}
} else if len ( route . Services ) == 1 {
fullName , serversLB , err := cb . nameAndService ( ctx , ingressRoute . Namespace , route . Services [ 0 ] . LoadBalancerSpec )
if err != nil {
logger . Error ( err )
continue
}
if serversLB != nil {
conf . Services [ serviceName ] = serversLB
} else {
serviceName = fullName
}
}
conf . Routers [ normalized ] = & dynamic . Router {
2019-08-26 08:30:05 +00:00
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 ,
2019-09-09 11:52:04 +00:00
Domains : ingressRoute . Spec . TLS . Domains ,
2019-08-26 08:30:05 +00:00
}
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
2019-11-14 18:28:04 +00:00
if ! strings . Contains ( tlsOptionsName , providerNamespaceSeparator ) {
2019-08-26 08:30:05 +00:00
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
}
2019-11-14 18:28:04 +00:00
conf . Routers [ normalized ] . TLS = tlsConf
2019-08-26 08:30:05 +00:00
}
}
}
return conf
}
2020-12-10 13:58:04 +00:00
func ( p * Provider ) makeMiddlewareKeys ( ctx context . Context , ingRouteNamespace string , middlewares [ ] v1alpha1 . MiddlewareRef ) ( [ ] string , error ) {
var mds [ ] string
for _ , mi := range middlewares {
2021-08-05 15:42:08 +00:00
name := mi . Name
if ! p . AllowCrossNamespace && strings . HasSuffix ( mi . Name , providerNamespaceSeparator + providerName ) {
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
// if the provider namespace kubernetescrd is used,
// we don't allow this format to avoid cross namespace references.
return nil , fmt . Errorf ( "invalid reference to middleware %s: with crossnamespace disallowed, the namespace field needs to be explicitly specified" , mi . Name )
}
if strings . Contains ( name , providerNamespaceSeparator ) {
2020-12-10 13:58:04 +00:00
if len ( mi . Namespace ) > 0 {
log . FromContext ( ctx ) .
WithField ( log . MiddlewareName , mi . Name ) .
Warnf ( "namespace %q is ignored in cross-provider context" , mi . Namespace )
}
2021-08-05 15:42:08 +00:00
mds = append ( mds , name )
2020-12-10 13:58:04 +00:00
continue
}
ns := ingRouteNamespace
if len ( mi . Namespace ) > 0 {
if ! isNamespaceAllowed ( p . AllowCrossNamespace , ingRouteNamespace , mi . Namespace ) {
return nil , fmt . Errorf ( "middleware %s/%s is not in the IngressRoute namespace %s" , mi . Namespace , mi . Name , ingRouteNamespace )
}
ns = mi . Namespace
}
2021-08-05 15:42:08 +00:00
mds = append ( mds , makeID ( ns , name ) )
2020-12-10 13:58:04 +00:00
}
return mds , nil
}
2019-11-14 18:28:04 +00:00
type configBuilder struct {
2021-07-13 10:54:09 +00:00
client Client
allowCrossNamespace bool
allowExternalNameServices bool
2019-11-14 18:28:04 +00:00
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
// and adds it to the given conf map.
func ( c configBuilder ) buildTraefikService ( ctx context . Context , tService * v1alpha1 . TraefikService , conf map [ string ] * dynamic . Service ) error {
id := provider . Normalize ( makeID ( tService . Namespace , tService . Name ) )
if tService . Spec . Weighted != nil {
return c . buildServicesLB ( ctx , tService . Namespace , tService . Spec , id , conf )
} else if tService . Spec . Mirroring != nil {
return c . buildMirroring ( ctx , tService , id , conf )
}
return errors . New ( "unspecified service type" )
}
// buildServicesLB creates the configuration for the load-balancer of services named id, and defined in tService.
// It adds it to the given conf map.
func ( c configBuilder ) buildServicesLB ( ctx context . Context , namespace string , tService v1alpha1 . ServiceSpec , id string , conf map [ string ] * dynamic . Service ) error {
var wrrServices [ ] dynamic . WRRService
for _ , service := range tService . Weighted . Services {
fullName , k8sService , err := c . nameAndService ( ctx , namespace , service . LoadBalancerSpec )
if err != nil {
return err
}
if k8sService != nil {
conf [ fullName ] = k8sService
}
weight := service . Weight
if weight == nil {
weight = func ( i int ) * int { return & i } ( 1 )
}
wrrServices = append ( wrrServices , dynamic . WRRService {
Name : fullName ,
Weight : weight ,
} )
}
conf [ id ] = & dynamic . Service {
Weighted : & dynamic . WeightedRoundRobin {
Services : wrrServices ,
Sticky : tService . Weighted . Sticky ,
} ,
}
return nil
}
// buildMirroring creates the configuration for the mirroring service named id, and defined by tService.
// It adds it to the given conf map.
func ( c configBuilder ) buildMirroring ( ctx context . Context , tService * v1alpha1 . TraefikService , id string , conf map [ string ] * dynamic . Service ) error {
fullNameMain , k8sService , err := c . nameAndService ( ctx , tService . Namespace , tService . Spec . Mirroring . LoadBalancerSpec )
if err != nil {
return err
}
if k8sService != nil {
conf [ fullNameMain ] = k8sService
}
var mirrorServices [ ] dynamic . MirrorService
for _ , mirror := range tService . Spec . Mirroring . Mirrors {
mirroredName , k8sService , err := c . nameAndService ( ctx , tService . Namespace , mirror . LoadBalancerSpec )
if err != nil {
return err
}
if k8sService != nil {
conf [ mirroredName ] = k8sService
}
mirrorServices = append ( mirrorServices , dynamic . MirrorService {
Name : mirroredName ,
Percent : mirror . Percent ,
} )
}
conf [ id ] = & dynamic . Service {
Mirroring : & dynamic . Mirroring {
2020-03-05 17:03:08 +00:00
Service : fullNameMain ,
Mirrors : mirrorServices ,
MaxBodySize : tService . Spec . Mirroring . MaxBodySize ,
2019-11-14 18:28:04 +00:00
} ,
}
return nil
}
// buildServersLB creates the configuration for the load-balancer of servers defined by svc.
func ( c configBuilder ) buildServersLB ( namespace string , svc v1alpha1 . LoadBalancerSpec ) ( * dynamic . Service , error ) {
servers , err := c . loadServers ( namespace , svc )
2019-08-26 08:30:05 +00:00
if err != nil {
return nil , err
}
2019-09-13 14:46:04 +00:00
lb := & dynamic . ServersLoadBalancer { }
lb . SetDefaults ( )
lb . Servers = servers
2019-09-30 16:12:04 +00:00
2019-11-14 18:28:04 +00:00
conf := svc
lb . PassHostHeader = conf . PassHostHeader
2019-09-30 16:12:04 +00:00
if lb . PassHostHeader == nil {
passHostHeader := true
lb . PassHostHeader = & passHostHeader
2019-09-13 14:46:04 +00:00
}
2019-11-14 18:28:04 +00:00
lb . ResponseForwarding = conf . ResponseForwarding
2019-09-13 14:46:04 +00:00
2019-11-14 18:28:04 +00:00
lb . Sticky = svc . Sticky
2020-09-11 13:40:03 +00:00
lb . ServersTransport = svc . ServersTransport
2019-11-14 18:28:04 +00:00
return & dynamic . Service { LoadBalancer : lb } , nil
2019-08-26 08:30:05 +00:00
}
2020-12-10 13:58:04 +00:00
func ( c configBuilder ) loadServers ( parentNamespace string , svc v1alpha1 . LoadBalancerSpec ) ( [ ] dynamic . Server , error ) {
2019-08-26 08:30:05 +00:00
strategy := svc . Strategy
if strategy == "" {
2019-11-14 18:28:04 +00:00
strategy = roundRobinStrategy
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
if strategy != roundRobinStrategy {
return nil , fmt . Errorf ( "load balancing strategy %s is not supported" , strategy )
2019-08-26 08:30:05 +00:00
}
2020-12-10 13:58:04 +00:00
namespace := namespaceOrFallback ( svc , parentNamespace )
if ! isNamespaceAllowed ( c . allowCrossNamespace , parentNamespace , namespace ) {
return nil , fmt . Errorf ( "load balancer service %s/%s is not in the parent resource namespace %s" , svc . Namespace , svc . Name , parentNamespace )
}
2019-11-14 18:28:04 +00:00
// If the service uses explicitly the provider suffix
sanitizedName := strings . TrimSuffix ( svc . Name , providerNamespaceSeparator + providerName )
service , exists , err := c . client . GetService ( namespace , sanitizedName )
2019-08-26 08:30:05 +00:00
if err != nil {
return nil , err
}
if ! exists {
2019-11-14 18:28:04 +00:00
return nil , fmt . Errorf ( "kubernetes service not found: %s/%s" , namespace , sanitizedName )
2019-08-26 08:30:05 +00:00
}
2020-03-10 11:46:05 +00:00
svcPort , err := getServicePort ( service , svc . Port )
if err != nil {
return nil , err
2019-08-26 08:30:05 +00:00
}
var servers [ ] dynamic . Server
if service . Spec . Type == corev1 . ServiceTypeExternalName {
2021-07-13 10:54:09 +00:00
if ! c . allowExternalNameServices {
return nil , fmt . Errorf ( "externalName services not allowed: %s/%s" , namespace , sanitizedName )
}
2020-03-10 11:46:05 +00:00
protocol , err := parseServiceProtocol ( svc . Scheme , svcPort . Name , svcPort . Port )
2020-02-25 09:12:04 +00:00
if err != nil {
return nil , err
2019-12-16 20:48:03 +00:00
}
2020-12-04 19:56:04 +00:00
hostPort := net . JoinHostPort ( service . Spec . ExternalName , strconv . Itoa ( int ( svcPort . Port ) ) )
2019-11-14 18:28:04 +00:00
return append ( servers , dynamic . Server {
2020-12-04 19:56:04 +00:00
URL : fmt . Sprintf ( "%s://%s" , protocol , hostPort ) ,
2019-11-14 18:28:04 +00:00
} ) , nil
}
endpoints , endpointsExists , endpointsErr := c . client . GetEndpoints ( namespace , sanitizedName )
if endpointsErr != nil {
return nil , endpointsErr
}
if ! endpointsExists {
return nil , fmt . Errorf ( "endpoints not found for %s/%s" , namespace , sanitizedName )
}
if len ( endpoints . Subsets ) == 0 {
return nil , fmt . Errorf ( "subset not found for %s/%s" , namespace , sanitizedName )
}
var port int32
for _ , subset := range endpoints . Subsets {
for _ , p := range subset . Ports {
2020-03-10 11:46:05 +00:00
if svcPort . Name == p . Name {
2019-11-14 18:28:04 +00:00
port = p . Port
break
}
2019-10-18 09:12:05 +00:00
}
2019-11-14 18:28:04 +00:00
if port == 0 {
return nil , fmt . Errorf ( "cannot define a port for %s/%s" , namespace , sanitizedName )
2019-08-26 08:30:05 +00:00
}
2020-03-10 11:46:05 +00:00
protocol , err := parseServiceProtocol ( svc . Scheme , svcPort . Name , svcPort . Port )
2020-02-25 09:12:04 +00:00
if err != nil {
return nil , err
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
for _ , addr := range subset . Addresses {
2020-12-04 19:56:04 +00:00
hostPort := net . JoinHostPort ( addr . IP , strconv . Itoa ( int ( port ) ) )
2019-11-14 18:28:04 +00:00
servers = append ( servers , dynamic . Server {
2020-12-04 19:56:04 +00:00
URL : fmt . Sprintf ( "%s://%s" , protocol , hostPort ) ,
2019-11-14 18:28:04 +00:00
} )
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
}
2019-08-26 08:30:05 +00:00
2019-11-14 18:28:04 +00:00
return servers , nil
}
2019-08-26 08:30:05 +00:00
2019-11-14 18:28:04 +00:00
// nameAndService returns the name that should be used for the svc service in the generated config.
// In addition, if the service is a Kubernetes one,
// it generates and returns the configuration part for such a service,
// so that the caller can add it to the global config map.
2020-12-10 13:58:04 +00:00
func ( c configBuilder ) nameAndService ( ctx context . Context , parentNamespace string , service v1alpha1 . LoadBalancerSpec ) ( string , * dynamic . Service , error ) {
2019-11-14 18:28:04 +00:00
svcCtx := log . With ( ctx , log . Str ( log . ServiceName , service . Name ) )
2019-08-26 08:30:05 +00:00
2020-12-10 13:58:04 +00:00
namespace := namespaceOrFallback ( service , parentNamespace )
if ! isNamespaceAllowed ( c . allowCrossNamespace , parentNamespace , namespace ) {
return "" , nil , fmt . Errorf ( "service %s/%s not in the parent resource namespace %s" , service . Namespace , service . Name , parentNamespace )
}
2019-08-26 08:30:05 +00:00
2019-11-14 18:28:04 +00:00
switch {
case service . Kind == "" || service . Kind == "Service" :
serversLB , err := c . buildServersLB ( namespace , service )
if err != nil {
return "" , nil , err
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
2020-01-07 09:46:04 +00:00
fullName := fullServiceName ( svcCtx , namespace , service , service . Port )
2019-11-14 18:28:04 +00:00
return fullName , serversLB , nil
case service . Kind == "TraefikService" :
2021-01-15 14:54:04 +00:00
return fullServiceName ( svcCtx , namespace , service , intstr . FromInt ( 0 ) ) , nil , nil
2019-11-14 18:28:04 +00:00
default :
return "" , nil , fmt . Errorf ( "unsupported service kind %s" , service . Kind )
2019-08-26 08:30:05 +00:00
}
2019-11-14 18:28:04 +00:00
}
2019-08-26 08:30:05 +00:00
2019-11-14 18:28:04 +00:00
func splitSvcNameProvider ( name string ) ( string , string ) {
parts := strings . Split ( name , providerNamespaceSeparator )
svc := strings . Join ( parts [ : len ( parts ) - 1 ] , providerNamespaceSeparator )
pvd := parts [ len ( parts ) - 1 ]
return svc , pvd
}
2021-01-15 14:54:04 +00:00
func fullServiceName ( ctx context . Context , namespace string , service v1alpha1 . LoadBalancerSpec , port intstr . IntOrString ) string {
if ( port . Type == intstr . Int && port . IntVal != 0 ) || ( port . Type == intstr . String && port . StrVal != "" ) {
return provider . Normalize ( fmt . Sprintf ( "%s-%s-%s" , namespace , service . Name , & port ) )
2019-11-14 18:28:04 +00:00
}
2020-01-07 09:46:04 +00:00
if ! strings . Contains ( service . Name , providerNamespaceSeparator ) {
return provider . Normalize ( fmt . Sprintf ( "%s-%s" , namespace , service . Name ) )
2019-11-14 18:28:04 +00:00
}
2020-01-07 09:46:04 +00:00
name , pName := splitSvcNameProvider ( service . Name )
2019-11-14 18:28:04 +00:00
if pName == providerName {
return provider . Normalize ( fmt . Sprintf ( "%s-%s" , namespace , name ) )
}
2020-01-07 09:46:04 +00:00
if service . Namespace != "" {
log . FromContext ( ctx ) . Warnf ( "namespace %q is ignored in cross-provider context" , service . Namespace )
2019-11-14 18:28:04 +00:00
}
return provider . Normalize ( name ) + providerNamespaceSeparator + pName
}
func namespaceOrFallback ( lb v1alpha1 . LoadBalancerSpec , fallback string ) string {
if lb . Namespace != "" {
return lb . Namespace
}
return fallback
2019-08-26 08:30:05 +00:00
}
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 ( "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
}
2020-02-25 09:12:04 +00:00
// parseServiceProtocol parses the scheme, port name, and number to determine the correct protocol.
// an error is returned if the scheme provided is invalid.
2020-07-07 12:42:03 +00:00
func parseServiceProtocol ( providedScheme , portName string , portNumber int32 ) ( string , error ) {
2020-02-25 09:12:04 +00:00
switch providedScheme {
case httpProtocol , httpsProtocol , "h2c" :
return providedScheme , nil
case "" :
if portNumber == 443 || strings . HasPrefix ( portName , httpsProtocol ) {
return httpsProtocol , nil
}
return httpProtocol , nil
}
return "" , fmt . Errorf ( "invalid scheme %q specified" , providedScheme )
}