2019-03-14 14:56:06 +00:00
package ingress
2019-02-21 22:08:05 +00:00
import (
"context"
2021-01-05 11:26:04 +00:00
"crypto/sha256"
2019-04-01 13:30:07 +00:00
"errors"
2019-02-21 22:08:05 +00:00
"fmt"
"math"
2020-12-04 19:56:04 +00:00
"net"
2019-02-21 22:08:05 +00:00
"os"
2022-11-28 14:48:05 +00:00
"regexp"
2024-02-19 14:44:03 +00:00
"slices"
2019-02-21 22:08:05 +00:00
"sort"
2020-12-04 19:56:04 +00:00
"strconv"
2019-02-21 22:08:05 +00:00
"strings"
"time"
2020-02-26 09:36:05 +00:00
"github.com/cenkalti/backoff/v4"
2019-10-25 13:46:05 +00:00
"github.com/mitchellh/hashstructure"
2022-11-21 17:36:05 +00:00
"github.com/rs/zerolog/log"
2020-08-17 16:04:03 +00:00
ptypes "github.com/traefik/paerser/types"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/job"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/provider"
2023-05-17 09:07:09 +00:00
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/tls"
2024-01-11 16:06:06 +00:00
"github.com/traefik/traefik/v3/pkg/types"
2019-02-21 22:08:05 +00:00
corev1 "k8s.io/api/core/v1"
2023-04-17 08:56:36 +00:00
netv1 "k8s.io/api/networking/v1"
2019-02-21 22:08:05 +00:00
"k8s.io/apimachinery/pkg/labels"
)
const (
2020-07-15 17:18:03 +00:00
annotationKubernetesIngressClass = "kubernetes.io/ingress.class"
traefikDefaultIngressClass = "traefik"
traefikDefaultIngressClassController = "traefik.io/ingress-controller"
defaultPathMatcher = "PathPrefix"
2019-02-21 22:08:05 +00:00
)
// Provider holds configurations of the provider.
type Provider struct {
2024-01-11 16:06:06 +00:00
Endpoint string ` description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" `
Token types . FileOrContent ` description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false" `
CertAuthFilePath string ` description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty" `
Namespaces [ ] string ` description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true" `
LabelSelector string ` description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true" `
IngressClass string ` description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true" `
IngressEndpoint * EndpointIngress ` description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true" `
ThrottleDuration ptypes . Duration ` description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true" `
AllowEmptyServices bool ` description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true" `
AllowExternalNameServices bool ` description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true" `
2024-08-01 13:50:04 +00:00
// Deprecated: please use DisableClusterScopeResources.
DisableIngressClassLookup bool ` description:"Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources)." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true" `
DisableClusterScopeResources bool ` description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true" `
NativeLBByDefault bool ` description:"Defines whether to use Native Kubernetes load-balancing mode by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true" `
2023-05-15 14:38:05 +00:00
lastConfiguration safe . Safe
routerTransform k8s . RouterTransform
}
func ( p * Provider ) SetRouterTransform ( routerTransform k8s . RouterTransform ) {
p . routerTransform = routerTransform
}
func ( p * Provider ) applyRouterTransform ( ctx context . Context , rt * dynamic . Router , ingress * netv1 . Ingress ) {
if p . routerTransform == nil {
return
}
2024-03-15 08:24:03 +00:00
err := p . routerTransform . Apply ( ctx , rt , ingress )
2023-05-15 14:38:05 +00:00
if err != nil {
2023-05-17 09:07:09 +00:00
log . Ctx ( ctx ) . Error ( ) . Err ( err ) . Msg ( "Apply router transform" )
2023-05-15 14:38:05 +00:00
}
2019-02-21 22:08:05 +00:00
}
2020-05-11 10:06:07 +00:00
// EndpointIngress holds the endpoint information for the Kubernetes provider.
2019-03-18 09:10:04 +00:00
type EndpointIngress struct {
2019-07-01 09:30:05 +00:00
IP string ` description:"IP used for Kubernetes Ingress endpoints." json:"ip,omitempty" toml:"ip,omitempty" yaml:"ip,omitempty" `
Hostname string ` description:"Hostname used for Kubernetes Ingress endpoints." json:"hostname,omitempty" toml:"hostname,omitempty" yaml:"hostname,omitempty" `
PublishedService string ` description:"Published Kubernetes Service to copy status from." json:"publishedService,omitempty" toml:"publishedService,omitempty" yaml:"publishedService,omitempty" `
2019-03-18 09:10:04 +00:00
}
2020-11-19 23:18:04 +00:00
func ( p * Provider ) newK8sClient ( ctx context . Context ) ( * clientWrapper , error ) {
_ , err := labels . Parse ( p . LabelSelector )
2019-02-21 22:08:05 +00:00
if err != nil {
2020-11-19 23:18:04 +00:00
return nil , fmt . Errorf ( "invalid ingress label selector: %q" , p . LabelSelector )
2019-02-21 22:08:05 +00:00
}
2019-03-14 14:56:06 +00:00
2022-11-21 17:36:05 +00:00
logger := log . Ctx ( ctx )
2019-03-14 14:56:06 +00:00
2022-11-21 17:36:05 +00:00
logger . Info ( ) . Msgf ( "ingress label selector is: %q" , p . LabelSelector )
2019-02-21 22:08:05 +00:00
withEndpoint := ""
if p . Endpoint != "" {
withEndpoint = fmt . Sprintf ( " with endpoint %v" , p . Endpoint )
}
2019-03-14 14:56:06 +00:00
var cl * clientWrapper
2019-03-11 13:54:05 +00:00
switch {
case os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" && os . Getenv ( "KUBERNETES_SERVICE_PORT" ) != "" :
2022-11-21 17:36:05 +00:00
logger . Info ( ) . Msgf ( "Creating in-cluster Provider client%s" , withEndpoint )
2019-02-21 22:08:05 +00:00
cl , err = newInClusterClient ( p . Endpoint )
2019-03-11 13:54:05 +00:00
case os . Getenv ( "KUBECONFIG" ) != "" :
2022-11-21 17:36:05 +00:00
logger . Info ( ) . Msgf ( "Creating cluster-external Provider client from KUBECONFIG %s" , os . Getenv ( "KUBECONFIG" ) )
2019-03-11 13:54:05 +00:00
cl , err = newExternalClusterClientFromFile ( os . Getenv ( "KUBECONFIG" ) )
default :
2022-11-21 17:36:05 +00:00
logger . Info ( ) . Msgf ( "Creating cluster-external Provider client%s" , withEndpoint )
2024-01-11 16:06:06 +00:00
cl , err = newExternalClusterClient ( p . Endpoint , p . CertAuthFilePath , p . Token )
2019-02-21 22:08:05 +00:00
}
2020-11-19 23:18:04 +00:00
if err != nil {
return nil , err
2019-02-21 22:08:05 +00:00
}
2020-11-19 23:18:04 +00:00
cl . ingressLabelSelector = p . LabelSelector
2024-08-01 13:50:04 +00:00
cl . disableIngressClassInformer = p . DisableIngressClassLookup || p . DisableClusterScopeResources
cl . disableClusterScopeInformer = p . DisableClusterScopeResources
2020-11-19 23:18:04 +00:00
return cl , nil
2019-02-21 22:08:05 +00:00
}
// Init the provider.
func ( p * Provider ) Init ( ) error {
2019-03-27 14:02:06 +00:00
return nil
2019-02-21 22:08:05 +00:00
}
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
2019-07-10 07:26:04 +00:00
func ( p * Provider ) Provide ( configurationChan chan <- dynamic . Message , pool * safe . Pool ) error {
2022-11-21 17:36:05 +00:00
logger := log . With ( ) . Str ( logs . ProviderName , "kubernetes" ) . Logger ( )
ctxLog := logger . WithContext ( context . Background ( ) )
2019-02-21 22:08:05 +00:00
2020-11-19 23:18:04 +00:00
k8sClient , err := p . newK8sClient ( ctxLog )
2019-02-21 22:08:05 +00:00
if err != nil {
return err
}
2021-07-13 10:54:09 +00:00
if p . AllowExternalNameServices {
2024-05-13 07:06:03 +00:00
logger . Info ( ) . Msg ( "ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)" )
2021-07-13 10:54:09 +00:00
}
2020-02-03 16:56:04 +00:00
pool . GoCtx ( func ( ctxPool context . Context ) {
2019-02-21 22:08:05 +00:00
operation := func ( ) error {
2020-02-03 16:56:04 +00:00
eventsChan , err := k8sClient . WatchAll ( p . Namespaces , ctxPool . Done ( ) )
2019-02-21 22:08:05 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Err ( err ) . Msg ( "Error watching kubernetes events" )
2019-02-21 22:08:05 +00:00
timer := time . NewTimer ( 1 * time . Second )
select {
case <- timer . C :
return err
2020-02-03 16:56:04 +00:00
case <- ctxPool . Done ( ) :
2019-02-21 22:08:05 +00:00
return nil
}
}
2019-03-14 14:56:06 +00:00
2019-08-30 10:16:04 +00:00
throttleDuration := time . Duration ( p . ThrottleDuration )
2020-02-03 16:56:04 +00:00
throttledChan := throttleEvents ( ctxLog , throttleDuration , pool , eventsChan )
2019-08-31 12:10:04 +00:00
if throttledChan != nil {
eventsChan = throttledChan
}
2019-08-30 10:16:04 +00:00
2019-02-21 22:08:05 +00:00
for {
select {
2020-02-03 16:56:04 +00:00
case <- ctxPool . Done ( ) :
2019-02-21 22:08:05 +00:00
return nil
2019-08-31 12:10:04 +00:00
case event := <- eventsChan :
2019-08-30 10:16:04 +00:00
// Note that event is the *first* event that came in during this
// throttling interval -- if we're hitting our throttle, we may have
// dropped events. This is fine, because we don't treat different
// event types differently. But if we do in the future, we'll need to
// track more information about the dropped events.
2019-02-21 22:08:05 +00:00
conf := p . loadConfigurationFromIngresses ( ctxLog , k8sClient )
2019-10-25 13:46:05 +00:00
confHash , err := hashstructure . Hash ( conf , nil )
switch {
case err != nil :
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Msg ( "Unable to hash the configuration" )
2019-10-25 13:46:05 +00:00
case p . lastConfiguration . Get ( ) == confHash :
2022-11-21 17:36:05 +00:00
logger . Debug ( ) . Msgf ( "Skipping Kubernetes event kind %T" , event )
2019-10-25 13:46:05 +00:00
default :
p . lastConfiguration . Set ( confHash )
2019-07-10 07:26:04 +00:00
configurationChan <- dynamic . Message {
2019-02-21 22:08:05 +00:00
ProviderName : "kubernetes" ,
Configuration : conf ,
}
}
2019-08-30 10:16:04 +00:00
// If we're throttling, we sleep here for the throttle duration to
// enforce that we don't refresh faster than our throttle. time.Sleep
// returns immediately if p.ThrottleDuration is 0 (no throttle).
time . Sleep ( throttleDuration )
2019-02-21 22:08:05 +00:00
}
}
}
notify := func ( err error , time time . Duration ) {
2022-11-30 08:50:05 +00:00
logger . Error ( ) . Err ( err ) . Msgf ( "Provider error, retrying in %s" , time )
2019-02-21 22:08:05 +00:00
}
2020-02-03 16:56:04 +00:00
err := backoff . RetryNotify ( safe . OperationWithRecover ( operation ) , backoff . WithContext ( job . NewBackOff ( backoff . NewExponentialBackOff ( ) ) , ctxPool ) , notify )
2019-02-21 22:08:05 +00:00
if err != nil {
2022-11-30 08:50:05 +00:00
logger . Error ( ) . Err ( err ) . Msg ( "Cannot retrieve data" )
2019-02-21 22:08:05 +00:00
}
} )
return nil
}
2019-07-10 07:26:04 +00:00
func ( p * Provider ) loadConfigurationFromIngresses ( ctx context . Context , client Client ) * dynamic . Configuration {
conf := & dynamic . Configuration {
HTTP : & dynamic . HTTPConfiguration {
Routers : map [ string ] * dynamic . Router { } ,
Middlewares : map [ string ] * dynamic . Middleware { } ,
Services : map [ string ] * dynamic . Service { } ,
2019-03-14 08:30:04 +00:00
} ,
2019-02-21 22:08:05 +00:00
}
2023-04-17 08:56:36 +00:00
var ingressClasses [ ] * netv1 . IngressClass
2020-07-15 17:18:03 +00:00
2023-06-20 08:26:05 +00:00
if ! p . DisableIngressClassLookup {
2021-01-28 14:08:04 +00:00
ics , err := client . GetIngressClasses ( )
2020-07-15 17:18:03 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
log . Ctx ( ctx ) . Warn ( ) . Err ( err ) . Msg ( "Failed to list ingress classes" )
2020-07-15 17:18:03 +00:00
}
2021-03-02 20:34:03 +00:00
if p . IngressClass != "" {
ingressClasses = filterIngressClassByName ( p . IngressClass , ics )
} else {
ingressClasses = ics
}
2020-07-15 17:18:03 +00:00
}
2019-02-21 22:08:05 +00:00
ingresses := client . GetIngresses ( )
2020-01-14 14:48:06 +00:00
certConfigs := make ( map [ string ] * tls . CertAndStores )
2019-02-21 22:08:05 +00:00
for _ , ingress := range ingresses {
2022-11-21 17:36:05 +00:00
logger := log . Ctx ( ctx ) . With ( ) . Str ( "ingress" , ingress . Name ) . Str ( "namespace" , ingress . Namespace ) . Logger ( )
2024-06-04 21:58:38 +00:00
ctxIngress := logger . WithContext ( ctx )
2019-02-21 22:08:05 +00:00
2021-01-28 14:08:04 +00:00
if ! p . shouldProcessIngress ( ingress , ingressClasses ) {
2019-02-21 22:08:05 +00:00
continue
}
2020-01-14 14:48:06 +00:00
rtConfig , err := parseRouterConfig ( ingress . Annotations )
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Err ( err ) . Msg ( "Failed to parse annotations" )
2020-01-14 14:48:06 +00:00
continue
}
2024-05-27 07:46:08 +00:00
err = getCertificates ( ctxIngress , ingress , client , certConfigs )
2019-02-21 22:08:05 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Err ( err ) . Msg ( "Error configuring TLS" )
2019-02-21 22:08:05 +00:00
}
2021-03-15 10:16:04 +00:00
if len ( ingress . Spec . Rules ) == 0 && ingress . Spec . DefaultBackend != nil {
2020-01-14 14:48:06 +00:00
if _ , ok := conf . HTTP . Services [ "default-backend" ] ; ok {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Msg ( "The default backend already exists." )
2020-01-14 14:48:06 +00:00
continue
}
2019-02-21 22:08:05 +00:00
2021-07-13 16:12:29 +00:00
service , err := p . loadService ( client , ingress . Namespace , * ingress . Spec . DefaultBackend )
2020-01-14 14:48:06 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) .
Str ( "serviceName" , ingress . Spec . DefaultBackend . Service . Name ) .
Str ( "servicePort" , ingress . Spec . DefaultBackend . Service . Port . String ( ) ) .
Err ( err ) .
Msg ( "Cannot create service" )
2020-01-14 14:48:06 +00:00
continue
}
2019-02-21 22:08:05 +00:00
2021-05-06 16:12:10 +00:00
if len ( service . LoadBalancer . Servers ) == 0 && ! p . AllowEmptyServices {
2022-11-21 17:36:05 +00:00
logger . Error ( ) .
Str ( "serviceName" , ingress . Spec . DefaultBackend . Service . Name ) .
Str ( "servicePort" , ingress . Spec . DefaultBackend . Service . Port . String ( ) ) .
Msg ( "Skipping service: no endpoints found" )
2021-05-06 16:12:10 +00:00
continue
}
2020-01-14 14:48:06 +00:00
rt := & dynamic . Router {
2024-05-15 08:46:04 +00:00
Rule : "PathPrefix(`/`)" ,
RuleSyntax : "v3" ,
Priority : math . MinInt32 ,
Service : "default-backend" ,
2020-01-14 14:48:06 +00:00
}
2019-02-21 22:08:05 +00:00
2020-01-14 14:48:06 +00:00
if rtConfig != nil && rtConfig . Router != nil {
rt . EntryPoints = rtConfig . Router . EntryPoints
rt . Middlewares = rtConfig . Router . Middlewares
rt . TLS = rtConfig . Router . TLS
2019-02-21 22:08:05 +00:00
}
2020-01-14 14:48:06 +00:00
2024-05-27 07:46:08 +00:00
p . applyRouterTransform ( ctxIngress , rt , ingress )
2023-05-15 14:38:05 +00:00
2020-01-14 14:48:06 +00:00
conf . HTTP . Routers [ "default-router" ] = rt
conf . HTTP . Services [ "default-backend" ] = service
2019-02-21 22:08:05 +00:00
}
2020-01-07 15:26:08 +00:00
2021-01-05 11:26:04 +00:00
routers := map [ string ] [ ] * dynamic . Router { }
2019-02-21 22:08:05 +00:00
for _ , rule := range ingress . Spec . Rules {
2020-01-22 02:44:04 +00:00
if err := p . updateIngressStatus ( ingress , client ) ; err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Err ( err ) . Msg ( "Error while updating ingress status" )
2020-01-22 02:44:04 +00:00
}
2019-02-21 22:08:05 +00:00
2020-01-22 02:44:04 +00:00
if rule . HTTP == nil {
continue
}
2019-02-21 22:08:05 +00:00
2020-01-22 02:44:04 +00:00
for _ , pa := range rule . HTTP . Paths {
2021-07-13 10:54:09 +00:00
service , err := p . loadService ( client , ingress . Namespace , pa . Backend )
2020-01-22 02:44:04 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) .
Str ( "serviceName" , pa . Backend . Service . Name ) .
Str ( "servicePort" , pa . Backend . Service . Port . String ( ) ) .
Err ( err ) .
Msg ( "Cannot create service" )
2020-01-22 02:44:04 +00:00
continue
2019-08-14 17:16:06 +00:00
}
2020-01-07 15:26:08 +00:00
2021-05-06 16:12:10 +00:00
if len ( service . LoadBalancer . Servers ) == 0 && ! p . AllowEmptyServices {
2022-11-21 17:36:05 +00:00
logger . Error ( ) .
Str ( "serviceName" , pa . Backend . Service . Name ) .
Str ( "servicePort" , pa . Backend . Service . Port . String ( ) ) .
Msg ( "Skipping service: no endpoints found" )
2021-05-06 16:12:10 +00:00
continue
}
2021-03-15 10:16:04 +00:00
portString := pa . Backend . Service . Port . Name
if len ( pa . Backend . Service . Port . Name ) == 0 {
2023-11-17 00:50:06 +00:00
portString = strconv . Itoa ( int ( pa . Backend . Service . Port . Number ) )
2021-03-15 10:16:04 +00:00
}
serviceName := provider . Normalize ( ingress . Namespace + "-" + pa . Backend . Service . Name + "-" + portString )
2020-01-22 02:44:04 +00:00
conf . HTTP . Services [ serviceName ] = service
2023-05-15 14:38:05 +00:00
rt := loadRouter ( rule , pa , rtConfig , serviceName )
2024-05-27 07:46:08 +00:00
p . applyRouterTransform ( ctxIngress , rt , ingress )
2023-05-15 14:38:05 +00:00
2022-08-04 08:22:08 +00:00
routerKey := strings . TrimPrefix ( provider . Normalize ( ingress . Namespace + "-" + ingress . Name + "-" + rule . Host + pa . Path ) , "-" )
2023-05-15 14:38:05 +00:00
routers [ routerKey ] = append ( routers [ routerKey ] , rt )
2021-01-05 11:26:04 +00:00
}
}
for routerKey , conflictingRouters := range routers {
if len ( conflictingRouters ) == 1 {
conf . HTTP . Routers [ routerKey ] = conflictingRouters [ 0 ]
continue
}
2022-11-21 17:36:05 +00:00
logger . Debug ( ) . Msgf ( "Multiple routers are defined with the same key %q, generating hashes to avoid conflicts" , routerKey )
2021-01-05 11:26:04 +00:00
for _ , router := range conflictingRouters {
key , err := makeRouterKeyWithHash ( routerKey , router . Rule )
if err != nil {
2022-11-21 17:36:05 +00:00
logger . Error ( ) . Err ( err ) . Send ( )
2021-01-05 11:26:04 +00:00
continue
}
2020-03-18 12:30:04 +00:00
2021-01-05 11:26:04 +00:00
conf . HTTP . Routers [ key ] = router
2019-03-18 09:10:04 +00:00
}
2019-02-21 22:08:05 +00:00
}
}
2020-01-14 14:48:06 +00:00
certs := getTLSConfig ( certConfigs )
2019-06-27 21:58:03 +00:00
if len ( certs ) > 0 {
2019-07-10 07:26:04 +00:00
conf . TLS = & dynamic . TLSConfiguration {
2019-06-27 21:58:03 +00:00
Certificates : certs ,
}
}
2019-02-21 22:08:05 +00:00
return conf
}
2023-04-17 08:56:36 +00:00
func ( p * Provider ) updateIngressStatus ( ing * netv1 . Ingress , k8sClient Client ) error {
2020-07-15 17:18:03 +00:00
// Only process if an EndpointIngress has been configured.
2020-01-14 14:48:06 +00:00
if p . IngressEndpoint == nil {
return nil
}
if len ( p . IngressEndpoint . PublishedService ) == 0 {
if len ( p . IngressEndpoint . IP ) == 0 && len ( p . IngressEndpoint . Hostname ) == 0 {
return errors . New ( "publishedService or ip or hostname must be defined" )
}
2023-04-03 08:06:06 +00:00
return k8sClient . UpdateIngressStatus ( ing , [ ] netv1 . IngressLoadBalancerIngress { { IP : p . IngressEndpoint . IP , Hostname : p . IngressEndpoint . Hostname } } )
2020-01-14 14:48:06 +00:00
}
serviceInfo := strings . Split ( p . IngressEndpoint . PublishedService , "/" )
if len ( serviceInfo ) != 2 {
return fmt . Errorf ( "invalid publishedService format (expected 'namespace/service' format): %s" , p . IngressEndpoint . PublishedService )
}
2020-01-16 09:14:06 +00:00
2020-01-14 14:48:06 +00:00
serviceNamespace , serviceName := serviceInfo [ 0 ] , serviceInfo [ 1 ]
service , exists , err := k8sClient . GetService ( serviceNamespace , serviceName )
if err != nil {
2020-05-11 10:06:07 +00:00
return fmt . Errorf ( "cannot get service %s, received error: %w" , p . IngressEndpoint . PublishedService , err )
2020-01-14 14:48:06 +00:00
}
if exists && service . Status . LoadBalancer . Ingress == nil {
// service exists, but has no Load Balancer status
2022-11-21 17:36:05 +00:00
log . Debug ( ) . Msgf ( "Skipping updating Ingress %s/%s due to service %s having no status set" , ing . Namespace , ing . Name , p . IngressEndpoint . PublishedService )
2020-01-14 14:48:06 +00:00
return nil
}
if ! exists {
return fmt . Errorf ( "missing service: %s" , p . IngressEndpoint . PublishedService )
}
2023-04-03 08:06:06 +00:00
ingresses , err := convertSlice [ netv1 . IngressLoadBalancerIngress ] ( service . Status . LoadBalancer . Ingress )
2023-03-27 10:14:05 +00:00
if err != nil {
return err
}
return k8sClient . UpdateIngressStatus ( ing , ingresses )
2020-01-14 14:48:06 +00:00
}
2023-04-17 08:56:36 +00:00
func ( p * Provider ) shouldProcessIngress ( ingress * netv1 . Ingress , ingressClasses [ ] * netv1 . IngressClass ) bool {
2020-07-28 15:50:04 +00:00
// configuration through the new kubernetes ingressClass
if ingress . Spec . IngressClassName != nil {
2024-02-19 14:44:03 +00:00
return slices . ContainsFunc ( ingressClasses , func ( ic * netv1 . IngressClass ) bool {
return * ingress . Spec . IngressClassName == ic . ObjectMeta . Name
} )
2020-07-28 15:50:04 +00:00
}
2021-01-28 14:08:04 +00:00
return p . IngressClass == ingress . Annotations [ annotationKubernetesIngressClass ] ||
len ( p . IngressClass ) == 0 && ingress . Annotations [ annotationKubernetesIngressClass ] == traefikDefaultIngressClass
2020-07-15 17:18:03 +00:00
}
2020-02-03 10:24:06 +00:00
func buildHostRule ( host string ) string {
if strings . HasPrefix ( host , "*." ) {
2022-11-28 14:48:05 +00:00
host = strings . Replace ( regexp . QuoteMeta ( host ) , ` \*\. ` , ` [a-zA-Z0-9-]+\. ` , 1 )
return fmt . Sprintf ( "HostRegexp(`^%s$`)" , host )
2020-02-03 10:24:06 +00:00
}
2022-11-28 14:48:05 +00:00
return fmt . Sprintf ( "Host(`%s`)" , host )
2020-02-03 10:24:06 +00:00
}
2023-04-17 08:56:36 +00:00
func getCertificates ( ctx context . Context , ingress * netv1 . Ingress , k8sClient Client , tlsConfigs map [ string ] * tls . CertAndStores ) error {
2019-02-21 22:08:05 +00:00
for _ , t := range ingress . Spec . TLS {
if t . SecretName == "" {
2022-11-21 17:36:05 +00:00
log . Ctx ( ctx ) . Debug ( ) . Msg ( "Skipping TLS sub-section: No secret name provided" )
2019-02-21 22:08:05 +00:00
continue
}
2019-09-13 18:44:04 +00:00
configKey := ingress . Namespace + "-" + t . SecretName
2019-02-21 22:08:05 +00:00
if _ , tlsExists := tlsConfigs [ configKey ] ; ! tlsExists {
secret , exists , err := k8sClient . GetSecret ( ingress . Namespace , t . SecretName )
if err != nil {
2020-05-11 10:06:07 +00:00
return fmt . Errorf ( "failed to fetch secret %s/%s: %w" , ingress . Namespace , t . SecretName , err )
2019-02-21 22:08:05 +00:00
}
if ! exists {
return fmt . Errorf ( "secret %s/%s does not exist" , ingress . Namespace , t . SecretName )
}
cert , key , err := getCertificateBlocks ( secret , ingress . Namespace , t . SecretName )
if err != nil {
return err
}
2019-06-27 21:58:03 +00:00
tlsConfigs [ configKey ] = & tls . CertAndStores {
Certificate : tls . Certificate {
2024-01-11 16:06:06 +00:00
CertFile : types . FileOrContent ( cert ) ,
KeyFile : types . FileOrContent ( key ) ,
2019-02-21 22:08:05 +00:00
} ,
}
}
}
return nil
}
func getCertificateBlocks ( secret * corev1 . Secret , namespace , secretName string ) ( string , string , error ) {
var missingEntries [ ] string
tlsCrtData , tlsCrtExists := secret . Data [ "tls.crt" ]
if ! tlsCrtExists {
missingEntries = append ( missingEntries , "tls.crt" )
}
tlsKeyData , tlsKeyExists := secret . Data [ "tls.key" ]
if ! tlsKeyExists {
missingEntries = append ( missingEntries , "tls.key" )
}
if len ( missingEntries ) > 0 {
return "" , "" , fmt . Errorf ( "secret %s/%s is missing the following TLS data entries: %s" ,
namespace , secretName , strings . Join ( missingEntries , ", " ) )
}
cert := string ( tlsCrtData )
if cert == "" {
missingEntries = append ( missingEntries , "tls.crt" )
}
key := string ( tlsKeyData )
if key == "" {
missingEntries = append ( missingEntries , "tls.key" )
}
if len ( missingEntries ) > 0 {
return "" , "" , fmt . Errorf ( "secret %s/%s contains the following empty TLS data entries: %s" ,
namespace , secretName , strings . Join ( missingEntries , ", " ) )
}
return cert , key , nil
}
2019-03-18 09:10:04 +00:00
2020-01-14 14:48:06 +00:00
func getTLSConfig ( tlsConfigs map [ string ] * tls . CertAndStores ) [ ] * tls . CertAndStores {
var secretNames [ ] string
for secretName := range tlsConfigs {
secretNames = append ( secretNames , secretName )
2019-03-18 09:10:04 +00:00
}
2020-01-14 14:48:06 +00:00
sort . Strings ( secretNames )
2019-03-18 09:10:04 +00:00
2020-01-14 14:48:06 +00:00
var configs [ ] * tls . CertAndStores
for _ , secretName := range secretNames {
configs = append ( configs , tlsConfigs [ secretName ] )
}
return configs
}
2023-04-17 08:56:36 +00:00
func ( p * Provider ) loadService ( client Client , namespace string , backend netv1 . IngressBackend ) ( * dynamic . Service , error ) {
2023-07-19 15:36:06 +00:00
if backend . Resource != nil {
// https://kubernetes.io/docs/concepts/services-networking/ingress/#resource-backend
return nil , errors . New ( "resource backends are not supported" )
}
if backend . Service == nil {
return nil , errors . New ( "missing service definition" )
}
2021-03-15 10:16:04 +00:00
service , exists , err := client . GetService ( namespace , backend . Service . Name )
2020-01-14 14:48:06 +00:00
if err != nil {
return nil , err
}
if ! exists {
return nil , errors . New ( "service not found" )
}
2021-07-13 10:54:09 +00:00
if ! p . AllowExternalNameServices && service . Spec . Type == corev1 . ServiceTypeExternalName {
2021-07-13 16:12:29 +00:00
return nil , fmt . Errorf ( "externalName services not allowed: %s/%s" , namespace , backend . Service . Name )
2021-07-13 10:54:09 +00:00
}
2020-01-14 14:48:06 +00:00
var portName string
var portSpec corev1 . ServicePort
var match bool
for _ , p := range service . Spec . Ports {
2021-03-15 10:16:04 +00:00
if backend . Service . Port . Number == p . Port || ( backend . Service . Port . Name == p . Name && len ( p . Name ) > 0 ) {
2020-01-14 14:48:06 +00:00
portName = p . Name
portSpec = p
match = true
break
2019-03-18 09:10:04 +00:00
}
2020-01-14 14:48:06 +00:00
}
2019-03-18 09:10:04 +00:00
2020-01-14 14:48:06 +00:00
if ! match {
return nil , errors . New ( "service port not found" )
2019-03-18 09:10:04 +00:00
}
2022-11-16 10:38:07 +00:00
lb := & dynamic . ServersLoadBalancer { }
lb . SetDefaults ( )
svc := & dynamic . Service { LoadBalancer : lb }
2019-03-18 09:10:04 +00:00
2020-01-14 14:48:06 +00:00
svcConfig , err := parseServiceConfig ( service . Annotations )
2019-03-18 09:10:04 +00:00
if err != nil {
2020-01-14 14:48:06 +00:00
return nil , err
2019-03-18 09:10:04 +00:00
}
2024-04-29 10:20:04 +00:00
nativeLB := p . NativeLBByDefault
2020-01-14 14:48:06 +00:00
if svcConfig != nil && svcConfig . Service != nil {
svc . LoadBalancer . Sticky = svcConfig . Service . Sticky
2021-05-28 15:37:11 +00:00
2020-01-14 14:48:06 +00:00
if svcConfig . Service . PassHostHeader != nil {
svc . LoadBalancer . PassHostHeader = svcConfig . Service . PassHostHeader
}
2019-03-18 09:10:04 +00:00
2021-05-28 15:37:11 +00:00
if svcConfig . Service . ServersTransport != "" {
svc . LoadBalancer . ServersTransport = svcConfig . Service . ServersTransport
}
2023-03-20 15:46:05 +00:00
2024-04-29 10:20:04 +00:00
if svcConfig . Service . NativeLB != nil {
nativeLB = * svcConfig . Service . NativeLB
2023-03-20 15:46:05 +00:00
}
2024-02-27 09:54:04 +00:00
if svcConfig . Service . NodePortLB && service . Spec . Type == corev1 . ServiceTypeNodePort {
2024-08-01 13:50:04 +00:00
if p . DisableClusterScopeResources {
return nil , errors . New ( "nodes lookup is disabled" )
}
2024-02-27 09:54:04 +00:00
nodes , nodesExists , nodesErr := client . GetNodes ( )
if nodesErr != nil {
return nil , nodesErr
}
if ! nodesExists || len ( nodes ) == 0 {
return nil , fmt . Errorf ( "nodes not found in namespace %s" , namespace )
}
protocol := getProtocol ( portSpec , portSpec . Name , svcConfig )
var servers [ ] dynamic . Server
for _ , node := range nodes {
for _ , addr := range node . Status . Addresses {
if addr . Type == corev1 . NodeInternalIP {
hostPort := net . JoinHostPort ( addr . Address , strconv . Itoa ( int ( portSpec . NodePort ) ) )
servers = append ( servers , dynamic . Server {
URL : fmt . Sprintf ( "%s://%s" , protocol , hostPort ) ,
} )
}
}
}
if len ( servers ) == 0 {
return nil , fmt . Errorf ( "no servers were generated for service %s in namespace" , backend . Service . Name )
}
svc . LoadBalancer . Servers = servers
return svc , nil
}
2021-04-20 15:19:29 +00:00
}
2020-01-14 14:48:06 +00:00
if service . Spec . Type == corev1 . ServiceTypeExternalName {
protocol := getProtocol ( portSpec , portSpec . Name , svcConfig )
2020-12-04 19:56:04 +00:00
hostPort := net . JoinHostPort ( service . Spec . ExternalName , strconv . Itoa ( int ( portSpec . Port ) ) )
2020-01-14 14:48:06 +00:00
svc . LoadBalancer . Servers = [ ] dynamic . Server {
2020-12-04 19:56:04 +00:00
{ URL : fmt . Sprintf ( "%s://%s" , protocol , hostPort ) } ,
2020-01-14 14:48:06 +00:00
}
2024-04-29 10:20:04 +00:00
return svc , nil
}
if nativeLB {
address , err := getNativeServiceAddress ( * service , portSpec )
if err != nil {
return nil , fmt . Errorf ( "getting native Kubernetes Service address: %w" , err )
}
protocol := getProtocol ( portSpec , portSpec . Name , svcConfig )
svc . LoadBalancer . Servers = [ ] dynamic . Server {
{ URL : fmt . Sprintf ( "%s://%s" , protocol , address ) } ,
}
2020-01-14 14:48:06 +00:00
return svc , nil
2019-03-18 09:10:04 +00:00
}
2024-06-21 12:56:03 +00:00
endpointSlices , err := client . GetEndpointSlicesForService ( namespace , backend . Service . Name )
if err != nil {
return nil , fmt . Errorf ( "getting endpointslices: %w" , err )
2020-01-14 14:48:06 +00:00
}
2024-06-21 12:56:03 +00:00
addresses := map [ string ] struct { } { }
for _ , endpointSlice := range endpointSlices {
2021-05-27 22:58:07 +00:00
var port int32
2024-06-21 12:56:03 +00:00
for _ , p := range endpointSlice . Ports {
if portName == * p . Name {
port = * p . Port
2020-01-14 14:48:06 +00:00
break
}
}
if port == 0 {
2021-04-15 16:16:04 +00:00
continue
2020-01-14 14:48:06 +00:00
}
protocol := getProtocol ( portSpec , portName , svcConfig )
2024-06-21 12:56:03 +00:00
for _ , endpoint := range endpointSlice . Endpoints {
if endpoint . Conditions . Ready == nil || ! * endpoint . Conditions . Ready {
continue
}
for _ , address := range endpoint . Addresses {
if _ , ok := addresses [ address ] ; ok {
continue
}
2020-12-04 19:56:04 +00:00
2024-06-21 12:56:03 +00:00
addresses [ address ] = struct { } { }
svc . LoadBalancer . Servers = append ( svc . LoadBalancer . Servers , dynamic . Server {
URL : fmt . Sprintf ( "%s://%s" , protocol , net . JoinHostPort ( address , strconv . Itoa ( int ( port ) ) ) ) ,
} )
}
2020-01-14 14:48:06 +00:00
}
}
return svc , nil
}
2023-03-20 15:46:05 +00:00
func getNativeServiceAddress ( service corev1 . Service , svcPort corev1 . ServicePort ) ( string , error ) {
if service . Spec . ClusterIP == "None" {
return "" , fmt . Errorf ( "no clusterIP on headless service: %s/%s" , service . Namespace , service . Name )
}
if service . Spec . ClusterIP == "" {
return "" , fmt . Errorf ( "no clusterIP found for service: %s/%s" , service . Namespace , service . Name )
}
return net . JoinHostPort ( service . Spec . ClusterIP , strconv . Itoa ( int ( svcPort . Port ) ) ) , nil
}
2020-01-14 14:48:06 +00:00
func getProtocol ( portSpec corev1 . ServicePort , portName string , svcConfig * ServiceConfig ) string {
if svcConfig != nil && svcConfig . Service != nil && svcConfig . Service . ServersScheme != "" {
return svcConfig . Service . ServersScheme
}
protocol := "http"
if portSpec . Port == 443 || strings . HasPrefix ( portName , "https" ) {
protocol = "https"
}
return protocol
}
2021-01-05 11:26:04 +00:00
func makeRouterKeyWithHash ( key , rule string ) ( string , error ) {
h := sha256 . New ( )
if _ , err := h . Write ( [ ] byte ( rule ) ) ; err != nil {
return "" , err
}
dupKey := fmt . Sprintf ( "%s-%.10x" , key , h . Sum ( nil ) )
return dupKey , nil
}
2023-04-17 08:56:36 +00:00
func loadRouter ( rule netv1 . IngressRule , pa netv1 . HTTPIngressPath , rtConfig * RouterConfig , serviceName string ) * dynamic . Router {
2020-01-14 14:48:06 +00:00
var rules [ ] string
if len ( rule . Host ) > 0 {
2020-02-10 15:03:39 +00:00
rules = [ ] string { buildHostRule ( rule . Host ) }
2020-01-14 14:48:06 +00:00
}
if len ( pa . Path ) > 0 {
matcher := defaultPathMatcher
2020-07-28 15:50:04 +00:00
2023-04-17 08:56:36 +00:00
if pa . PathType == nil || * pa . PathType == "" || * pa . PathType == netv1 . PathTypeImplementationSpecific {
2020-07-28 15:50:04 +00:00
if rtConfig != nil && rtConfig . Router != nil && rtConfig . Router . PathMatcher != "" {
matcher = rtConfig . Router . PathMatcher
}
2023-04-17 08:56:36 +00:00
} else if * pa . PathType == netv1 . PathTypeExact {
2020-07-28 15:50:04 +00:00
matcher = "Path"
2020-01-14 14:48:06 +00:00
}
rules = append ( rules , fmt . Sprintf ( "%s(`%s`)" , matcher , pa . Path ) )
}
rt := & dynamic . Router {
Rule : strings . Join ( rules , " && " ) ,
Service : serviceName ,
}
if rtConfig != nil && rtConfig . Router != nil {
2024-08-12 12:28:04 +00:00
rt . RuleSyntax = rtConfig . Router . RuleSyntax
2020-01-14 14:48:06 +00:00
rt . Priority = rtConfig . Router . Priority
rt . EntryPoints = rtConfig . Router . EntryPoints
rt . Middlewares = rtConfig . Router . Middlewares
if rtConfig . Router . TLS != nil {
rt . TLS = rtConfig . Router . TLS
}
}
return rt
}
2020-02-03 16:56:04 +00:00
func throttleEvents ( ctx context . Context , throttleDuration time . Duration , pool * safe . Pool , eventsChan <- chan interface { } ) chan interface { } {
2019-08-30 10:16:04 +00:00
if throttleDuration == 0 {
return nil
}
2020-07-15 17:18:03 +00:00
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling).
2019-08-30 10:16:04 +00:00
eventsChanBuffered := make ( chan interface { } , 1 )
// Run a goroutine that reads events from eventChan and does a
// non-blocking write to pendingEvent. This guarantees that writing to
// eventChan will never block, and that pendingEvent will have
// something in it if there's been an event since we read from that channel.
2020-02-03 16:56:04 +00:00
pool . GoCtx ( func ( ctxPool context . Context ) {
2019-08-30 10:16:04 +00:00
for {
select {
2020-02-03 16:56:04 +00:00
case <- ctxPool . Done ( ) :
2019-08-30 10:16:04 +00:00
return
case nextEvent := <- eventsChan :
select {
case eventsChanBuffered <- nextEvent :
default :
// We already have an event in eventsChanBuffered, so we'll
// do a refresh as soon as our throttle allows us to. It's fine
// to drop the event and keep whatever's in the buffer -- we
2020-07-15 17:18:03 +00:00
// don't do different things for different events.
2022-11-21 17:36:05 +00:00
log . Ctx ( ctx ) . Debug ( ) . Msgf ( "Dropping event kind %T due to throttling" , nextEvent )
2019-08-30 10:16:04 +00:00
}
}
}
2020-02-03 16:56:04 +00:00
} )
2019-08-30 10:16:04 +00:00
return eventsChanBuffered
}