2020-12-15 15:40:05 +00:00
package gateway
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/hashstructure"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/job"
"github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/tls"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
2021-03-15 08:44:03 +00:00
"sigs.k8s.io/gateway-api/apis/v1alpha1"
2020-12-15 15:40:05 +00:00
)
2021-02-02 18:36:04 +00:00
const (
providerName = "kubernetesgateway"
traefikServiceKind = "TraefikService"
traefikServiceGroupName = "traefik.containo.us"
2021-05-20 09:50:12 +00:00
routeHTTPKind = "HTTPRoute"
routeTCPKind = "TCPRoute"
routeTLSKind = "TLSRoute"
2021-02-02 18:36:04 +00:00
)
2020-12-15 15:40:05 +00:00
// Provider holds configurations of the provider.
type Provider struct {
Endpoint string ` description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" `
Token string ` description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" `
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 label selector to select specific GatewayClasses." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true" `
ThrottleDuration ptypes . Duration ` description:"Kubernetes refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true" `
EntryPoints map [ string ] Entrypoint ` json:"-" toml:"-" yaml:"-" label:"-" file:"-" `
lastConfiguration safe . Safe
}
// Entrypoint defines the available entry points.
type Entrypoint struct {
Address string
HasHTTPTLSConf bool
}
func ( p * Provider ) newK8sClient ( ctx context . Context ) ( * clientWrapper , error ) {
// Label selector validation
_ , err := labels . Parse ( p . LabelSelector )
if err != nil {
return nil , fmt . Errorf ( "invalid label selector: %q" , p . LabelSelector )
}
log . FromContext ( ctx ) . Infof ( "label selector is: %q" , p . LabelSelector )
withEndpoint := ""
if p . Endpoint != "" {
withEndpoint = fmt . Sprintf ( " with endpoint %s" , p . Endpoint )
}
var client * clientWrapper
switch {
case os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" && os . Getenv ( "KUBERNETES_SERVICE_PORT" ) != "" :
log . FromContext ( ctx ) . Infof ( "Creating in-cluster Provider client%s" , withEndpoint )
client , err = newInClusterClient ( p . Endpoint )
case os . Getenv ( "KUBECONFIG" ) != "" :
log . FromContext ( ctx ) . Infof ( "Creating cluster-external Provider client from KUBECONFIG %s" , os . Getenv ( "KUBECONFIG" ) )
client , err = newExternalClusterClientFromFile ( os . Getenv ( "KUBECONFIG" ) )
default :
log . FromContext ( ctx ) . Infof ( "Creating cluster-external Provider client%s" , withEndpoint )
client , err = newExternalClusterClient ( p . Endpoint , p . Token , p . CertAuthFilePath )
}
if err != nil {
return nil , err
}
client . labelSelector = p . LabelSelector
return client , nil
}
// Init the provider.
func ( p * Provider ) Init ( ) error {
return nil
}
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
func ( p * Provider ) Provide ( configurationChan chan <- dynamic . Message , pool * safe . Pool ) error {
ctxLog := log . With ( context . Background ( ) , log . Str ( log . ProviderName , providerName ) )
logger := log . FromContext ( ctxLog )
k8sClient , err := p . newK8sClient ( ctxLog )
if err != nil {
return err
}
pool . GoCtx ( func ( ctxPool context . Context ) {
operation := func ( ) error {
eventsChan , err := k8sClient . WatchAll ( p . Namespaces , ctxPool . Done ( ) )
if err != nil {
logger . Errorf ( "Error watching kubernetes events: %v" , err )
timer := time . NewTimer ( 1 * time . Second )
select {
case <- timer . C :
return err
case <- ctxPool . Done ( ) :
return nil
}
}
throttleDuration := time . Duration ( p . ThrottleDuration )
throttledChan := throttleEvents ( ctxLog , throttleDuration , pool , eventsChan )
if throttledChan != nil {
eventsChan = throttledChan
}
for {
select {
case <- ctxPool . Done ( ) :
return nil
case event := <- eventsChan :
// 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.
conf := p . loadConfigurationFromGateway ( ctxLog , k8sClient )
confHash , err := hashstructure . Hash ( conf , nil )
switch {
case err != nil :
logger . Error ( "Unable to hash the configuration" )
case p . lastConfiguration . Get ( ) == confHash :
logger . Debugf ( "Skipping Kubernetes event kind %T" , event )
default :
p . lastConfiguration . Set ( confHash )
configurationChan <- dynamic . Message {
ProviderName : providerName ,
Configuration : conf ,
}
}
// 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 )
}
}
}
notify := func ( err error , time time . Duration ) {
logger . Errorf ( "Provider connection error: %v; retrying in %s" , err , time )
}
err := backoff . RetryNotify ( safe . OperationWithRecover ( operation ) , backoff . WithContext ( job . NewBackOff ( backoff . NewExponentialBackOff ( ) ) , ctxPool ) , notify )
if err != nil {
logger . Errorf ( "Cannot connect to Provider: %v" , err )
}
} )
return nil
}
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
func ( p * Provider ) loadConfigurationFromGateway ( ctx context . Context , client Client ) * dynamic . Configuration {
logger := log . FromContext ( ctx )
gatewayClassNames := map [ string ] struct { } { }
gatewayClasses , err := client . GetGatewayClasses ( )
if err != nil {
logger . Errorf ( "Cannot find GatewayClasses: %v" , err )
return & dynamic . Configuration {
UDP : & dynamic . UDPConfiguration {
Routers : map [ string ] * dynamic . UDPRouter { } ,
Services : map [ string ] * dynamic . UDPService { } ,
} ,
TCP : & dynamic . TCPConfiguration {
Routers : map [ string ] * dynamic . TCPRouter { } ,
Services : map [ string ] * dynamic . TCPService { } ,
} ,
HTTP : & dynamic . HTTPConfiguration {
Routers : map [ string ] * dynamic . Router { } ,
Middlewares : map [ string ] * dynamic . Middleware { } ,
Services : map [ string ] * dynamic . Service { } ,
} ,
TLS : & dynamic . TLSConfiguration { } ,
}
}
for _ , gatewayClass := range gatewayClasses {
if gatewayClass . Spec . Controller == "traefik.io/gateway-controller" {
gatewayClassNames [ gatewayClass . Name ] = struct { } { }
err := client . UpdateGatewayClassStatus ( gatewayClass , metav1 . Condition {
Type : string ( v1alpha1 . GatewayClassConditionStatusAdmitted ) ,
Status : metav1 . ConditionTrue ,
Reason : "Handled" ,
Message : "Handled by Traefik controller" ,
LastTransitionTime : metav1 . Now ( ) ,
} )
if err != nil {
logger . Errorf ( "Failed to update %s condition: %v" , v1alpha1 . GatewayClassConditionStatusAdmitted , err )
}
}
}
cfgs := map [ string ] * dynamic . Configuration { }
// TODO check if we can only use the default filtering mechanism
for _ , gateway := range client . GetGateways ( ) {
ctxLog := log . With ( ctx , log . Str ( "gateway" , gateway . Name ) , log . Str ( "namespace" , gateway . Namespace ) )
logger := log . FromContext ( ctxLog )
if _ , ok := gatewayClassNames [ gateway . Spec . GatewayClassName ] ; ! ok {
continue
}
2021-05-20 09:50:12 +00:00
cfg , err := p . createGatewayConf ( ctxLog , client , gateway )
2020-12-15 15:40:05 +00:00
if err != nil {
logger . Error ( err )
continue
}
cfgs [ gateway . Name + gateway . Namespace ] = cfg
}
conf := provider . Merge ( ctx , cfgs )
conf . TLS = & dynamic . TLSConfiguration { }
for _ , cfg := range cfgs {
if conf . TLS == nil {
conf . TLS = & dynamic . TLSConfiguration { }
}
conf . TLS . Certificates = append ( conf . TLS . Certificates , cfg . TLS . Certificates ... )
for name , options := range cfg . TLS . Options {
if conf . TLS . Options == nil {
conf . TLS . Options = map [ string ] tls . Options { }
}
conf . TLS . Options [ name ] = options
}
for name , store := range cfg . TLS . Stores {
if conf . TLS . Stores == nil {
conf . TLS . Stores = map [ string ] tls . Store { }
}
conf . TLS . Stores [ name ] = store
}
}
return conf
}
2021-05-20 09:50:12 +00:00
func ( p * Provider ) createGatewayConf ( ctx context . Context , client Client , gateway * v1alpha1 . Gateway ) ( * dynamic . Configuration , error ) {
2020-12-15 15:40:05 +00:00
conf := & dynamic . Configuration {
UDP : & dynamic . UDPConfiguration {
Routers : map [ string ] * dynamic . UDPRouter { } ,
Services : map [ string ] * dynamic . UDPService { } ,
} ,
TCP : & dynamic . TCPConfiguration {
Routers : map [ string ] * dynamic . TCPRouter { } ,
Services : map [ string ] * dynamic . TCPService { } ,
} ,
HTTP : & dynamic . HTTPConfiguration {
Routers : map [ string ] * dynamic . Router { } ,
Middlewares : map [ string ] * dynamic . Middleware { } ,
Services : map [ string ] * dynamic . Service { } ,
} ,
TLS : & dynamic . TLSConfiguration { } ,
}
tlsConfigs := make ( map [ string ] * tls . CertAndStores )
// GatewayReasonListenersNotValid is used when one or more
// Listeners have an invalid or unsupported configuration
// and cannot be configured on the Gateway.
2021-05-20 09:50:12 +00:00
listenerStatuses := p . fillGatewayConf ( ctx , client , gateway , conf , tlsConfigs )
2020-12-15 15:40:05 +00:00
gatewayStatus , errG := p . makeGatewayStatus ( listenerStatuses )
err := client . UpdateGatewayStatus ( gateway , gatewayStatus )
if err != nil {
return nil , fmt . Errorf ( "an error occurred while updating gateway status: %w" , err )
}
if errG != nil {
return nil , fmt . Errorf ( "an error occurred while creating gateway status: %w" , errG )
}
if len ( tlsConfigs ) > 0 {
conf . TLS . Certificates = append ( conf . TLS . Certificates , getTLSConfig ( tlsConfigs ) ... )
}
return conf , nil
}
2021-05-20 09:50:12 +00:00
func ( p * Provider ) fillGatewayConf ( ctx context . Context , client Client , gateway * v1alpha1 . Gateway , conf * dynamic . Configuration , tlsConfigs map [ string ] * tls . CertAndStores ) [ ] v1alpha1 . ListenerStatus {
2020-12-15 15:40:05 +00:00
listenerStatuses := make ( [ ] v1alpha1 . ListenerStatus , len ( gateway . Spec . Listeners ) )
2021-05-20 09:50:12 +00:00
logger := log . FromContext ( ctx )
allocatedPort := map [ v1alpha1 . PortNumber ] v1alpha1 . ProtocolType { }
2020-12-15 15:40:05 +00:00
for i , listener := range gateway . Spec . Listeners {
listenerStatuses [ i ] = v1alpha1 . ListenerStatus {
Port : listener . Port ,
Conditions : [ ] metav1 . Condition { } ,
}
// Supported Protocol
2021-05-20 09:50:12 +00:00
if listener . Protocol != v1alpha1 . HTTPProtocolType && listener . Protocol != v1alpha1 . HTTPSProtocolType &&
listener . Protocol != v1alpha1 . TCPProtocolType && listener . Protocol != v1alpha1 . TLSProtocolType {
2020-12-15 15:40:05 +00:00
// update "Detached" status true with "UnsupportedProtocol" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonUnsupportedProtocol ) ,
Message : fmt . Sprintf ( "Unsupported listener protocol %q" , listener . Protocol ) ,
} )
continue
}
2021-05-20 09:50:12 +00:00
// Supported Route types
if listener . Routes . Kind != routeHTTPKind && listener . Routes . Kind != routeTCPKind && listener . Routes . Kind != routeTLSKind {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidRoutesRef ) ,
Message : fmt . Sprintf ( "Unsupported Route Kind %q" , listener . Routes . Kind ) ,
} )
continue
}
// Protocol compliant with route type
if listener . Protocol == v1alpha1 . HTTPProtocolType && listener . Routes . Kind != routeHTTPKind ||
listener . Protocol == v1alpha1 . HTTPSProtocolType && listener . Routes . Kind != routeHTTPKind ||
listener . Protocol == v1alpha1 . TCPProtocolType && listener . Routes . Kind != routeTCPKind ||
listener . Protocol == v1alpha1 . TLSProtocolType && listener . Routes . Kind != routeTLSKind && listener . Routes . Kind != routeTCPKind {
// update "Detached" status true with "UnsupportedProtocol" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonUnsupportedProtocol ) ,
Message : fmt . Sprintf ( "listener protocol %q not supported with route kind %q" , listener . Protocol , listener . Routes . Kind ) ,
} )
continue
}
if _ , ok := allocatedPort [ listener . Port ] ; ok {
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonPortUnavailable ) ,
Message : fmt . Sprintf ( "port %d unavailable" , listener . Port ) ,
} )
continue
}
allocatedPort [ listener . Port ] = listener . Protocol
2020-12-15 15:40:05 +00:00
ep , err := p . entryPointName ( listener . Port , listener . Protocol )
if err != nil {
// update "Detached" status with "PortUnavailable" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonPortUnavailable ) ,
Message : fmt . Sprintf ( "Cannot find entryPoint for Gateway: %v" , err ) ,
} )
continue
}
2021-05-20 09:50:12 +00:00
// TLS
if listener . Protocol == v1alpha1 . HTTPSProtocolType || listener . Protocol == v1alpha1 . TLSProtocolType {
if listener . TLS == nil || ( listener . TLS . CertificateRef == nil && listener . TLS . Mode != v1alpha1 . TLSModePassthrough ) {
2020-12-15 15:40:05 +00:00
// update "Detached" status with "UnsupportedProtocol" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonUnsupportedProtocol ) ,
Message : fmt . Sprintf ( "No TLS configuration for Gateway Listener port %d and protocol %q" , listener . Port , listener . Protocol ) ,
} )
continue
}
2021-05-20 09:50:12 +00:00
if listener . TLS . Mode == v1alpha1 . TLSModePassthrough && listener . TLS . CertificateRef != nil {
// https://gateway-api.sigs.k8s.io/guides/tls/
logger . Warnf ( "In case of Passthrough TLS mode, no TLS settings take effect as the TLS session from the client is NOT terminated at the Gateway" )
}
isTLSPassthrough := listener . TLS . Mode == v1alpha1 . TLSModePassthrough
// Allowed configurations:
// Protocol TLS -> Passthrough -> TLSRoute
// Protocol TLS -> Terminate -> TCPRoute
// Protocol HTTPS -> Terminate -> HTTPRoute
if ! ( listener . Protocol == v1alpha1 . TLSProtocolType && isTLSPassthrough && listener . Routes . Kind == routeTLSKind ||
listener . Protocol == v1alpha1 . TLSProtocolType && ! isTLSPassthrough && listener . Routes . Kind == routeTCPKind ||
listener . Protocol == v1alpha1 . HTTPSProtocolType && ! isTLSPassthrough && listener . Routes . Kind == routeHTTPKind ) {
// update "ConditionDetached" status true with "ReasonUnsupportedProtocol" reason
2020-12-15 15:40:05 +00:00
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
2021-05-20 09:50:12 +00:00
Type : string ( v1alpha1 . ListenerConditionDetached ) ,
Status : metav1 . ConditionTrue ,
2020-12-15 15:40:05 +00:00
LastTransitionTime : metav1 . Now ( ) ,
2021-05-20 09:50:12 +00:00
Reason : string ( v1alpha1 . ListenerReasonUnsupportedProtocol ) ,
Message : fmt . Sprintf ( "Unsupported route kind %q with %q" ,
listener . Routes . Kind , listener . TLS . Mode ) ,
2020-12-15 15:40:05 +00:00
} )
continue
}
2021-05-20 09:50:12 +00:00
if ! isTLSPassthrough {
if listener . TLS . CertificateRef . Kind != "Secret" || listener . TLS . CertificateRef . Group != "core" {
2020-12-15 15:40:05 +00:00
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidCertificateRef ) ,
2021-05-20 09:50:12 +00:00
Message : fmt . Sprintf ( "Unsupported TLS CertificateRef group/kind : %v/%v" , listener . TLS . CertificateRef . Group , listener . TLS . CertificateRef . Kind ) ,
2020-12-15 15:40:05 +00:00
} )
continue
}
2021-05-20 09:50:12 +00:00
configKey := gateway . Namespace + "/" + listener . TLS . CertificateRef . Name
if _ , tlsExists := tlsConfigs [ configKey ] ; ! tlsExists {
tlsConf , err := getTLS ( client , listener . TLS . CertificateRef . Name , gateway . Namespace )
if err != nil {
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidCertificateRef ) ,
Message : fmt . Sprintf ( "Error while retrieving certificate: %v" , err ) ,
} )
continue
}
tlsConfigs [ configKey ] = tlsConf
}
2020-12-15 15:40:05 +00:00
}
}
2021-05-20 09:50:12 +00:00
switch listener . Routes . Kind {
case routeHTTPKind :
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , gatewayHTTPRouteToHTTPConf ( ctx , ep , listener , gateway , client , conf ) ... )
case routeTCPKind :
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , gatewayTCPRouteToTCPConf ( ctx , ep , listener , gateway , client , conf ) ... )
case routeTLSKind :
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , gatewayTLSRouteToTCPConf ( ctx , ep , listener , gateway , client , conf ) ... )
2020-12-15 15:40:05 +00:00
}
2021-05-20 09:50:12 +00:00
}
return listenerStatuses
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
func gatewayHTTPRouteToHTTPConf ( ctx context . Context , ep string , listener v1alpha1 . Listener , gateway * v1alpha1 . Gateway , client Client , conf * dynamic . Configuration ) [ ] metav1 . Condition {
// TODO: support RouteNamespaces
selector := labels . SelectorFromSet ( listener . Routes . Selector . MatchLabels )
httpRoutes , err := client . GetHTTPRoutes ( gateway . Namespace , selector )
if err != nil {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
return [ ] metav1 . Condition { {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidRoutesRef ) ,
Message : fmt . Sprintf ( "Cannot fetch %ss for namespace %q and matchLabels %v" , listener . Routes . Kind , gateway . Namespace , listener . Routes . Selector . MatchLabels ) ,
} }
}
if len ( httpRoutes ) == 0 {
log . FromContext ( ctx ) . Debugf ( "No HTTPRoutes found for selector %q" , selector )
return nil
}
var conditions [ ] metav1 . Condition
for _ , httpRoute := range httpRoutes {
hostRule , err := hostRule ( httpRoute . Spec )
2020-12-15 15:40:05 +00:00
if err != nil {
2021-05-20 09:50:12 +00:00
conditions = append ( conditions , metav1 . Condition {
2020-12-15 15:40:05 +00:00
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
2021-05-20 09:50:12 +00:00
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping HTTPRoute %s: invalid hostname: %v" , httpRoute . Name , err ) ,
2020-12-15 15:40:05 +00:00
} )
continue
}
2021-05-20 09:50:12 +00:00
for _ , routeRule := range httpRoute . Spec . Rules {
rule , err := extractRule ( routeRule , hostRule )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping %s %s: cannot generate rule: %v" , listener . Routes . Kind , httpRoute . Name , err ) ,
} )
}
router := dynamic . Router {
Rule : rule ,
EntryPoints : [ ] string { ep } ,
2020-12-15 15:40:05 +00:00
}
2021-05-20 09:50:12 +00:00
if listener . TLS != nil {
// TODO support let's encrypt
router . TLS = & dynamic . RouterTLSConfig { }
}
// Adding the gateway name and the entryPoint name prevents overlapping of routers build from the same routes.
routerName := httpRoute . Name + "-" + gateway . Name + "-" + ep
routerKey , err := makeRouterKey ( router . Rule , makeID ( httpRoute . Namespace , routerName ) )
2021-04-29 15:18:04 +00:00
if err != nil {
2021-05-20 09:50:12 +00:00
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
2021-04-29 15:18:04 +00:00
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
2021-05-20 09:50:12 +00:00
Message : fmt . Sprintf ( "Skipping %s %s: cannot make router's key with rule %s: %v" , listener . Routes . Kind , httpRoute . Name , router . Rule , err ) ,
2021-04-29 15:18:04 +00:00
} )
2021-05-20 09:50:12 +00:00
// TODO update the RouteStatus condition / deduplicate conditions on listener
2021-04-29 15:18:04 +00:00
continue
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
if routeRule . ForwardTo == nil {
continue
}
// Traefik internal service can be used only if there is only one ForwardTo service reference.
if len ( routeRule . ForwardTo ) == 1 && isInternalService ( routeRule . ForwardTo [ 0 ] ) {
router . Service = routeRule . ForwardTo [ 0 ] . BackendRef . Name
} else {
wrrService , subServices , err := loadServices ( client , gateway . Namespace , routeRule . ForwardTo )
2020-12-15 15:40:05 +00:00
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
2021-05-20 09:50:12 +00:00
conditions = append ( conditions , metav1 . Condition {
2020-12-15 15:40:05 +00:00
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
2021-05-20 09:50:12 +00:00
Message : fmt . Sprintf ( "Cannot load service from %s %s/%s : %v" , listener . Routes . Kind , gateway . Namespace , httpRoute . Name , err ) ,
2020-12-15 15:40:05 +00:00
} )
2021-05-20 09:50:12 +00:00
// TODO update the RouteStatus condition / deduplicate conditions on listener
2020-12-15 15:40:05 +00:00
continue
}
2021-05-20 09:50:12 +00:00
for svcName , svc := range subServices {
conf . HTTP . Services [ svcName ] = svc
2020-12-15 15:40:05 +00:00
}
2021-05-20 09:50:12 +00:00
serviceName := provider . Normalize ( routerKey + "-wrr" )
conf . HTTP . Services [ serviceName ] = wrrService
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
router . Service = serviceName
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
routerKey = provider . Normalize ( routerKey )
conf . HTTP . Routers [ routerKey ] = & router
}
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
return conditions
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
func gatewayTCPRouteToTCPConf ( ctx context . Context , ep string , listener v1alpha1 . Listener , gateway * v1alpha1 . Gateway , client Client , conf * dynamic . Configuration ) [ ] metav1 . Condition {
// TODO: support RouteNamespaces
selector := labels . SelectorFromSet ( listener . Routes . Selector . MatchLabels )
tcpRoutes , err := client . GetTCPRoutes ( gateway . Namespace , selector )
if err != nil {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
return [ ] metav1 . Condition { {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidRoutesRef ) ,
Message : fmt . Sprintf ( "Cannot fetch %ss for namespace %q and matchLabels %v" , listener . Routes . Kind , gateway . Namespace , listener . Routes . Selector . MatchLabels ) ,
} }
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
if len ( tcpRoutes ) == 0 {
log . FromContext ( ctx ) . Debugf ( "No TCPRoutes found for selector %q" , selector )
return nil
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
var conditions [ ] metav1 . Condition
for _ , tcpRoute := range tcpRoutes {
if len ( tcpRoute . Spec . Rules ) > 1 {
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping %s %s: multiple rules are not supported" , listener . Routes . Kind , tcpRoute . Name ) ,
} )
continue
}
for _ , routeRule := range tcpRoute . Spec . Rules {
router := dynamic . TCPRouter {
Rule : "HostSNI(`*`)" , // Gateway listener hostname not available in TCP
EntryPoints : [ ] string { ep } ,
}
if listener . TLS != nil {
// TODO support let's encrypt
router . TLS = & dynamic . RouterTCPTLSConfig { }
}
// Adding the gateway name and the entryPoint name prevents overlapping of routers build from the same routes.
routerName := tcpRoute . Name + "-" + gateway . Name + "-" + ep
routerKey , err := makeRouterKey ( "" , makeID ( tcpRoute . Namespace , routerName ) )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping %s %s: cannot make router's key with rule %s: %v" , listener . Routes . Kind , tcpRoute . Name , router . Rule , err ) ,
} )
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
// Should not happen due to validation
// https://github.com/kubernetes-sigs/gateway-api/blob/af68a622f072811767d246ef5897135d93af0704/apis/v1alpha1/tcproute_types.go#L76
if routeRule . ForwardTo == nil {
continue
}
wrrService , subServices , err := loadTCPServices ( client , gateway . Namespace , routeRule . ForwardTo )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Cannot load service from %s %s/%s : %v" , listener . Routes . Kind , gateway . Namespace , tcpRoute . Name , err ) ,
} )
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
for svcName , svc := range subServices {
conf . TCP . Services [ svcName ] = svc
}
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
serviceName := provider . Normalize ( routerKey + "-wrr" )
conf . TCP . Services [ serviceName ] = wrrService
2020-12-15 15:40:05 +00:00
2021-05-20 09:50:12 +00:00
router . Service = serviceName
routerKey = provider . Normalize ( routerKey )
conf . TCP . Routers [ routerKey ] = & router
}
}
return conditions
}
func gatewayTLSRouteToTCPConf ( ctx context . Context , ep string , listener v1alpha1 . Listener , gateway * v1alpha1 . Gateway , client Client , conf * dynamic . Configuration ) [ ] metav1 . Condition {
// TODO: support RouteNamespaces
selector := labels . SelectorFromSet ( listener . Routes . Selector . MatchLabels )
tlsRoutes , err := client . GetTLSRoutes ( gateway . Namespace , selector )
if err != nil {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
return [ ] metav1 . Condition { {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonInvalidRoutesRef ) ,
Message : fmt . Sprintf ( "Cannot fetch %ss for namespace %q and matchLabels %v" , listener . Routes . Kind , gateway . Namespace , listener . Routes . Selector . MatchLabels ) ,
} }
}
if len ( tlsRoutes ) == 0 {
log . FromContext ( ctx ) . Debugf ( "No TLSRoutes found for selector %q" , selector )
return nil
}
var conditions [ ] metav1 . Condition
for _ , tlsRoute := range tlsRoutes {
for _ , routeRule := range tlsRoute . Spec . Rules {
rule , err := hostSNIRule ( routeRule )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping %s %s: cannot make route's SNI match: %v" , listener . Routes . Kind , tlsRoute . Name , err ) ,
} )
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
router := dynamic . TCPRouter {
Rule : rule ,
EntryPoints : [ ] string { ep } ,
}
if listener . TLS != nil {
// TODO support let's encrypt
router . TLS = & dynamic . RouterTCPTLSConfig {
Passthrough : listener . TLS . Mode == v1alpha1 . TLSModePassthrough ,
2020-12-15 15:40:05 +00:00
}
}
2021-05-20 09:50:12 +00:00
// Adding the gateway name and the entryPoint name prevents overlapping of routers build from the same routes.
routerName := tlsRoute . Name + "-" + gateway . Name + "-" + ep
routerKey , err := makeRouterKey ( rule , makeID ( tlsRoute . Namespace , routerName ) )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Skipping %s %s: cannot make router's key with rule %s: %v" , listener . Routes . Kind , tlsRoute . Name , router . Rule , err ) ,
} )
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
// Should not happen due to validation
// https://github.com/kubernetes-sigs/gateway-api/blob/af68a622f072811767d246ef5897135d93af0704/apis/v1alpha1/tlsroute_types.go#L79
if routeRule . ForwardTo == nil {
continue
}
wrrService , subServices , err := loadTCPServices ( client , gateway . Namespace , routeRule . ForwardTo )
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
conditions = append ( conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionResolvedRefs ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . ListenerReasonDegradedRoutes ) ,
Message : fmt . Sprintf ( "Cannot load service from %s %s/%s : %v" , listener . Routes . Kind , gateway . Namespace , tlsRoute . Name , err ) ,
} )
// TODO update the RouteStatus condition / deduplicate conditions on listener
continue
}
for svcName , svc := range subServices {
conf . TCP . Services [ svcName ] = svc
}
serviceName := provider . Normalize ( routerKey + "-wrr" )
conf . TCP . Services [ serviceName ] = wrrService
router . Service = serviceName
routerKey = provider . Normalize ( routerKey )
conf . TCP . Routers [ routerKey ] = & router
2020-12-15 15:40:05 +00:00
}
}
2021-05-20 09:50:12 +00:00
return conditions
2020-12-15 15:40:05 +00:00
}
func ( p * Provider ) makeGatewayStatus ( listenerStatuses [ ] v1alpha1 . ListenerStatus ) ( v1alpha1 . GatewayStatus , error ) {
// As Status.Addresses are not implemented yet, we initialize an empty array to follow the API expectations.
gatewayStatus := v1alpha1 . GatewayStatus {
Addresses : [ ] v1alpha1 . GatewayAddress { } ,
}
var result error
for i , listener := range listenerStatuses {
if len ( listener . Conditions ) == 0 {
// GatewayConditionReady "Ready", GatewayConditionReason "ListenerReady"
listenerStatuses [ i ] . Conditions = append ( listenerStatuses [ i ] . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . ListenerConditionReady ) ,
Status : metav1 . ConditionTrue ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : "ListenerReady" ,
Message : "No error found" ,
} )
continue
}
for _ , condition := range listener . Conditions {
result = multierror . Append ( result , errors . New ( condition . Message ) )
}
}
if result != nil {
// GatewayConditionReady "Ready", GatewayConditionReason "ListenersNotValid"
gatewayStatus . Conditions = append ( gatewayStatus . Conditions , metav1 . Condition {
Type : string ( v1alpha1 . GatewayConditionReady ) ,
Status : metav1 . ConditionFalse ,
LastTransitionTime : metav1 . Now ( ) ,
Reason : string ( v1alpha1 . GatewayReasonListenersNotValid ) ,
Message : "All Listeners must be valid" ,
} )
return gatewayStatus , result
}
gatewayStatus . Listeners = listenerStatuses
2021-03-04 08:02:03 +00:00
gatewayStatus . Conditions = append ( gatewayStatus . Conditions ,
// update "Scheduled" status with "ResourcesAvailable" reason
metav1 . Condition {
Type : string ( v1alpha1 . GatewayConditionScheduled ) ,
Status : metav1 . ConditionTrue ,
Reason : "ResourcesAvailable" ,
Message : "Resources available" ,
LastTransitionTime : metav1 . Now ( ) ,
} ,
// update "Ready" status with "ListenersValid" reason
metav1 . Condition {
Type : string ( v1alpha1 . GatewayConditionReady ) ,
Status : metav1 . ConditionTrue ,
Reason : "ListenersValid" ,
Message : "Listeners valid" ,
LastTransitionTime : metav1 . Now ( ) ,
} ,
)
2020-12-15 15:40:05 +00:00
return gatewayStatus , nil
}
2021-04-29 15:18:04 +00:00
func hostRule ( httpRouteSpec v1alpha1 . HTTPRouteSpec ) ( string , error ) {
var hostNames [ ] string
var hostRegexNames [ ] string
for _ , hostname := range httpRouteSpec . Hostnames {
host := string ( hostname )
// When unspecified, "", or *, all hostnames are matched.
// This field can be omitted for protocols that don't require hostname based matching.
// TODO Refactor this when building support for TLS options.
if host == "*" || host == "" {
return "" , nil
2020-12-15 15:40:05 +00:00
}
2021-04-29 15:18:04 +00:00
wildcard := strings . Count ( host , "*" )
if wildcard == 0 {
hostNames = append ( hostNames , host )
continue
}
2021-05-20 09:50:12 +00:00
// https://gateway-api.sigs.k8s.io/references/spec/#networking.x-k8s.io/v1alpha1.Hostname
2021-04-29 15:18:04 +00:00
if ! strings . HasPrefix ( host , "*." ) || wildcard > 1 {
return "" , fmt . Errorf ( "invalid rule: %q" , host )
}
hostRegexNames = append ( hostRegexNames , strings . Replace ( host , "*." , "{subdomain:[a-zA-Z0-9-]+}." , 1 ) )
2020-12-15 15:40:05 +00:00
}
2021-04-29 15:18:04 +00:00
var res string
if len ( hostNames ) > 0 {
res = "Host(`" + strings . Join ( hostNames , "`, `" ) + "`)"
}
if len ( hostRegexNames ) == 0 {
return res , nil
}
hostRegexp := "HostRegexp(`" + strings . Join ( hostRegexNames , "`, `" ) + "`)"
if len ( res ) > 0 {
return "(" + res + " || " + hostRegexp + ")" , nil
2020-12-15 15:40:05 +00:00
}
2021-04-29 15:18:04 +00:00
return hostRegexp , nil
2020-12-15 15:40:05 +00:00
}
2021-05-20 09:50:12 +00:00
func hostSNIRule ( rule v1alpha1 . TLSRouteRule ) ( string , error ) {
uniqHostnames := map [ string ] struct { } { }
var hostnames [ ] string
for _ , match := range rule . Matches {
for _ , hostname := range match . SNIs {
if len ( hostname ) == 0 {
continue
}
h := string ( hostname )
// first naive validation, should be improved
wildcardNb := strings . Count ( h , "*" )
if wildcardNb != 0 && ! strings . HasPrefix ( h , "*." ) || wildcardNb > 1 {
return "" , fmt . Errorf ( "invalid hostname: %q" , h )
}
hostname := "`" + h + "`"
if _ , ok := uniqHostnames [ hostname ] ; ! ok {
hostnames = append ( hostnames , hostname )
uniqHostnames [ hostname ] = struct { } { }
}
}
}
if len ( hostnames ) == 0 {
return "HostSNI(`*`)" , nil
}
return "HostSNI(" + strings . Join ( hostnames , "," ) + ")" , nil
}
2020-12-15 15:40:05 +00:00
func extractRule ( routeRule v1alpha1 . HTTPRouteRule , hostRule string ) ( string , error ) {
var rule string
var matchesRules [ ] string
for _ , match := range routeRule . Matches {
if len ( match . Path . Type ) == 0 && match . Headers == nil {
continue
}
var matchRules [ ] string
// TODO handle other path types
if len ( match . Path . Type ) > 0 {
switch match . Path . Type {
case v1alpha1 . PathMatchExact :
matchRules = append ( matchRules , "Path(`" + match . Path . Value + "`)" )
case v1alpha1 . PathMatchPrefix :
matchRules = append ( matchRules , "PathPrefix(`" + match . Path . Value + "`)" )
default :
return "" , fmt . Errorf ( "unsupported path match %s" , match . Path . Type )
}
}
// TODO handle other headers types
if match . Headers != nil {
switch match . Headers . Type {
case v1alpha1 . HeaderMatchExact :
var headerRules [ ] string
for headerName , headerValue := range match . Headers . Values {
headerRules = append ( headerRules , "Headers(`" + headerName + "`,`" + headerValue + "`)" )
}
// to have a consistent order
sort . Strings ( headerRules )
matchRules = append ( matchRules , headerRules ... )
default :
return "" , fmt . Errorf ( "unsupported header match type %s" , match . Headers . Type )
}
}
matchesRules = append ( matchesRules , strings . Join ( matchRules , " && " ) )
}
// If no matches are specified, the default is a prefix
// path match on "/", which has the effect of matching every
// HTTP request.
if len ( routeRule . Matches ) == 0 {
matchesRules = append ( matchesRules , "PathPrefix(`/`)" )
}
if hostRule != "" {
if len ( matchesRules ) == 0 {
return hostRule , nil
}
rule += hostRule + " && "
}
if len ( matchesRules ) == 1 {
return rule + matchesRules [ 0 ] , nil
}
if len ( rule ) == 0 {
return strings . Join ( matchesRules , " || " ) , nil
}
return rule + "(" + strings . Join ( matchesRules , " || " ) + ")" , nil
}
func ( p * Provider ) entryPointName ( port v1alpha1 . PortNumber , protocol v1alpha1 . ProtocolType ) ( string , error ) {
portStr := strconv . FormatInt ( int64 ( port ) , 10 )
for name , entryPoint := range p . EntryPoints {
if strings . HasSuffix ( entryPoint . Address , ":" + portStr ) {
// if the protocol is HTTP the entryPoint must have no TLS conf
2021-05-20 09:50:12 +00:00
// Not relevant for v1alpha1.TLSProtocolType && v1alpha1.TCPProtocolType
2020-12-15 15:40:05 +00:00
if protocol == v1alpha1 . HTTPProtocolType && entryPoint . HasHTTPTLSConf {
continue
}
return name , nil
}
}
return "" , fmt . Errorf ( "no matching entryPoint for port %d and protocol %q" , port , protocol )
}
func makeRouterKey ( rule , name string ) ( string , error ) {
h := sha256 . New ( )
if _ , err := h . Write ( [ ] byte ( rule ) ) ; err != nil {
return "" , err
}
key := fmt . Sprintf ( "%s-%.10x" , name , h . Sum ( nil ) )
return key , nil
}
func makeID ( namespace , name string ) string {
if namespace == "" {
return name
}
return namespace + "-" + name
}
func getTLS ( k8sClient Client , secretName , namespace string ) ( * tls . CertAndStores , error ) {
secret , exists , err := k8sClient . GetSecret ( namespace , secretName )
if err != nil {
return nil , fmt . Errorf ( "failed to fetch secret %s/%s: %w" , namespace , secretName , err )
}
if ! exists {
return nil , fmt . Errorf ( "secret %s/%s does not exist" , namespace , secretName )
}
cert , key , err := getCertificateBlocks ( secret , namespace , secretName )
if err != nil {
return nil , err
}
return & tls . CertAndStores {
Certificate : tls . Certificate {
CertFile : tls . FileOrContent ( cert ) ,
KeyFile : tls . FileOrContent ( key ) ,
} ,
} , nil
}
func getTLSConfig ( tlsConfigs map [ string ] * tls . CertAndStores ) [ ] * tls . CertAndStores {
var secretNames [ ] string
for secretName := range tlsConfigs {
secretNames = append ( secretNames , secretName )
}
sort . Strings ( secretNames )
var configs [ ] * tls . CertAndStores
for _ , secretName := range secretNames {
configs = append ( configs , tlsConfigs [ secretName ] )
}
return configs
}
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
}
// loadServices is generating a WRR service, even when there is only one target.
func loadServices ( client Client , namespace string , targets [ ] v1alpha1 . HTTPRouteForwardTo ) ( * dynamic . Service , map [ string ] * dynamic . Service , error ) {
services := map [ string ] * dynamic . Service { }
wrrSvc := & dynamic . Service {
Weighted : & dynamic . WeightedRoundRobin {
Services : [ ] dynamic . WRRService { } ,
} ,
}
for _ , forwardTo := range targets {
2021-02-02 18:36:04 +00:00
weight := int ( forwardTo . Weight )
if forwardTo . ServiceName == nil && forwardTo . BackendRef != nil {
if ! ( forwardTo . BackendRef . Group == traefikServiceGroupName && forwardTo . BackendRef . Kind == traefikServiceKind ) {
continue
}
if strings . HasSuffix ( forwardTo . BackendRef . Name , "@internal" ) {
return nil , nil , fmt . Errorf ( "traefik internal service %s is not allowed in a WRR loadbalancer" , forwardTo . BackendRef . Name )
}
wrrSvc . Weighted . Services = append ( wrrSvc . Weighted . Services , dynamic . WRRService { Name : forwardTo . BackendRef . Name , Weight : & weight } )
continue
}
2020-12-15 15:40:05 +00:00
if forwardTo . ServiceName == nil {
continue
}
svc := dynamic . Service {
LoadBalancer : & dynamic . ServersLoadBalancer {
PassHostHeader : func ( v bool ) * bool { return & v } ( true ) ,
} ,
}
service , exists , err := client . GetService ( namespace , * forwardTo . ServiceName )
if err != nil {
return nil , nil , err
}
if ! exists {
return nil , nil , errors . New ( "service not found" )
}
2021-03-15 08:44:03 +00:00
if len ( service . Spec . Ports ) > 1 && forwardTo . Port == nil {
2020-12-15 15:40:05 +00:00
// If the port is unspecified and the backend is a Service
// object consisting of multiple port definitions, the route
// must be dropped from the Gateway. The controller should
// raise the "ResolvedRefs" condition on the Gateway with the
2021-03-15 08:44:03 +00:00
// "DroppedRoutes" reason. The gateway status for this route
2020-12-15 15:40:05 +00:00
// should be updated with a condition that describes the error
// more specifically.
log . WithoutContext ( ) . Errorf ( "A multiple ports Kubernetes Service cannot be used if unspecified forwardTo.Port" )
continue
}
var portSpec corev1 . ServicePort
var match bool
for _ , p := range service . Spec . Ports {
2021-03-15 08:44:03 +00:00
if forwardTo . Port == nil || p . Port == int32 ( * forwardTo . Port ) {
2020-12-15 15:40:05 +00:00
portSpec = p
match = true
break
}
}
if ! match {
return nil , nil , errors . New ( "service port not found" )
}
endpoints , endpointsExists , endpointsErr := client . GetEndpoints ( namespace , * forwardTo . ServiceName )
if endpointsErr != nil {
return nil , nil , endpointsErr
}
if ! endpointsExists {
return nil , nil , errors . New ( "endpoints not found" )
}
if len ( endpoints . Subsets ) == 0 {
return nil , nil , errors . New ( "subset not found" )
}
var port int32
var portStr string
for _ , subset := range endpoints . Subsets {
for _ , p := range subset . Ports {
2021-05-20 09:50:12 +00:00
if portSpec . Name == p . Name {
2020-12-15 15:40:05 +00:00
port = p . Port
break
}
}
if port == 0 {
return nil , nil , errors . New ( "cannot define a port" )
}
2021-05-20 09:50:12 +00:00
protocol := getProtocol ( portSpec )
2020-12-15 15:40:05 +00:00
portStr = strconv . FormatInt ( int64 ( port ) , 10 )
for _ , addr := range subset . Addresses {
svc . LoadBalancer . Servers = append ( svc . LoadBalancer . Servers , dynamic . Server {
URL : fmt . Sprintf ( "%s://%s" , protocol , net . JoinHostPort ( addr . IP , portStr ) ) ,
} )
}
}
serviceName := provider . Normalize ( makeID ( service . Namespace , service . Name ) + "-" + portStr )
services [ serviceName ] = & svc
wrrSvc . Weighted . Services = append ( wrrSvc . Weighted . Services , dynamic . WRRService { Name : serviceName , Weight : & weight } )
}
2021-02-02 18:36:04 +00:00
if len ( wrrSvc . Weighted . Services ) == 0 {
2020-12-15 15:40:05 +00:00
return nil , nil , errors . New ( "no service has been created" )
}
return wrrSvc , services , nil
}
2021-05-20 09:50:12 +00:00
// loadTCPServices is generating a WRR service, even when there is only one target.
func loadTCPServices ( client Client , namespace string , targets [ ] v1alpha1 . RouteForwardTo ) ( * dynamic . TCPService , map [ string ] * dynamic . TCPService , error ) {
services := map [ string ] * dynamic . TCPService { }
wrrSvc := & dynamic . TCPService {
Weighted : & dynamic . TCPWeightedRoundRobin {
Services : [ ] dynamic . TCPWRRService { } ,
} ,
}
for _ , forwardTo := range targets {
weight := int ( forwardTo . Weight )
if forwardTo . ServiceName == nil && forwardTo . BackendRef != nil {
if ! ( forwardTo . BackendRef . Group == traefikServiceGroupName && forwardTo . BackendRef . Kind == traefikServiceKind ) {
continue
}
if strings . HasSuffix ( forwardTo . BackendRef . Name , "@internal" ) {
return nil , nil , fmt . Errorf ( "traefik internal service %s is not allowed in a TCP service" , forwardTo . BackendRef . Name )
}
wrrSvc . Weighted . Services = append ( wrrSvc . Weighted . Services , dynamic . TCPWRRService { Name : forwardTo . BackendRef . Name , Weight : & weight } )
continue
}
if forwardTo . ServiceName == nil {
continue
}
svc := dynamic . TCPService {
LoadBalancer : & dynamic . TCPServersLoadBalancer { } ,
}
service , exists , err := client . GetService ( namespace , * forwardTo . ServiceName )
if err != nil {
return nil , nil , err
}
if ! exists {
return nil , nil , errors . New ( "service not found" )
}
if len ( service . Spec . Ports ) > 1 && forwardTo . Port == nil {
// If the port is unspecified and the backend is a Service
// object consisting of multiple port definitions, the route
// must be dropped from the Gateway. The controller should
// raise the "ResolvedRefs" condition on the Gateway with the
// "DroppedRoutes" reason. The gateway status for this route
// should be updated with a condition that describes the error
// more specifically.
log . WithoutContext ( ) . Errorf ( "A multiple ports Kubernetes Service cannot be used if unspecified forwardTo.Port" )
continue
}
var portSpec corev1 . ServicePort
var match bool
for _ , p := range service . Spec . Ports {
if forwardTo . Port == nil || p . Port == int32 ( * forwardTo . Port ) {
portSpec = p
match = true
break
}
}
if ! match {
return nil , nil , errors . New ( "service port not found" )
}
endpoints , endpointsExists , endpointsErr := client . GetEndpoints ( namespace , * forwardTo . ServiceName )
if endpointsErr != nil {
return nil , nil , endpointsErr
}
if ! endpointsExists {
return nil , nil , errors . New ( "endpoints not found" )
}
if len ( endpoints . Subsets ) == 0 {
return nil , nil , errors . New ( "subset not found" )
}
var port int32
var portStr string
for _ , subset := range endpoints . Subsets {
for _ , p := range subset . Ports {
if portSpec . Name == p . Name {
port = p . Port
break
}
}
if port == 0 {
return nil , nil , errors . New ( "cannot define a port" )
}
portStr = strconv . FormatInt ( int64 ( port ) , 10 )
for _ , addr := range subset . Addresses {
svc . LoadBalancer . Servers = append ( svc . LoadBalancer . Servers , dynamic . TCPServer {
Address : net . JoinHostPort ( addr . IP , portStr ) ,
} )
}
}
serviceName := provider . Normalize ( makeID ( service . Namespace , service . Name ) + "-" + portStr )
services [ serviceName ] = & svc
wrrSvc . Weighted . Services = append ( wrrSvc . Weighted . Services , dynamic . TCPWRRService { Name : serviceName , Weight : & weight } )
}
if len ( wrrSvc . Weighted . Services ) == 0 {
return nil , nil , errors . New ( "no service has been created" )
}
return wrrSvc , services , nil
}
func getProtocol ( portSpec corev1 . ServicePort ) string {
2020-12-15 15:40:05 +00:00
protocol := "http"
2021-05-20 09:50:12 +00:00
if portSpec . Port == 443 || strings . HasPrefix ( portSpec . Name , "https" ) {
2020-12-15 15:40:05 +00:00
protocol = "https"
}
return protocol
}
func throttleEvents ( ctx context . Context , throttleDuration time . Duration , pool * safe . Pool , eventsChan <- chan interface { } ) chan interface { } {
if throttleDuration == 0 {
return nil
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling)
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.
pool . GoCtx ( func ( ctxPool context . Context ) {
for {
select {
case <- ctxPool . Done ( ) :
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 don't do different things for different events
log . FromContext ( ctx ) . Debugf ( "Dropping event kind %T due to throttling" , nextEvent )
}
}
}
} )
return eventsChanBuffered
}
2021-02-02 18:36:04 +00:00
func isInternalService ( forwardTo v1alpha1 . HTTPRouteForwardTo ) bool {
return forwardTo . ServiceName == nil &&
forwardTo . BackendRef != nil &&
forwardTo . BackendRef . Kind == traefikServiceKind &&
forwardTo . BackendRef . Group == traefikServiceGroupName &&
strings . HasSuffix ( forwardTo . BackendRef . Name , "@internal" )
}