2019-03-14 14:56:06 +00:00
package crd
2019-02-21 22:08:05 +00:00
import (
2019-09-05 11:42:04 +00:00
"bufio"
"bytes"
2019-02-21 22:08:05 +00:00
"context"
2021-09-14 13:16:11 +00:00
"crypto/sha1"
2019-03-14 14:56:06 +00:00
"crypto/sha256"
2021-09-14 13:16:11 +00:00
"encoding/base64"
2021-03-03 14:32:04 +00:00
"encoding/json"
2020-03-10 11:46:05 +00:00
"errors"
2019-02-21 22:08:05 +00:00
"fmt"
"os"
"sort"
"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"
2020-08-17 16:04:03 +00:00
ptypes "github.com/traefik/paerser/types"
2020-09-16 13:46:04 +00:00
"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/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/tls"
2021-10-26 08:54:11 +00:00
"github.com/traefik/traefik/v2/pkg/types"
2019-02-21 22:08:05 +00:00
corev1 "k8s.io/api/core/v1"
2021-03-03 14:32:04 +00:00
apiextensionv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2019-02-21 22:08:05 +00:00
"k8s.io/apimachinery/pkg/labels"
2021-01-15 14:54:04 +00:00
"k8s.io/apimachinery/pkg/util/intstr"
2019-02-21 22:08:05 +00:00
)
const (
annotationKubernetesIngressClass = "kubernetes.io/ingress.class"
traefikDefaultIngressClass = "traefik"
)
2019-11-14 18:28:04 +00:00
const (
providerName = "kubernetescrd"
providerNamespaceSeparator = "@"
)
2019-02-21 22:08:05 +00:00
// Provider holds configurations of the provider.
type Provider struct {
2021-07-13 10:54:09 +00:00
Endpoint string ` description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" `
2022-01-24 10:08:05 +00:00
Token string ` description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false" `
2021-07-13 10:54:09 +00:00
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" `
AllowCrossNamespace bool ` description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true" `
AllowExternalNameServices bool ` description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true" `
LabelSelector string ` description:"Kubernetes 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 to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true" `
ThrottleDuration ptypes . Duration ` description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true" `
2022-03-07 10:08:07 +00:00
AllowEmptyServices bool ` description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true" `
2021-07-13 10:54:09 +00:00
lastConfiguration safe . Safe
2020-12-11 09:58:00 +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 label selector: %q" , p . LabelSelector )
2019-02-21 22:08:05 +00:00
}
2020-11-19 23:18:04 +00:00
log . FromContext ( ctx ) . Infof ( "label selector is: %q" , p . LabelSelector )
2019-02-21 22:08:05 +00:00
withEndpoint := ""
if p . Endpoint != "" {
2019-11-14 18:28:04 +00:00
withEndpoint = fmt . Sprintf ( " with endpoint %s" , p . Endpoint )
2019-02-21 22:08:05 +00:00
}
2019-03-14 14:56:06 +00:00
var client * clientWrapper
2019-03-11 13:54:05 +00:00
switch {
case os . Getenv ( "KUBERNETES_SERVICE_HOST" ) != "" && os . Getenv ( "KUBERNETES_SERVICE_PORT" ) != "" :
2019-02-21 22:08:05 +00:00
log . FromContext ( ctx ) . Infof ( "Creating in-cluster Provider client%s" , withEndpoint )
2019-03-14 14:56:06 +00:00
client , err = newInClusterClient ( p . Endpoint )
2019-03-11 13:54:05 +00:00
case os . Getenv ( "KUBECONFIG" ) != "" :
log . FromContext ( ctx ) . Infof ( "Creating cluster-external Provider client from KUBECONFIG %s" , os . Getenv ( "KUBECONFIG" ) )
2019-03-14 14:56:06 +00:00
client , err = newExternalClusterClientFromFile ( os . Getenv ( "KUBECONFIG" ) )
2019-03-11 13:54:05 +00:00
default :
2019-02-21 22:08:05 +00:00
log . FromContext ( ctx ) . Infof ( "Creating cluster-external Provider client%s" , withEndpoint )
2019-03-14 14:56:06 +00:00
client , err = newExternalClusterClient ( p . Endpoint , p . Token , p . CertAuthFilePath )
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
client . labelSelector = p . LabelSelector
return client , 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 {
2019-11-14 18:28:04 +00:00
ctxLog := log . With ( context . Background ( ) , log . Str ( log . ProviderName , providerName ) )
2019-02-21 22:08:05 +00:00
logger := log . FromContext ( ctxLog )
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 08:48:05 +00:00
if p . AllowCrossNamespace {
2020-12-10 13:58:04 +00:00
logger . Warn ( "Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)" )
}
2021-07-13 10:54:09 +00:00
if p . AllowExternalNameServices {
logger . Warn ( "ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)" )
}
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 {
logger . Errorf ( "Error watching kubernetes events: %v" , err )
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-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-11-14 18:28: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-06-21 15:18:05 +00:00
conf := p . loadConfigurationFromCRD ( ctxLog , k8sClient )
2019-02-21 22:08:05 +00:00
2019-10-25 13:46:05 +00:00
confHash , err := hashstructure . Hash ( conf , nil )
switch {
case err != nil :
logger . Error ( "Unable to hash the configuration" )
case p . lastConfiguration . Get ( ) == confHash :
2019-02-21 22:08:05 +00:00
logger . Debugf ( "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-11-14 18:28:04 +00:00
ProviderName : providerName ,
2019-02-21 22:08:05 +00:00
Configuration : conf ,
}
}
2019-08-30 10:16:04 +00:00
2019-11-14 18:28: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).
2019-08-30 10:16:04 +00:00
time . Sleep ( throttleDuration )
2019-02-21 22:08:05 +00:00
}
}
}
notify := func ( err error , time time . Duration ) {
2019-11-14 18:28:04 +00:00
logger . Errorf ( "Provider connection error: %v; retrying in %s" , err , 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 {
2019-11-14 18:28:04 +00:00
logger . Errorf ( "Cannot connect to Provider: %v" , err )
2019-02-21 22:08:05 +00:00
}
} )
return nil
}
2019-08-26 08:30:05 +00:00
func ( p * Provider ) loadConfigurationFromCRD ( ctx context . Context , client Client ) * dynamic . Configuration {
tlsConfigs := make ( map [ string ] * tls . CertAndStores )
conf := & dynamic . Configuration {
HTTP : p . loadIngressRouteConfiguration ( ctx , client , tlsConfigs ) ,
TCP : p . loadIngressRouteTCPConfiguration ( ctx , client , tlsConfigs ) ,
2020-02-26 11:28:05 +00:00
UDP : p . loadIngressRouteUDPConfiguration ( ctx , client ) ,
2019-08-26 08:30:05 +00:00
TLS : & dynamic . TLSConfiguration {
Certificates : getTLSConfig ( tlsConfigs ) ,
Options : buildTLSOptions ( ctx , client ) ,
2020-02-24 16:14:06 +00:00
Stores : buildTLSStores ( ctx , client ) ,
2019-08-26 08:30:05 +00:00
} ,
2019-02-21 22:08:05 +00:00
}
2019-08-26 08:30:05 +00:00
for _ , middleware := range client . GetMiddlewares ( ) {
2019-11-14 18:28:04 +00:00
id := provider . Normalize ( makeID ( middleware . Namespace , middleware . Name ) )
2019-09-03 17:20:04 +00:00
ctxMid := log . With ( ctx , log . Str ( log . MiddlewareName , id ) )
2019-09-05 11:42:04 +00:00
basicAuth , err := createBasicAuthMiddleware ( client , middleware . Namespace , middleware . Spec . BasicAuth )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading basic auth middleware: %v" , err )
continue
}
digestAuth , err := createDigestAuthMiddleware ( client , middleware . Namespace , middleware . Spec . DigestAuth )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading digest auth middleware: %v" , err )
continue
}
forwardAuth , err := createForwardAuthMiddleware ( client , middleware . Namespace , middleware . Spec . ForwardAuth )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading forward auth middleware: %v" , err )
continue
}
2020-12-10 13:58:04 +00:00
errorPage , errorPageService , err := p . createErrorPageMiddleware ( client , middleware . Namespace , middleware . Spec . Errors )
2019-09-10 15:24:03 +00:00
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading error page middleware: %v" , err )
continue
}
if errorPage != nil && errorPageService != nil {
serviceName := id + "-errorpage-service"
errorPage . Service = serviceName
conf . HTTP . Services [ serviceName ] = errorPageService
}
2021-03-03 14:32:04 +00:00
plugin , err := createPluginMiddleware ( middleware . Spec . Plugin )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading plugins middleware: %v" , err )
continue
}
rateLimit , err := createRateLimitMiddleware ( middleware . Spec . RateLimit )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading rateLimit middleware: %v" , err )
continue
}
retry , err := createRetryMiddleware ( middleware . Spec . Retry )
if err != nil {
log . FromContext ( ctxMid ) . Errorf ( "Error while reading retry middleware: %v" , err )
continue
}
2019-09-03 17:20:04 +00:00
conf . HTTP . Middlewares [ id ] = & dynamic . Middleware {
AddPrefix : middleware . Spec . AddPrefix ,
StripPrefix : middleware . Spec . StripPrefix ,
StripPrefixRegex : middleware . Spec . StripPrefixRegex ,
ReplacePath : middleware . Spec . ReplacePath ,
ReplacePathRegex : middleware . Spec . ReplacePathRegex ,
Chain : createChainMiddleware ( ctxMid , middleware . Namespace , middleware . Spec . Chain ) ,
IPWhiteList : middleware . Spec . IPWhiteList ,
Headers : middleware . Spec . Headers ,
2019-09-10 15:24:03 +00:00
Errors : errorPage ,
2021-03-03 14:32:04 +00:00
RateLimit : rateLimit ,
2019-09-03 17:20:04 +00:00
RedirectRegex : middleware . Spec . RedirectRegex ,
RedirectScheme : middleware . Spec . RedirectScheme ,
2019-09-05 11:42:04 +00:00
BasicAuth : basicAuth ,
DigestAuth : digestAuth ,
ForwardAuth : forwardAuth ,
2019-09-03 17:20:04 +00:00
InFlightReq : middleware . Spec . InFlightReq ,
Buffering : middleware . Spec . Buffering ,
CircuitBreaker : middleware . Spec . CircuitBreaker ,
Compress : middleware . Spec . Compress ,
PassTLSClientCert : middleware . Spec . PassTLSClientCert ,
2021-03-03 14:32:04 +00:00
Retry : retry ,
2020-07-13 10:30:03 +00:00
ContentType : middleware . Spec . ContentType ,
2021-03-03 14:32:04 +00:00
Plugin : plugin ,
2019-09-03 17:20:04 +00:00
}
2019-02-21 22:08:05 +00:00
}
2021-06-11 13:30:05 +00:00
for _ , middlewareTCP := range client . GetMiddlewareTCPs ( ) {
id := provider . Normalize ( makeID ( middlewareTCP . Namespace , middlewareTCP . Name ) )
conf . TCP . Middlewares [ id ] = & dynamic . TCPMiddleware {
2021-11-29 16:12:06 +00:00
InFlightConn : middlewareTCP . Spec . InFlightConn ,
IPWhiteList : middlewareTCP . Spec . IPWhiteList ,
2021-06-11 13:30:05 +00:00
}
}
2021-07-13 10:54:09 +00:00
cb := configBuilder { client : client , allowCrossNamespace : p . AllowCrossNamespace , allowExternalNameServices : p . AllowExternalNameServices }
2020-09-11 13:40:03 +00:00
2019-11-14 18:28:04 +00:00
for _ , service := range client . GetTraefikServices ( ) {
err := cb . buildTraefikService ( ctx , service , conf . HTTP . Services )
if err != nil {
log . FromContext ( ctx ) . WithField ( log . ServiceName , service . Name ) .
Errorf ( "Error while building TraefikService: %v" , err )
continue
}
}
2020-09-11 13:40:03 +00:00
for _ , serversTransport := range client . GetServersTransports ( ) {
logger := log . FromContext ( ctx ) . WithField ( log . ServersTransportName , serversTransport . Name )
var rootCAs [ ] tls . FileOrContent
for _ , secret := range serversTransport . Spec . RootCAsSecrets {
caSecret , err := loadCASecret ( serversTransport . Namespace , secret , client )
if err != nil {
logger . Errorf ( "Error while loading rootCAs %s: %v" , secret , err )
continue
}
rootCAs = append ( rootCAs , tls . FileOrContent ( caSecret ) )
}
var certs tls . Certificates
for _ , secret := range serversTransport . Spec . CertificatesSecrets {
tlsSecret , tlsKey , err := loadAuthTLSSecret ( serversTransport . Namespace , secret , client )
if err != nil {
logger . Errorf ( "Error while loading certificates %s: %v" , secret , err )
continue
}
certs = append ( certs , tls . Certificate {
CertFile : tls . FileOrContent ( tlsSecret ) ,
KeyFile : tls . FileOrContent ( tlsKey ) ,
} )
}
forwardingTimeout := & dynamic . ForwardingTimeouts { }
forwardingTimeout . SetDefaults ( )
if serversTransport . Spec . ForwardingTimeouts != nil {
if serversTransport . Spec . ForwardingTimeouts . DialTimeout != nil {
err := forwardingTimeout . DialTimeout . Set ( serversTransport . Spec . ForwardingTimeouts . DialTimeout . String ( ) )
if err != nil {
logger . Errorf ( "Error while reading DialTimeout: %v" , err )
}
}
if serversTransport . Spec . ForwardingTimeouts . ResponseHeaderTimeout != nil {
err := forwardingTimeout . ResponseHeaderTimeout . Set ( serversTransport . Spec . ForwardingTimeouts . ResponseHeaderTimeout . String ( ) )
if err != nil {
logger . Errorf ( "Error while reading ResponseHeaderTimeout: %v" , err )
}
}
if serversTransport . Spec . ForwardingTimeouts . IdleConnTimeout != nil {
err := forwardingTimeout . IdleConnTimeout . Set ( serversTransport . Spec . ForwardingTimeouts . IdleConnTimeout . String ( ) )
if err != nil {
logger . Errorf ( "Error while reading IdleConnTimeout: %v" , err )
}
}
2021-11-09 11:16:08 +00:00
if serversTransport . Spec . ForwardingTimeouts . ReadIdleTimeout != nil {
err := forwardingTimeout . ReadIdleTimeout . Set ( serversTransport . Spec . ForwardingTimeouts . ReadIdleTimeout . String ( ) )
if err != nil {
logger . Errorf ( "Error while reading ReadIdleTimeout: %v" , err )
}
}
if serversTransport . Spec . ForwardingTimeouts . PingTimeout != nil {
err := forwardingTimeout . PingTimeout . Set ( serversTransport . Spec . ForwardingTimeouts . PingTimeout . String ( ) )
if err != nil {
logger . Errorf ( "Error while reading PingTimeout: %v" , err )
}
}
2020-09-11 13:40:03 +00:00
}
2021-09-16 13:12:13 +00:00
id := provider . Normalize ( makeID ( serversTransport . Namespace , serversTransport . Name ) )
conf . HTTP . ServersTransports [ id ] = & dynamic . ServersTransport {
2020-09-11 13:40:03 +00:00
ServerName : serversTransport . Spec . ServerName ,
InsecureSkipVerify : serversTransport . Spec . InsecureSkipVerify ,
RootCAs : rootCAs ,
Certificates : certs ,
2021-09-16 10:18:08 +00:00
DisableHTTP2 : serversTransport . Spec . DisableHTTP2 ,
2020-09-11 13:40:03 +00:00
MaxIdleConnsPerHost : serversTransport . Spec . MaxIdleConnsPerHost ,
ForwardingTimeouts : forwardingTimeout ,
2021-09-17 06:56:07 +00:00
PeerCertURI : serversTransport . Spec . PeerCertURI ,
2020-09-11 13:40:03 +00:00
}
}
2019-08-26 08:30:05 +00:00
return conf
2019-02-21 22:08:05 +00:00
}
2021-01-15 14:54:04 +00:00
func getServicePort ( svc * corev1 . Service , port intstr . IntOrString ) ( * corev1 . ServicePort , error ) {
2020-03-10 11:46:05 +00:00
if svc == nil {
return nil , errors . New ( "service is not defined" )
}
2021-01-15 14:54:04 +00:00
if ( port . Type == intstr . Int && port . IntVal == 0 ) || ( port . Type == intstr . String && port . StrVal == "" ) {
2020-03-10 11:46:05 +00:00
return nil , errors . New ( "ingressRoute service port not defined" )
}
hasValidPort := false
for _ , p := range svc . Spec . Ports {
2021-01-15 14:54:04 +00:00
if ( port . Type == intstr . Int && port . IntVal == p . Port ) || ( port . Type == intstr . String && port . StrVal == p . Name ) {
2020-03-10 11:46:05 +00:00
return & p , nil
}
if p . Port != 0 {
hasValidPort = true
}
}
2021-01-15 14:54:04 +00:00
if svc . Spec . Type != corev1 . ServiceTypeExternalName || port . Type == intstr . String {
return nil , fmt . Errorf ( "service port not found: %s" , & port )
2020-03-10 11:46:05 +00:00
}
if hasValidPort {
log . WithoutContext ( ) .
Warning ( "The port %d from IngressRoute doesn't match with ports defined in the ExternalName service %s/%s." , port , svc . Namespace , svc . Name )
}
2021-01-15 14:54:04 +00:00
return & corev1 . ServicePort { Port : port . IntVal } , nil
2020-03-10 11:46:05 +00:00
}
2021-03-03 14:32:04 +00:00
func createPluginMiddleware ( plugins map [ string ] apiextensionv1 . JSON ) ( map [ string ] dynamic . PluginConf , error ) {
if plugins == nil {
return nil , nil
}
data , err := json . Marshal ( plugins )
if err != nil {
return nil , err
}
pc := map [ string ] dynamic . PluginConf { }
err = json . Unmarshal ( data , & pc )
if err != nil {
return nil , err
}
return pc , nil
}
func createRateLimitMiddleware ( rateLimit * v1alpha1 . RateLimit ) ( * dynamic . RateLimit , error ) {
if rateLimit == nil {
return nil , nil
}
rl := & dynamic . RateLimit { Average : rateLimit . Average }
rl . SetDefaults ( )
if rateLimit . Burst != nil {
rl . Burst = * rateLimit . Burst
}
if rateLimit . Period != nil {
err := rl . Period . Set ( rateLimit . Period . String ( ) )
if err != nil {
return nil , err
}
}
2021-11-26 11:10:11 +00:00
if rateLimit . SourceCriterion != nil {
rl . SourceCriterion = rateLimit . SourceCriterion
}
2021-03-03 14:32:04 +00:00
return rl , nil
}
func createRetryMiddleware ( retry * v1alpha1 . Retry ) ( * dynamic . Retry , error ) {
if retry == nil {
return nil , nil
}
r := & dynamic . Retry { Attempts : retry . Attempts }
err := r . InitialInterval . Set ( retry . InitialInterval . String ( ) )
if err != nil {
return nil , err
}
return r , nil
}
2020-12-10 13:58:04 +00:00
func ( p * Provider ) createErrorPageMiddleware ( client Client , namespace string , errorPage * v1alpha1 . ErrorPage ) ( * dynamic . ErrorPage , * dynamic . Service , error ) {
2019-09-10 15:24:03 +00:00
if errorPage == nil {
return nil , nil , nil
}
errorPageMiddleware := & dynamic . ErrorPage {
Status : errorPage . Status ,
Query : errorPage . Query ,
}
2021-07-13 10:54:09 +00:00
balancerServerHTTP , err := configBuilder { client : client , allowCrossNamespace : p . AllowCrossNamespace , allowExternalNameServices : p . AllowExternalNameServices } . buildServersLB ( namespace , errorPage . Service . LoadBalancerSpec )
2019-09-10 15:24:03 +00:00
if err != nil {
return nil , nil , err
}
return errorPageMiddleware , balancerServerHTTP , nil
}
2019-09-05 11:42:04 +00:00
func createForwardAuthMiddleware ( k8sClient Client , namespace string , auth * v1alpha1 . ForwardAuth ) ( * dynamic . ForwardAuth , error ) {
if auth == nil {
return nil , nil
}
if len ( auth . Address ) == 0 {
return nil , fmt . Errorf ( "forward authentication requires an address" )
}
forwardAuth := & dynamic . ForwardAuth {
2020-10-29 14:10:04 +00:00
Address : auth . Address ,
TrustForwardHeader : auth . TrustForwardHeader ,
AuthResponseHeaders : auth . AuthResponseHeaders ,
AuthResponseHeadersRegex : auth . AuthResponseHeadersRegex ,
AuthRequestHeaders : auth . AuthRequestHeaders ,
2019-09-05 11:42:04 +00:00
}
if auth . TLS == nil {
return forwardAuth , nil
}
2021-10-26 08:54:11 +00:00
forwardAuth . TLS = & types . ClientTLS {
2019-09-05 11:42:04 +00:00
CAOptional : auth . TLS . CAOptional ,
InsecureSkipVerify : auth . TLS . InsecureSkipVerify ,
}
if len ( auth . TLS . CASecret ) > 0 {
caSecret , err := loadCASecret ( namespace , auth . TLS . CASecret , k8sClient )
if err != nil {
2020-05-11 10:06:07 +00:00
return nil , fmt . Errorf ( "failed to load auth ca secret: %w" , err )
2019-09-05 11:42:04 +00:00
}
forwardAuth . TLS . CA = caSecret
}
if len ( auth . TLS . CertSecret ) > 0 {
authSecretCert , authSecretKey , err := loadAuthTLSSecret ( namespace , auth . TLS . CertSecret , k8sClient )
if err != nil {
2020-05-11 10:06:07 +00:00
return nil , fmt . Errorf ( "failed to load auth secret: %w" , err )
2019-09-05 11:42:04 +00:00
}
forwardAuth . TLS . Cert = authSecretCert
forwardAuth . TLS . Key = authSecretKey
}
return forwardAuth , nil
}
func loadCASecret ( namespace , secretName string , k8sClient Client ) ( string , error ) {
secret , ok , err := k8sClient . GetSecret ( namespace , secretName )
if err != nil {
2020-05-11 10:06:07 +00:00
return "" , fmt . Errorf ( "failed to fetch secret '%s/%s': %w" , namespace , secretName , err )
2019-09-05 11:42:04 +00:00
}
2021-06-14 16:06:10 +00:00
2019-09-05 11:42:04 +00:00
if ! ok {
return "" , fmt . Errorf ( "secret '%s/%s' not found" , namespace , secretName )
}
2021-06-14 16:06:10 +00:00
2019-09-05 11:42:04 +00:00
if secret == nil {
return "" , fmt . Errorf ( "data for secret '%s/%s' must not be nil" , namespace , secretName )
}
2021-06-14 16:06:10 +00:00
tlsCAData , err := getCABlocks ( secret , namespace , secretName )
if err == nil {
return tlsCAData , nil
2019-09-05 11:42:04 +00:00
}
2021-06-14 16:06:10 +00:00
// TODO: remove this behavior in the next major version (v3)
if len ( secret . Data ) == 1 {
// For backwards compatibility, use the only available secret data as CA if both 'ca.crt' and 'tls.ca' are missing.
for _ , v := range secret . Data {
return string ( v ) , nil
}
2019-09-05 11:42:04 +00:00
}
2021-06-14 16:06:10 +00:00
return "" , fmt . Errorf ( "could not find CA block: %w" , err )
2019-09-05 11:42:04 +00:00
}
func loadAuthTLSSecret ( namespace , secretName string , k8sClient Client ) ( string , string , error ) {
secret , exists , err := k8sClient . GetSecret ( namespace , secretName )
if err != nil {
2020-05-11 10:06:07 +00:00
return "" , "" , fmt . Errorf ( "failed to fetch secret '%s/%s': %w" , namespace , secretName , err )
2019-09-05 11:42:04 +00:00
}
2021-06-14 16:06:10 +00:00
2019-09-05 11:42:04 +00:00
if ! exists {
return "" , "" , fmt . Errorf ( "secret '%s/%s' does not exist" , namespace , secretName )
}
2021-06-14 16:06:10 +00:00
2019-09-05 11:42:04 +00:00
if secret == nil {
return "" , "" , fmt . Errorf ( "data for secret '%s/%s' must not be nil" , namespace , secretName )
}
return getCertificateBlocks ( secret , namespace , secretName )
}
func createBasicAuthMiddleware ( client Client , namespace string , basicAuth * v1alpha1 . BasicAuth ) ( * dynamic . BasicAuth , error ) {
if basicAuth == nil {
return nil , nil
}
2021-09-14 13:16:11 +00:00
if basicAuth . Secret == "" {
return nil , fmt . Errorf ( "auth secret must be set" )
}
secret , ok , err := client . GetSecret ( namespace , basicAuth . Secret )
2019-09-05 11:42:04 +00:00
if err != nil {
2021-09-14 13:16:11 +00:00
return nil , fmt . Errorf ( "failed to fetch secret '%s/%s': %w" , namespace , basicAuth . Secret , err )
}
if ! ok {
return nil , fmt . Errorf ( "secret '%s/%s' not found" , namespace , basicAuth . Secret )
}
if secret == nil {
return nil , fmt . Errorf ( "data for secret '%s/%s' must not be nil" , namespace , basicAuth . Secret )
}
if secret . Type == corev1 . SecretTypeBasicAuth {
credentials , err := loadBasicAuthCredentials ( secret )
if err != nil {
return nil , fmt . Errorf ( "failed to load basic auth credentials: %w" , err )
}
return & dynamic . BasicAuth {
Users : credentials ,
Realm : basicAuth . Realm ,
RemoveHeader : basicAuth . RemoveHeader ,
HeaderField : basicAuth . HeaderField ,
} , nil
}
credentials , err := loadAuthCredentials ( secret )
if err != nil {
return nil , fmt . Errorf ( "failed to load basic auth credentials: %w" , err )
2019-09-05 11:42:04 +00:00
}
return & dynamic . BasicAuth {
Users : credentials ,
Realm : basicAuth . Realm ,
RemoveHeader : basicAuth . RemoveHeader ,
HeaderField : basicAuth . HeaderField ,
} , nil
}
func createDigestAuthMiddleware ( client Client , namespace string , digestAuth * v1alpha1 . DigestAuth ) ( * dynamic . DigestAuth , error ) {
if digestAuth == nil {
return nil , nil
}
2021-09-14 13:16:11 +00:00
if digestAuth . Secret == "" {
return nil , fmt . Errorf ( "auth secret must be set" )
}
secret , ok , err := client . GetSecret ( namespace , digestAuth . Secret )
2019-09-05 11:42:04 +00:00
if err != nil {
2021-09-14 13:16:11 +00:00
return nil , fmt . Errorf ( "failed to fetch secret '%s/%s': %w" , namespace , digestAuth . Secret , err )
}
if ! ok {
return nil , fmt . Errorf ( "secret '%s/%s' not found" , namespace , digestAuth . Secret )
}
if secret == nil {
return nil , fmt . Errorf ( "data for secret '%s/%s' must not be nil" , namespace , digestAuth . Secret )
}
credentials , err := loadAuthCredentials ( secret )
if err != nil {
return nil , fmt . Errorf ( "failed to load digest auth credentials: %w" , err )
2019-09-05 11:42:04 +00:00
}
return & dynamic . DigestAuth {
Users : credentials ,
Realm : digestAuth . Realm ,
RemoveHeader : digestAuth . RemoveHeader ,
HeaderField : digestAuth . HeaderField ,
} , nil
}
2021-09-14 13:16:11 +00:00
func loadBasicAuthCredentials ( secret * corev1 . Secret ) ( [ ] string , error ) {
username , usernameExists := secret . Data [ "username" ]
password , passwordExists := secret . Data [ "password" ]
if ! ( usernameExists && passwordExists ) {
return nil , fmt . Errorf ( "secret '%s/%s' must contain both username and password keys" , secret . Namespace , secret . Name )
2019-09-05 11:42:04 +00:00
}
2021-09-14 13:16:11 +00:00
hash := sha1 . New ( )
hash . Write ( password )
passwordSum := base64 . StdEncoding . EncodeToString ( hash . Sum ( nil ) )
2019-09-05 11:42:04 +00:00
2021-09-14 13:16:11 +00:00
return [ ] string { fmt . Sprintf ( "%s:{SHA}%s" , username , passwordSum ) } , nil
2019-09-05 11:42:04 +00:00
}
2021-09-14 13:16:11 +00:00
func loadAuthCredentials ( secret * corev1 . Secret ) ( [ ] string , error ) {
2019-09-05 11:42:04 +00:00
if len ( secret . Data ) != 1 {
2021-09-14 13:16:11 +00:00
return nil , fmt . Errorf ( "found %d elements for secret '%s/%s', must be single element exactly" , len ( secret . Data ) , secret . Namespace , secret . Name )
2019-09-05 11:42:04 +00:00
}
var firstSecret [ ] byte
for _ , v := range secret . Data {
firstSecret = v
break
}
var credentials [ ] string
scanner := bufio . NewScanner ( bytes . NewReader ( firstSecret ) )
for scanner . Scan ( ) {
if cred := scanner . Text ( ) ; len ( cred ) > 0 {
credentials = append ( credentials , cred )
}
}
if err := scanner . Err ( ) ; err != nil {
2021-09-14 13:16:11 +00:00
return nil , fmt . Errorf ( "error reading secret for %s/%s: %w" , secret . Namespace , secret . Name , err )
2019-09-05 11:42:04 +00:00
}
if len ( credentials ) == 0 {
2021-09-14 13:16:11 +00:00
return nil , fmt . Errorf ( "secret '%s/%s' does not contain any credentials" , secret . Namespace , secret . Name )
2019-09-05 11:42:04 +00:00
}
return credentials , nil
}
2019-09-03 17:20:04 +00:00
func createChainMiddleware ( ctx context . Context , namespace string , chain * v1alpha1 . Chain ) * dynamic . Chain {
if chain == nil {
return nil
}
var mds [ ] string
for _ , mi := range chain . Middlewares {
2019-11-14 18:28:04 +00:00
if strings . Contains ( mi . Name , providerNamespaceSeparator ) {
2019-09-03 17:20:04 +00:00
if len ( mi . Namespace ) > 0 {
log . FromContext ( ctx ) .
Warnf ( "namespace %q is ignored in cross-provider context" , mi . Namespace )
}
mds = append ( mds , mi . Name )
continue
}
ns := mi . Namespace
if len ( ns ) == 0 {
ns = namespace
}
mds = append ( mds , makeID ( ns , mi . Name ) )
}
return & dynamic . Chain { Middlewares : mds }
}
2019-06-27 21:58:03 +00:00
func buildTLSOptions ( ctx context . Context , client Client ) map [ string ] tls . Options {
2019-06-21 15:18:05 +00:00
tlsOptionsCRD := client . GetTLSOptions ( )
2019-06-27 21:58:03 +00:00
var tlsOptions map [ string ] tls . Options
2019-06-21 15:18:05 +00:00
if len ( tlsOptionsCRD ) == 0 {
return tlsOptions
}
2019-06-27 21:58:03 +00:00
tlsOptions = make ( map [ string ] tls . Options )
2020-02-24 16:14:06 +00:00
var nsDefault [ ] string
2019-06-21 15:18:05 +00:00
for _ , tlsOption := range tlsOptionsCRD {
logger := log . FromContext ( log . With ( ctx , log . Str ( "tlsOption" , tlsOption . Name ) , log . Str ( "namespace" , tlsOption . Namespace ) ) )
var clientCAs [ ] tls . FileOrContent
2019-07-12 15:50:04 +00:00
for _ , secretName := range tlsOption . Spec . ClientAuth . SecretNames {
2019-06-21 15:18:05 +00:00
secret , exists , err := client . GetSecret ( tlsOption . Namespace , secretName )
if err != nil {
logger . Errorf ( "Failed to fetch secret %s/%s: %v" , tlsOption . Namespace , secretName , err )
continue
}
if ! exists {
logger . Warnf ( "Secret %s/%s does not exist" , tlsOption . Namespace , secretName )
continue
}
cert , err := getCABlocks ( secret , tlsOption . Namespace , secretName )
if err != nil {
logger . Errorf ( "Failed to extract CA from secret %s/%s: %v" , tlsOption . Namespace , secretName , err )
continue
}
clientCAs = append ( clientCAs , tls . FileOrContent ( cert ) )
}
2020-02-24 16:14:06 +00:00
id := makeID ( tlsOption . Namespace , tlsOption . Name )
// If the name is default, we override the default config.
2021-06-14 08:06:05 +00:00
if tlsOption . Name == tls . DefaultTLSConfigName {
2020-02-24 16:14:06 +00:00
id = tlsOption . Name
nsDefault = append ( nsDefault , tlsOption . Namespace )
}
2021-08-20 16:20:06 +00:00
alpnProtocols := tls . DefaultTLSOptions . ALPNProtocols
if len ( tlsOption . Spec . ALPNProtocols ) > 0 {
alpnProtocols = tlsOption . Spec . ALPNProtocols
}
2020-02-24 16:14:06 +00:00
tlsOptions [ id ] = tls . Options {
2019-11-03 14:54:04 +00:00
MinVersion : tlsOption . Spec . MinVersion ,
MaxVersion : tlsOption . Spec . MaxVersion ,
CipherSuites : tlsOption . Spec . CipherSuites ,
CurvePreferences : tlsOption . Spec . CurvePreferences ,
2019-07-12 15:50:04 +00:00
ClientAuth : tls . ClientAuth {
CAFiles : clientCAs ,
ClientAuthType : tlsOption . Spec . ClientAuth . ClientAuthType ,
2019-06-21 15:18:05 +00:00
} ,
2020-02-12 17:06:04 +00:00
SniStrict : tlsOption . Spec . SniStrict ,
PreferServerCipherSuites : tlsOption . Spec . PreferServerCipherSuites ,
2021-08-20 16:20:06 +00:00
ALPNProtocols : alpnProtocols ,
2019-06-21 15:18:05 +00:00
}
}
2020-02-24 16:14:06 +00:00
if len ( nsDefault ) > 1 {
2021-06-14 08:06:05 +00:00
delete ( tlsOptions , tls . DefaultTLSConfigName )
2020-02-24 16:14:06 +00:00
log . FromContext ( ctx ) . Errorf ( "Default TLS Options defined in multiple namespaces: %v" , nsDefault )
}
2019-06-21 15:18:05 +00:00
return tlsOptions
}
2020-02-24 16:14:06 +00:00
func buildTLSStores ( ctx context . Context , client Client ) map [ string ] tls . Store {
tlsStoreCRD := client . GetTLSStores ( )
var tlsStores map [ string ] tls . Store
if len ( tlsStoreCRD ) == 0 {
return tlsStores
}
tlsStores = make ( map [ string ] tls . Store )
var nsDefault [ ] string
for _ , tlsStore := range tlsStoreCRD {
namespace := tlsStore . Namespace
secretName := tlsStore . Spec . DefaultCertificate . SecretName
logger := log . FromContext ( log . With ( ctx , log . Str ( "tlsStore" , tlsStore . Name ) , log . Str ( "namespace" , namespace ) , log . Str ( "secretName" , secretName ) ) )
secret , exists , err := client . GetSecret ( namespace , secretName )
if err != nil {
logger . Errorf ( "Failed to fetch secret %s/%s: %v" , namespace , secretName , err )
continue
}
if ! exists {
logger . Errorf ( "Secret %s/%s does not exist" , namespace , secretName )
continue
}
cert , key , err := getCertificateBlocks ( secret , namespace , secretName )
if err != nil {
logger . Errorf ( "Could not get certificate blocks: %v" , err )
continue
}
id := makeID ( tlsStore . Namespace , tlsStore . Name )
// If the name is default, we override the default config.
2021-06-14 08:06:05 +00:00
if tlsStore . Name == tls . DefaultTLSStoreName {
2020-02-24 16:14:06 +00:00
id = tlsStore . Name
nsDefault = append ( nsDefault , tlsStore . Namespace )
}
tlsStores [ id ] = tls . Store {
DefaultCertificate : & tls . Certificate {
CertFile : tls . FileOrContent ( cert ) ,
KeyFile : tls . FileOrContent ( key ) ,
} ,
}
}
if len ( nsDefault ) > 1 {
2021-06-14 08:06:05 +00:00
delete ( tlsStores , tls . DefaultTLSStoreName )
2020-02-24 16:14:06 +00:00
log . FromContext ( ctx ) . Errorf ( "Default TLS Stores defined in multiple namespaces: %v" , nsDefault )
}
return tlsStores
}
2019-06-11 13:12:04 +00:00
func makeServiceKey ( rule , ingressName string ) ( string , error ) {
h := sha256 . New ( )
if _ , err := h . Write ( [ ] byte ( rule ) ) ; err != nil {
return "" , err
}
key := fmt . Sprintf ( "%s-%.10x" , ingressName , h . Sum ( nil ) )
return key , nil
}
2019-03-14 14:56:06 +00:00
func makeID ( namespace , name string ) string {
if namespace == "" {
return name
}
2019-06-11 13:12:04 +00:00
2019-09-13 18:44:04 +00:00
return namespace + "-" + name
2019-03-14 14:56:06 +00:00
}
2020-07-07 12:42:03 +00:00
func shouldProcessIngress ( ingressClass , ingressClassAnnotation string ) bool {
2019-02-21 22:08:05 +00:00
return ingressClass == ingressClassAnnotation ||
( len ( ingressClass ) == 0 && ingressClassAnnotation == traefikDefaultIngressClass )
}
2019-06-27 21:58:03 +00:00
func getTLS ( k8sClient Client , secretName , namespace string ) ( * tls . CertAndStores , error ) {
2019-06-11 13:12:04 +00:00
secret , exists , err := k8sClient . GetSecret ( namespace , secretName )
if err != nil {
2020-05-11 10:06:07 +00:00
return nil , fmt . Errorf ( "failed to fetch secret %s/%s: %w" , namespace , secretName , err )
2019-06-11 13:12:04 +00:00
}
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
}
2019-06-27 21:58:03 +00:00
return & tls . CertAndStores {
Certificate : tls . Certificate {
2019-06-11 13:12:04 +00:00
CertFile : tls . FileOrContent ( cert ) ,
KeyFile : tls . FileOrContent ( key ) ,
} ,
} , nil
}
2019-06-27 21:58:03 +00:00
func getTLSConfig ( tlsConfigs map [ string ] * tls . CertAndStores ) [ ] * tls . CertAndStores {
2019-02-21 22:08:05 +00:00
var secretNames [ ] string
for secretName := range tlsConfigs {
secretNames = append ( secretNames , secretName )
}
sort . Strings ( secretNames )
2019-06-27 21:58:03 +00:00
var configs [ ] * tls . CertAndStores
2019-02-21 22:08:05 +00:00
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
}
2019-06-21 15:18:05 +00:00
func getCABlocks ( secret * corev1 . Secret , namespace , secretName string ) ( string , error ) {
tlsCrtData , tlsCrtExists := secret . Data [ "tls.ca" ]
2021-06-14 16:06:10 +00:00
if tlsCrtExists {
return string ( tlsCrtData ) , nil
2019-06-21 15:18:05 +00:00
}
2021-06-14 16:06:10 +00:00
tlsCrtData , tlsCrtExists = secret . Data [ "ca.crt" ]
if tlsCrtExists {
return string ( tlsCrtData ) , nil
2019-06-21 15:18:05 +00:00
}
2021-06-14 16:06:10 +00:00
return "" , fmt . Errorf ( "secret %s/%s contains neither tls.ca nor ca.crt" , namespace , secretName )
2019-06-21 15:18:05 +00:00
}
2019-08-30 10:16:04 +00:00
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
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling)
eventsChanBuffered := make ( chan interface { } , 1 )
2019-11-14 18:28:04 +00:00
// 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 :
2019-11-14 18:28:04 +00:00
// 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
2019-08-30 10:16:04 +00:00
log . FromContext ( ctx ) . Debugf ( "Dropping event kind %T due to throttling" , nextEvent )
}
}
}
2020-02-03 16:56:04 +00:00
} )
2019-08-30 10:16:04 +00:00
return eventsChanBuffered
}
2020-12-10 13:58:04 +00:00
2021-07-13 08:48:05 +00:00
func isNamespaceAllowed ( allowCrossNamespace bool , parentNamespace , namespace string ) bool {
2020-12-10 13:58:04 +00:00
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
2021-07-13 08:48:05 +00:00
return allowCrossNamespace || parentNamespace == namespace
2020-12-10 13:58:04 +00:00
}