mirror of
https://github.com/traefik/traefik.git
synced 2026-06-18 19:38:23 +03:00
Merge branch v3.6 into v3.7
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRouteTCP
|
||||
metadata:
|
||||
name: test.route
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- foo
|
||||
|
||||
routes:
|
||||
- match: HostSNI(`foo.com`)
|
||||
services:
|
||||
- name: whoamitcp
|
||||
port: 8000
|
||||
|
||||
tls:
|
||||
options:
|
||||
name: foo@file
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: test.route
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
|
||||
routes:
|
||||
- match: Host(`foo.com`) && PathPrefix(`/bar`)
|
||||
kind: Rule
|
||||
priority: 12
|
||||
services:
|
||||
- name: whoami
|
||||
port: 80
|
||||
serversTransport: foo@file
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: TraefikService
|
||||
metadata:
|
||||
name: mirror-cp
|
||||
namespace: foo
|
||||
|
||||
spec:
|
||||
mirroring:
|
||||
name: external-main@file
|
||||
kind: TraefikService
|
||||
mirrors:
|
||||
- name: external-mirror@file
|
||||
kind: TraefikService
|
||||
percent: 50
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: TraefikService
|
||||
metadata:
|
||||
name: weighted-cp
|
||||
namespace: bar
|
||||
|
||||
spec:
|
||||
weighted:
|
||||
services:
|
||||
- name: external-a@file
|
||||
kind: TraefikService
|
||||
weight: 1
|
||||
- name: external-b@file
|
||||
kind: TraefikService
|
||||
weight: 1
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: ir-mirror
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
|
||||
routes:
|
||||
- match: Host(`mirror.example.com`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: mirror-cp
|
||||
namespace: foo
|
||||
kind: TraefikService
|
||||
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: ir-weighted
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
|
||||
routes:
|
||||
- match: Host(`weighted.example.com`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: weighted-cp
|
||||
namespace: bar
|
||||
kind: TraefikService
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: test.route
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
|
||||
routes:
|
||||
- match: Host(`foo.com`) && PathPrefix(`/bar`)
|
||||
kind: Rule
|
||||
priority: 12
|
||||
services:
|
||||
- name: whoami
|
||||
port: 80
|
||||
|
||||
tls:
|
||||
options:
|
||||
name: foo@file
|
||||
@@ -57,6 +57,7 @@ type Provider struct {
|
||||
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"`
|
||||
CrossProviderNamespaces []string `description:"List of namespaces from which IngressRoute, IngressRouteTCP, IngressRouteUDP, and TraefikService are allowed to declare cross-provider references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,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 ingressClassName field or 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"`
|
||||
@@ -93,6 +94,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
|
||||
}
|
||||
|
||||
if p.CrossProviderNamespaces != nil {
|
||||
logger.Warn().Msgf("Cross-provider references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
|
||||
}
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
operation := func() error {
|
||||
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
||||
@@ -307,7 +312,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
||||
continue
|
||||
}
|
||||
|
||||
chain, err := createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain, p.AllowCrossNamespace)
|
||||
chain, err := p.createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error while reading chain middleware")
|
||||
continue
|
||||
@@ -358,6 +363,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
||||
allowCrossNamespace: p.AllowCrossNamespace,
|
||||
allowExternalNameServices: p.AllowExternalNameServices,
|
||||
allowEmptyServices: p.AllowEmptyServices,
|
||||
crossProviderNamespaces: p.CrossProviderNamespaces,
|
||||
}
|
||||
|
||||
for _, service := range client.GetTraefikServices() {
|
||||
@@ -666,6 +672,7 @@ func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client,
|
||||
allowCrossNamespace: p.AllowCrossNamespace,
|
||||
allowExternalNameServices: p.AllowExternalNameServices,
|
||||
allowEmptyServices: p.AllowEmptyServices,
|
||||
crossProviderNamespaces: p.CrossProviderNamespaces,
|
||||
}
|
||||
|
||||
balancerName, balancerServerHTTP, err := cb.nameAndService(ctx, namespace, errorPage.Service.LoadBalancerSpec)
|
||||
@@ -680,6 +687,26 @@ func (p *Provider) createErrorPageMiddleware(ctx context.Context, client Client,
|
||||
}, balancerServerHTTP, nil
|
||||
}
|
||||
|
||||
func (p *Provider) createChainMiddleware(ctx context.Context, parentNamespace string, chain *traefikv1alpha1.Chain) (*dynamic.Chain, error) {
|
||||
if chain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var mds []string
|
||||
for _, mi := range chain.Middlewares {
|
||||
ctxMid := log.Ctx(ctx).With().Str("middlewareRef", mi.Namespace+"/"+mi.Name).Logger().WithContext(ctx)
|
||||
|
||||
middlewareRef, err := resolveReference(ctxMid, parentNamespace, mi.Namespace, mi.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
|
||||
}
|
||||
|
||||
mds = append(mds, middlewareRef)
|
||||
}
|
||||
|
||||
return &dynamic.Chain{Middlewares: mds}, nil
|
||||
}
|
||||
|
||||
// getServicePort always returns a valid port, an error otherwise.
|
||||
func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.ServicePort, error) {
|
||||
if svc == nil {
|
||||
@@ -1280,43 +1307,6 @@ func loadAuthCredentials(secret *corev1.Secret) ([]string, error) {
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func createChainMiddleware(ctx context.Context, parentNamespace string, chain *traefikv1alpha1.Chain, allowCrossNamespace bool) (*dynamic.Chain, error) {
|
||||
if chain == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var mds []string
|
||||
for _, mi := range chain.Middlewares {
|
||||
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross-namespace references.
|
||||
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
|
||||
}
|
||||
|
||||
if strings.Contains(mi.Name, providerNamespaceSeparator) {
|
||||
if len(mi.Namespace) > 0 {
|
||||
log.Ctx(ctx).Warn().Msgf("namespace %q is ignored in cross-provider context", mi.Namespace)
|
||||
}
|
||||
mds = append(mds, mi.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
ns := parentNamespace
|
||||
if len(mi.Namespace) > 0 {
|
||||
if !isNamespaceAllowed(allowCrossNamespace, parentNamespace, mi.Namespace) {
|
||||
return nil, fmt.Errorf("middleware %s/%s is not in the chain namespace %s", mi.Namespace, mi.Name, parentNamespace)
|
||||
}
|
||||
|
||||
ns = mi.Namespace
|
||||
}
|
||||
|
||||
mds = append(mds, makeID(ns, mi.Name))
|
||||
}
|
||||
|
||||
return &dynamic.Chain{Middlewares: mds}, nil
|
||||
}
|
||||
|
||||
func buildTLSOptions(ctx context.Context, client Client) map[string]tls.Options {
|
||||
tlsOptionsCRDs := client.GetTLSOptions()
|
||||
var tlsOptions map[string]tls.Options
|
||||
@@ -1659,3 +1649,39 @@ func isNamespaceAllowed(allowCrossNamespace bool, parentNamespace, namespace str
|
||||
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
|
||||
return allowCrossNamespace || parentNamespace == namespace
|
||||
}
|
||||
|
||||
// isCrossProviderNamespaceAllowed reports whether the given namespace is allowed to declare direct references to Traefik resources.
|
||||
// A nil allowList means references are unrestricted, and an empty allowList disables them entirely.
|
||||
func isCrossProviderNamespaceAllowed(allowList []string, namespace string) bool {
|
||||
if allowList == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return slices.Contains(allowList, namespace)
|
||||
}
|
||||
|
||||
func resolveReference(ctx context.Context, parentNs, ns, name string, crossProviderNamespaces []string, allowCrossNamespace bool) (string, error) {
|
||||
if strings.Contains(name, providerNamespaceSeparator) {
|
||||
if !allowCrossNamespace && strings.HasSuffix(name, providerNamespaceSeparator+ProviderName) {
|
||||
return "", errors.New("when allowCrossNamespace is disabled, @kubernetescrd references are disallowed")
|
||||
}
|
||||
|
||||
if !isCrossProviderNamespaceAllowed(crossProviderNamespaces, parentNs) {
|
||||
return "", fmt.Errorf("namespace %q is not in crossProviderNamespaces", parentNs)
|
||||
}
|
||||
|
||||
if ns != "" {
|
||||
log.Ctx(ctx).Warn().Msgf("Namespace %q is ignored in cross-provider context", ns)
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
ns = namespaceOrParentNamespace(ns, parentNs)
|
||||
|
||||
if !isNamespaceAllowed(allowCrossNamespace, parentNs, ns) {
|
||||
return "", errors.New("allowCrossNamespace is disabled, cross-namespace are disallowed")
|
||||
}
|
||||
|
||||
return provider.Normalize(ns + "-" + name), nil
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
|
||||
allowEmptyServices: p.AllowEmptyServices,
|
||||
nativeLBByDefault: p.NativeLBByDefault,
|
||||
disableClusterScopeResources: p.DisableClusterScopeResources,
|
||||
crossProviderNamespaces: p.CrossProviderNamespaces,
|
||||
}
|
||||
|
||||
parentRouterNames, err := resolveParentRouterNames(client, ingressRoute, p.AllowCrossNamespace)
|
||||
@@ -82,7 +83,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
|
||||
|
||||
serviceKey := makeServiceKey(route.Match, ingressName)
|
||||
|
||||
mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.AllowCrossNamespace)
|
||||
mds, err := makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares, p.CrossProviderNamespaces, p.AllowCrossNamespace)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to create middleware keys")
|
||||
continue
|
||||
@@ -147,27 +148,14 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
|
||||
}
|
||||
|
||||
if ingressRoute.Spec.TLS.Options != nil && len(ingressRoute.Spec.TLS.Options.Name) > 0 {
|
||||
tlsOptionsName := ingressRoute.Spec.TLS.Options.Name
|
||||
// Is a Kubernetes CRD reference, (i.e. not a cross-provider reference)
|
||||
ns := ingressRoute.Spec.TLS.Options.Namespace
|
||||
if !strings.Contains(tlsOptionsName, providerNamespaceSeparator) {
|
||||
if len(ns) == 0 {
|
||||
ns = ingressRoute.Namespace
|
||||
}
|
||||
tlsOptionsName = makeID(ns, tlsOptionsName)
|
||||
} else if len(ns) > 0 {
|
||||
logger.
|
||||
Warn().Str("TLSOption", ingressRoute.Spec.TLS.Options.Name).
|
||||
Msgf("Namespace %q is ignored in cross-provider context", ns)
|
||||
}
|
||||
tlsOptions := ingressRoute.Spec.TLS.Options
|
||||
ctxTLSOption := log.Ctx(ctx).With().Str("TLSOption", tlsOptions.Name).Logger().WithContext(ctx)
|
||||
|
||||
if !isNamespaceAllowed(p.AllowCrossNamespace, ingressRoute.Namespace, ns) {
|
||||
logger.Error().Msgf("TLSOption %s/%s is not in the IngressRoute namespace %s",
|
||||
ns, ingressRoute.Spec.TLS.Options.Name, ingressRoute.Namespace)
|
||||
r.TLS.Options, err = resolveReference(ctxTLSOption, ingressRoute.Namespace, tlsOptions.Namespace, tlsOptions.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Invalid reference to TLSOption %q", ingressRoute.Spec.TLS.Options.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
r.TLS.Options = tlsOptionsName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,40 +168,18 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
|
||||
return conf
|
||||
}
|
||||
|
||||
func makeMiddlewareKeys(ctx context.Context, namespace string, middlewares []traefikv1alpha1.MiddlewareRef, allowCrossNamespace bool) ([]string, error) {
|
||||
func makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []traefikv1alpha1.MiddlewareRef, crossProviderNamespaces []string, allowCrossNamespace bool) ([]string, error) {
|
||||
var mds []string
|
||||
|
||||
for _, mi := range middlewares {
|
||||
name := mi.Name
|
||||
ctxMid := log.Ctx(ctx).With().Str(logs.MiddlewareName, mi.Name).Logger().WithContext(ctx)
|
||||
|
||||
if !allowCrossNamespace && strings.HasSuffix(mi.Name, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross-namespace references.
|
||||
return nil, fmt.Errorf("invalid reference to middleware %s: when allowCrossNamespace is disabled @kubernetescrd provider references are disallowed", mi.Name)
|
||||
middlewareRef, err := resolveReference(ctxMid, ingRouteNamespace, mi.Namespace, mi.Name, crossProviderNamespaces, allowCrossNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
|
||||
}
|
||||
|
||||
if strings.Contains(name, providerNamespaceSeparator) {
|
||||
if len(mi.Namespace) > 0 {
|
||||
log.Ctx(ctx).
|
||||
Warn().Str(logs.MiddlewareName, mi.Name).
|
||||
Msgf("namespace %q is ignored in cross-provider context", mi.Namespace)
|
||||
}
|
||||
|
||||
mds = append(mds, name)
|
||||
continue
|
||||
}
|
||||
|
||||
ns := namespace
|
||||
if len(mi.Namespace) > 0 {
|
||||
if !isNamespaceAllowed(allowCrossNamespace, namespace, mi.Namespace) {
|
||||
return nil, fmt.Errorf("middleware %s/%s is not in the parent namespace %s", mi.Namespace, mi.Name, namespace)
|
||||
}
|
||||
|
||||
ns = mi.Namespace
|
||||
}
|
||||
|
||||
mds = append(mds, provider.Normalize(makeID(ns, name)))
|
||||
mds = append(mds, middlewareRef)
|
||||
}
|
||||
|
||||
return mds, nil
|
||||
@@ -270,6 +236,7 @@ type configBuilder struct {
|
||||
allowEmptyServices bool
|
||||
nativeLBByDefault bool
|
||||
disableClusterScopeResources bool
|
||||
crossProviderNamespaces []string
|
||||
}
|
||||
|
||||
// buildTraefikService creates the configuration for the traefik service defined in tService,
|
||||
@@ -514,7 +481,7 @@ func (c configBuilder) buildServersLB(ctx context.Context, svc traefikv1alpha1.L
|
||||
|
||||
service := &dynamic.Service{LoadBalancer: lb}
|
||||
if len(svc.Middlewares) > 0 {
|
||||
mds, err := makeMiddlewareKeys(ctx, svc.Namespace, svc.Middlewares, c.allowCrossNamespace)
|
||||
mds, err := makeMiddlewareKeys(ctx, svc.Namespace, svc.Middlewares, c.crossProviderNamespaces, c.allowCrossNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create middleware keys: %w", err)
|
||||
}
|
||||
@@ -529,14 +496,18 @@ func (c configBuilder) makeServersTransportKey(parentNamespace string, serversTr
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross namespace references.
|
||||
return "", fmt.Errorf("invalid reference to serversTransport %s: namespace-name@kubernetescrd format is not allowed when crossnamespace is disallowed", serversTransportName)
|
||||
}
|
||||
|
||||
if strings.Contains(serversTransportName, providerNamespaceSeparator) {
|
||||
if !c.allowCrossNamespace && strings.HasSuffix(serversTransportName, providerNamespaceSeparator+ProviderName) {
|
||||
// Since we are not able to know if another namespace is in the name (namespace-name@kubernetescrd),
|
||||
// if the provider namespace kubernetescrd is used,
|
||||
// we don't allow this format to avoid cross namespace references.
|
||||
return "", fmt.Errorf("invalid reference to serversTransport %s: namespace-name@kubernetescrd format is not allowed when crossnamespace is disallowed", serversTransportName)
|
||||
}
|
||||
|
||||
if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) {
|
||||
return "", fmt.Errorf("serversTransport %q reference is not allowed: namespace %q is not in crossProviderNamespaces", serversTransportName, parentNamespace)
|
||||
}
|
||||
|
||||
return serversTransportName, nil
|
||||
}
|
||||
|
||||
@@ -691,11 +662,17 @@ func (c configBuilder) loadServers(svc traefikv1alpha1.LoadBalancerSpec) ([]dyna
|
||||
func (c configBuilder) nameAndService(ctx context.Context, parentNamespace string, service traefikv1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
|
||||
svcCtx := log.Ctx(ctx).With().Str(logs.ServiceName, service.Name).Logger().WithContext(ctx)
|
||||
|
||||
service = *service.DeepCopy()
|
||||
service.Namespace = namespaceOrFallback(service, parentNamespace)
|
||||
if !strings.Contains(service.Name, providerNamespaceSeparator) {
|
||||
service = *service.DeepCopy()
|
||||
service.Namespace = namespaceOrParentNamespace(service.Namespace, parentNamespace)
|
||||
|
||||
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, service.Namespace) {
|
||||
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
|
||||
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, service.Namespace) {
|
||||
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
if !isCrossProviderNamespaceAllowed(c.crossProviderNamespaces, parentNamespace) && strings.Contains(service.Name, providerNamespaceSeparator) {
|
||||
return "", nil, fmt.Errorf("service %q reference is not allowed: namespace %q is not in crossProviderNamespaces", service.Name, parentNamespace)
|
||||
}
|
||||
|
||||
switch service.Kind {
|
||||
@@ -811,11 +788,12 @@ func fullServiceName(ctx context.Context, service traefikv1alpha1.LoadBalancerSp
|
||||
return provider.Normalize(name) + providerNamespaceSeparator + pName
|
||||
}
|
||||
|
||||
func namespaceOrFallback(lb traefikv1alpha1.LoadBalancerSpec, fallback string) string {
|
||||
if lb.Namespace != "" {
|
||||
return lb.Namespace
|
||||
func namespaceOrParentNamespace(namespace, parentNamespace string) string {
|
||||
if namespace != "" {
|
||||
return namespace
|
||||
}
|
||||
return fallback
|
||||
|
||||
return parentNamespace
|
||||
}
|
||||
|
||||
// getTLSHTTP mutates tlsConfigs.
|
||||
@@ -823,6 +801,7 @@ func getTLSHTTP(ctx context.Context, ingressRoute *traefikv1alpha1.IngressRoute,
|
||||
if ingressRoute.Spec.TLS == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ingressRoute.Spec.TLS.SecretName == "" {
|
||||
log.Ctx(ctx).Debug().Msg("No secret name provided")
|
||||
return nil
|
||||
|
||||
@@ -114,27 +114,14 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
|
||||
}
|
||||
|
||||
if ingressRouteTCP.Spec.TLS.Options != nil && len(ingressRouteTCP.Spec.TLS.Options.Name) > 0 {
|
||||
tlsOptionsName := ingressRouteTCP.Spec.TLS.Options.Name
|
||||
// Is a Kubernetes CRD reference (i.e. not a cross-provider reference)
|
||||
ns := ingressRouteTCP.Spec.TLS.Options.Namespace
|
||||
if !strings.Contains(tlsOptionsName, providerNamespaceSeparator) {
|
||||
if len(ns) == 0 {
|
||||
ns = ingressRouteTCP.Namespace
|
||||
}
|
||||
tlsOptionsName = makeID(ns, tlsOptionsName)
|
||||
} else if len(ns) > 0 {
|
||||
logger.Warn().
|
||||
Str("TLSOption", ingressRouteTCP.Spec.TLS.Options.Name).
|
||||
Msgf("Namespace %q is ignored in cross-provider context", ns)
|
||||
}
|
||||
tlsOptions := ingressRouteTCP.Spec.TLS.Options
|
||||
ctxTLSOption := log.Ctx(ctx).With().Str("TLSOption", tlsOptions.Name).Logger().WithContext(ctx)
|
||||
|
||||
if !isNamespaceAllowed(p.AllowCrossNamespace, ingressRouteTCP.Namespace, ns) {
|
||||
logger.Error().Msgf("TLSOption %s/%s is not in the IngressRouteTCP namespace %s",
|
||||
ns, ingressRouteTCP.Spec.TLS.Options.Name, ingressRouteTCP.Namespace)
|
||||
r.TLS.Options, err = resolveReference(ctxTLSOption, ingressRouteTCP.Namespace, tlsOptions.Namespace, tlsOptions.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Invalid reference to TLSOption %q", ingressRouteTCP.Spec.TLS.Options.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
r.TLS.Options = tlsOptionsName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,39 +136,24 @@ func (p *Provider) makeMiddlewareTCPKeys(ctx context.Context, ingRouteTCPNamespa
|
||||
var mds []string
|
||||
|
||||
for _, mi := range middlewares {
|
||||
if strings.Contains(mi.Name, providerNamespaceSeparator) {
|
||||
if len(mi.Namespace) > 0 {
|
||||
log.Ctx(ctx).Warn().
|
||||
Str(logs.MiddlewareName, mi.Name).
|
||||
Msgf("Namespace %q is ignored in cross-provider context", mi.Namespace)
|
||||
}
|
||||
mds = append(mds, mi.Name)
|
||||
continue
|
||||
ctxMid := log.Ctx(ctx).With().Str(logs.MiddlewareName, mi.Name).Logger().WithContext(ctx)
|
||||
|
||||
middlewareRef, err := resolveReference(ctxMid, ingRouteTCPNamespace, mi.Namespace, mi.Name, p.CrossProviderNamespaces, p.AllowCrossNamespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid reference to middleware %s: %w", mi.Name, err)
|
||||
}
|
||||
|
||||
ns := ingRouteTCPNamespace
|
||||
if len(mi.Namespace) > 0 {
|
||||
if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteTCPNamespace, mi.Namespace) {
|
||||
return nil, fmt.Errorf("middleware %s/%s is not in the IngressRouteTCP namespace %s", mi.Namespace, mi.Name, ingRouteTCPNamespace)
|
||||
}
|
||||
|
||||
ns = mi.Namespace
|
||||
}
|
||||
|
||||
mds = append(mds, provider.Normalize(makeID(ns, mi.Name)))
|
||||
mds = append(mds, middlewareRef)
|
||||
}
|
||||
|
||||
return mds, nil
|
||||
}
|
||||
|
||||
func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace string, service traefikv1alpha1.ServiceTCP) (*dynamic.TCPService, error) {
|
||||
ns := parentNamespace
|
||||
if len(service.Namespace) > 0 {
|
||||
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, service.Namespace) {
|
||||
return nil, fmt.Errorf("tcp service %s/%s is not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
|
||||
}
|
||||
ns := namespaceOrParentNamespace(service.Namespace, parentNamespace)
|
||||
|
||||
ns = service.Namespace
|
||||
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, ns) {
|
||||
return nil, fmt.Errorf("tcp service %s/%s is not in the parent resource namespace %s", ns, service.Name, parentNamespace)
|
||||
}
|
||||
|
||||
servers, err := p.loadTCPServers(client, ns, service)
|
||||
|
||||
@@ -259,7 +259,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "Simple Ingress Route, with foo entrypoint and crossprovider middleware",
|
||||
paths: []string{"tcp/services.yml", "tcp/with_middleware_crossprovider.yml"},
|
||||
paths: []string{"tcp/services.yml", "tcp/with_middleware_cross_provider.yml"},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
@@ -1813,12 +1813,13 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
|
||||
|
||||
func TestLoadIngressRoutes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
ingressClass string
|
||||
paths []string
|
||||
expected *dynamic.Configuration
|
||||
allowCrossNamespace bool
|
||||
allowEmptyServices bool
|
||||
desc string
|
||||
ingressClass string
|
||||
paths []string
|
||||
expected *dynamic.Configuration
|
||||
allowCrossNamespace bool
|
||||
allowEmptyServices bool
|
||||
crossProviderNamespaces []string
|
||||
}{
|
||||
{
|
||||
desc: "Empty",
|
||||
@@ -2104,9 +2105,10 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple Ingress Route with middleware crossprovider",
|
||||
allowCrossNamespace: true,
|
||||
paths: []string{"services.yml", "with_middleware_crossprovider.yml"},
|
||||
desc: "Simple Ingress Route with middleware crossprovider",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
allowCrossNamespace: true,
|
||||
paths: []string{"services.yml", "with_middleware_cross_provider.yml"},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
@@ -6389,6 +6391,7 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||
AllowCrossNamespace: test.allowCrossNamespace,
|
||||
AllowExternalNameServices: true,
|
||||
AllowEmptyServices: test.allowEmptyServices,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
@@ -8676,6 +8679,392 @@ func TestCrossNamespace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
allowList []string
|
||||
namespace string
|
||||
want bool
|
||||
}{
|
||||
{desc: "nil allowList allows any namespace", allowList: nil, namespace: "ns-a", want: true},
|
||||
{desc: "empty allowList denies every namespace", allowList: []string{}, namespace: "ns-a", want: false},
|
||||
{desc: "namespace in allowList is accepted", allowList: []string{"ns-a"}, namespace: "ns-a", want: true},
|
||||
{desc: "namespace not in allowList is rejected", allowList: []string{"ns-b"}, namespace: "ns-a", want: false},
|
||||
{desc: "namespace among multiple allowed entries is accepted", allowList: []string{"ns-a", "ns-b"}, namespace: "ns-b", want: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := isCrossProviderNamespaceAllowed(test.allowList, test.namespace)
|
||||
assert.Equal(t, test.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_HTTPMiddleware verifies that the
|
||||
// CrossProviderNamespaces option gates middleware references.
|
||||
// Plain in-namespace middleware references are not affected.
|
||||
func TestCrossProviderNamespaces_HTTPMiddleware(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantMiddlewares []string
|
||||
wantRouterDropped bool
|
||||
}{
|
||||
{
|
||||
desc: "nil: cross-provider middleware refs are accepted (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
wantMiddlewares: []string{"default-stripprefix", "foo-addprefix", "basicauth@file", "redirect@file"},
|
||||
},
|
||||
{
|
||||
desc: "empty list: cross-provider middleware refs are rejected, IngressRoute is dropped",
|
||||
crossProviderNamespaces: []string{},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
{
|
||||
desc: "namespace allowed: cross-provider middleware refs are accepted",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
wantMiddlewares: []string{"default-stripprefix", "foo-addprefix", "basicauth@file", "redirect@file"},
|
||||
},
|
||||
{
|
||||
desc: "namespace not allowed: cross-provider middleware refs are rejected, IngressRoute is dropped",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_middleware_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
crdClient := traefikcrdfake.NewClientset(crdObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, crdClient)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
eventCh, err := client.WatchAll(nil, stopCh)
|
||||
require.NoError(t, err)
|
||||
|
||||
if k8sObjects != nil || crdObjects != nil {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
AllowCrossNamespace: true,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
|
||||
router, ok := conf.HTTP.Routers["default-test2-route-23c7f4c450289ee29016"]
|
||||
if test.wantRouterDropped {
|
||||
assert.False(t, ok)
|
||||
return
|
||||
}
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, test.wantMiddlewares, router.Middlewares)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_HTTPServiceTransitivity verifies that the option for a TraefikService chain
|
||||
// (here: IngressRoute -> Mirror / Weighted TraefikService -> @file service).
|
||||
func TestCrossProviderNamespaces_HTTPServiceTransitivity(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantMirrorService bool
|
||||
wantWeightedService bool
|
||||
}{
|
||||
{
|
||||
desc: "nil: cross-provider service refs accepted (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
wantMirrorService: true,
|
||||
wantWeightedService: true,
|
||||
},
|
||||
{
|
||||
desc: "empty list: both Mirror and Weighted TraefikServices are rejected",
|
||||
crossProviderNamespaces: []string{},
|
||||
wantMirrorService: false,
|
||||
wantWeightedService: false,
|
||||
},
|
||||
{
|
||||
desc: "only the Mirror's namespace is allowed: Weighted is still rejected",
|
||||
crossProviderNamespaces: []string{"foo"},
|
||||
wantMirrorService: true,
|
||||
wantWeightedService: false,
|
||||
},
|
||||
{
|
||||
desc: "only the Weighted's namespace is allowed: Mirror is still rejected",
|
||||
crossProviderNamespaces: []string{"bar"},
|
||||
wantMirrorService: false,
|
||||
wantWeightedService: true,
|
||||
},
|
||||
{
|
||||
desc: "both namespaces allowed: both TraefikServices are accepted",
|
||||
crossProviderNamespaces: []string{"foo", "bar"},
|
||||
wantMirrorService: true,
|
||||
wantWeightedService: true,
|
||||
},
|
||||
{
|
||||
desc: "originating IngressRoute namespace alone is not enough: TraefikService namespace must also be allowed",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
wantMirrorService: false,
|
||||
wantWeightedService: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_service_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
crdClient := traefikcrdfake.NewClientset(crdObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, crdClient)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
eventCh, err := client.WatchAll(nil, stopCh)
|
||||
require.NoError(t, err)
|
||||
|
||||
if k8sObjects != nil || crdObjects != nil {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
AllowCrossNamespace: true,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
|
||||
_, mirrorOK := conf.HTTP.Services["foo-mirror-cp"]
|
||||
_, weightedOK := conf.HTTP.Services["bar-weighted-cp"]
|
||||
|
||||
assert.Equal(t, test.wantMirrorService, mirrorOK)
|
||||
assert.Equal(t, test.wantWeightedService, weightedOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_HTTPTLSOption verifies that the
|
||||
// CrossProviderNamespaces option gates @file references in IngressRoute tls.options.
|
||||
func TestCrossProviderNamespaces_HTTPTLSOption(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantRouterDropped bool
|
||||
}{
|
||||
{
|
||||
desc: "nil: cross-provider TLSOption ref is accepted (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty list: cross-provider TLSOption ref is rejected, IngressRoute is dropped",
|
||||
crossProviderNamespaces: []string{},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
{
|
||||
desc: "namespace allowed: cross-provider TLSOption ref is accepted",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
},
|
||||
{
|
||||
desc: "namespace not allowed: cross-provider TLSOption ref is rejected, IngressRoute is dropped",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_tls_option_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
crdClient := traefikcrdfake.NewClientset(crdObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, crdClient)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
eventCh, err := client.WatchAll(nil, stopCh)
|
||||
require.NoError(t, err)
|
||||
|
||||
if k8sObjects != nil || crdObjects != nil {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
AllowCrossNamespace: true,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
|
||||
router, ok := conf.HTTP.Routers["default-test-route-6b204d94623b3df4370c"]
|
||||
if test.wantRouterDropped {
|
||||
assert.False(t, ok)
|
||||
return
|
||||
}
|
||||
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, router.TLS)
|
||||
assert.Equal(t, "foo@file", router.TLS.Options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_TCPTLSOption verifies that the
|
||||
// CrossProviderNamespaces option gates @file references in IngressRouteTCP tls.options.
|
||||
func TestCrossProviderNamespaces_TCPTLSOption(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantRouterDropped bool
|
||||
}{
|
||||
{
|
||||
desc: "nil: cross-provider TLSOption ref is accepted (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty list: cross-provider TLSOption ref is rejected, IngressRouteTCP is dropped",
|
||||
crossProviderNamespaces: []string{},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
{
|
||||
desc: "namespace allowed: cross-provider TLSOption ref is accepted",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
},
|
||||
{
|
||||
desc: "namespace not allowed: cross-provider TLSOption ref is rejected, IngressRouteTCP is dropped",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
wantRouterDropped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, crdObjects := readResources(t, []string{"tcp/services.yml", "tcp/with_tls_options_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
crdClient := traefikcrdfake.NewClientset(crdObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, crdClient)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
eventCh, err := client.WatchAll(nil, stopCh)
|
||||
require.NoError(t, err)
|
||||
|
||||
if k8sObjects != nil || crdObjects != nil {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
AllowCrossNamespace: true,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
|
||||
router, ok := conf.TCP.Routers["default-test.route-fdd3e9338e47a45efefc"]
|
||||
if test.wantRouterDropped {
|
||||
assert.False(t, ok)
|
||||
return
|
||||
}
|
||||
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, router.TLS)
|
||||
assert.Equal(t, "foo@file", router.TLS.Options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_HTTPServersTransport verifies that the
|
||||
// CrossProviderNamespaces option gates @file references in service.serversTransport.
|
||||
func TestCrossProviderNamespaces_HTTPServersTransport(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantServiceDropped bool
|
||||
}{
|
||||
{
|
||||
desc: "nil: cross-provider ServersTransport ref is accepted (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty list: cross-provider ServersTransport ref is rejected, service is dropped",
|
||||
crossProviderNamespaces: []string{},
|
||||
wantServiceDropped: true,
|
||||
},
|
||||
{
|
||||
desc: "namespace allowed: cross-provider ServersTransport ref is accepted",
|
||||
crossProviderNamespaces: []string{"default"},
|
||||
},
|
||||
{
|
||||
desc: "namespace not allowed: cross-provider ServersTransport ref is rejected, service is dropped",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
wantServiceDropped: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, crdObjects := readResources(t, []string{"services.yml", "with_servers_transport_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
crdClient := traefikcrdfake.NewClientset(crdObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, crdClient)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
|
||||
eventCh, err := client.WatchAll(nil, stopCh)
|
||||
require.NoError(t, err)
|
||||
|
||||
if k8sObjects != nil || crdObjects != nil {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
AllowCrossNamespace: true,
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromCRD(t.Context(), client)
|
||||
|
||||
service, ok := conf.HTTP.Services["default-test-route-6b204d94623b3df4370c"]
|
||||
if test.wantServiceDropped {
|
||||
assert.False(t, ok)
|
||||
return
|
||||
}
|
||||
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, service.LoadBalancer)
|
||||
assert.Equal(t, "foo@file", service.LoadBalancer.ServersTransport)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalNameService(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
||||
@@ -36,16 +36,16 @@ spec:
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TCPRoute
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TCPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tcp-app-1
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
|
||||
@@ -161,7 +161,18 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
|
||||
router.Service = errWrrName
|
||||
|
||||
case len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0].BackendRef):
|
||||
router.Service = string(routeRule.BackendRefs[0].Name)
|
||||
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
|
||||
condition = metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load HTTPRoute BackendRef %s: internal service reference is not allowed: HTTPRoute namespace %q is not in crossProviderNamespaces", routeRule.BackendRefs[0].Name, route.Namespace),
|
||||
}
|
||||
} else {
|
||||
router.Service = string(routeRule.BackendRefs[0].Name)
|
||||
}
|
||||
|
||||
default:
|
||||
var serviceCondition *metav1.Condition
|
||||
@@ -312,6 +323,10 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
|
||||
// Support for cross-provider references (e.g: api@internal).
|
||||
// This provides the same behavior as for IngressRoutes.
|
||||
if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") {
|
||||
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, namespace) {
|
||||
return "", nil, fmt.Errorf("TraefikService %q reference is not allowed: namespace %q is not in crossProviderNamespaces", string(backendRef.Name), namespace)
|
||||
}
|
||||
|
||||
return string(backendRef.Name), nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -63,17 +63,17 @@ const (
|
||||
|
||||
// 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 types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
|
||||
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
||||
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
|
||||
LabelSelector string `description:"Kubernetes 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"`
|
||||
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
|
||||
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
|
||||
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
|
||||
|
||||
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
Token types.FileOrContent `description:"Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty" loggable:"false"`
|
||||
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
|
||||
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
|
||||
LabelSelector string `description:"Kubernetes 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"`
|
||||
ExperimentalChannel bool `description:"Toggles Experimental Channel resources support (TCPRoute, TLSRoute...)." json:"experimentalChannel,omitempty" toml:"experimentalChannel,omitempty" yaml:"experimentalChannel,omitempty" export:"true"`
|
||||
StatusAddress *StatusAddress `description:"Defines the Kubernetes Gateway status address." json:"statusAddress,omitempty" toml:"statusAddress,omitempty" yaml:"statusAddress,omitempty" export:"true"`
|
||||
NativeLBByDefault bool `description:"Defines whether to use Native Kubernetes load-balancing by default." json:"nativeLBByDefault,omitempty" toml:"nativeLBByDefault,omitempty" yaml:"nativeLBByDefault,omitempty" export:"true"`
|
||||
CrossProviderNamespaces []string `description:"List of namespaces from which Gateway API routes are allowed to declare TraefikService backendRef references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
|
||||
EntryPoints map[string]Entrypoint `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
|
||||
// groupKindFilterFuncs is the list of allowed Group and Kinds for the Filter ExtensionRef objects.
|
||||
groupKindFilterFuncs map[string]map[string]BuildFilterFunc
|
||||
@@ -183,6 +183,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
logger := log.With().Str(logs.ProviderName, ProviderName).Logger()
|
||||
ctxLog := logger.WithContext(context.Background())
|
||||
|
||||
if p.CrossProviderNamespaces != nil {
|
||||
logger.Warn().Msgf("Cross-provider references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
|
||||
}
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
operation := func() error {
|
||||
eventsChan, err := p.client.WatchAll(p.Namespaces, ctxPool.Done())
|
||||
@@ -1238,6 +1242,15 @@ func isInternalService(ref gatev1.BackendRef) bool {
|
||||
return isTraefikService(ref) && strings.HasSuffix(string(ref.Name), "@internal")
|
||||
}
|
||||
|
||||
// isCrossProviderNamespaceAllowed reports whether the given namespace is allowed to use cross-provider references.
|
||||
func isCrossProviderNamespaceAllowed(allowList []string, namespace string) bool {
|
||||
if allowList == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return slices.Contains(allowList, namespace)
|
||||
}
|
||||
|
||||
// makeListenerKey joins protocol, hostname, and port of a listener into a string key.
|
||||
func makeListenerKey(l gatev1.Listener) string {
|
||||
var hostname gatev1.Hostname
|
||||
|
||||
@@ -2,9 +2,11 @@ package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -5158,9 +5160,53 @@ func TestLoadTLSRoutes(t *testing.T) {
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"deny-unknown-host": {
|
||||
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
|
||||
Priority: 1,
|
||||
Service: "deny-unknown-host",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
|
||||
EntryPoints: []string{"tls"},
|
||||
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
|
||||
Rule: `HostSNI("*")`,
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"deny-unknown-host": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
|
||||
Weighted: &dynamic.TCPWeightedRoundRobin{
|
||||
Services: []dynamic.TCPWRRService{
|
||||
{
|
||||
Name: "service@file",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
{
|
||||
Name: "default-whoamitcp-9000",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoamitcp-9000": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: "10.10.0.9:9000",
|
||||
},
|
||||
{
|
||||
Address: "10.10.0.10:9000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
@@ -5169,7 +5215,16 @@ func TestLoadTLSRoutes(t *testing.T) {
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
TLS: &dynamic.TLSConfiguration{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: types.FileOrContent(listenerCert),
|
||||
KeyFile: types.FileOrContent(listenerKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -8339,3 +8394,236 @@ func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Ob
|
||||
|
||||
return k8sObjects, gwObjects
|
||||
}
|
||||
|
||||
func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
allowList []string
|
||||
namespace string
|
||||
want bool
|
||||
}{
|
||||
{desc: "nil allowList allows any namespace", allowList: nil, namespace: "ns-a", want: true},
|
||||
{desc: "empty allowList denies every namespace", allowList: []string{}, namespace: "ns-a", want: false},
|
||||
{desc: "namespace in allowList is accepted", allowList: []string{"ns-a"}, namespace: "ns-a", want: true},
|
||||
{desc: "namespace not in allowList is rejected", allowList: []string{"ns-b"}, namespace: "ns-a", want: false},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := isCrossProviderNamespaceAllowed(test.allowList, test.namespace)
|
||||
assert.Equal(t, test.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_HTTPRoute verifies that the
|
||||
// CrossProviderNamespaces option gates `@otherProvider` TraefikService
|
||||
// backendRefs declared on a Gateway HTTPRoute. The check is anchored on the
|
||||
// HTTPRoute's namespace; when the route is rejected, the whole router is
|
||||
// dropped from the dynamic configuration.
|
||||
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, gwClient)
|
||||
|
||||
eventCh, err := client.WatchAll(nil, make(chan struct{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
EntryPoints: map[string]Entrypoint{"web": {Address: ":80"}},
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
|
||||
router, ok := conf.HTTP.Routers["httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3"]
|
||||
require.True(t, ok)
|
||||
|
||||
service, ok := conf.HTTP.Services[router.Service]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, service.Weighted)
|
||||
require.Len(t, service.Weighted.Services, 2)
|
||||
|
||||
var hasError bool
|
||||
for _, wrrService := range service.Weighted.Services {
|
||||
// Whenever a service fails to be loaded, a placeholder service is added to the WRR to server a 500 status code.
|
||||
if wrrService.Status != nil && *wrrService.Status == http.StatusInternalServerError {
|
||||
hasError = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, test.wantError, hasError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_TCPRoute verifies that the option also gates
|
||||
// cross-provider TraefikService backendRefs declared on a Gateway TCPRoute.
|
||||
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, gwClient)
|
||||
client.experimentalChannel = true
|
||||
|
||||
eventCh, err := client.WatchAll(nil, make(chan struct{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
EntryPoints: map[string]Entrypoint{"tcp": {Address: ":9000"}},
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
client: client,
|
||||
ExperimentalChannel: true,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
|
||||
router, ok := conf.TCP.Routers["tcproute-default-tcp-app-1-gw-default-my-gateway-ep-tcp-0-e3b0c44298fc1c149afb"]
|
||||
require.True(t, ok)
|
||||
|
||||
service, ok := conf.TCP.Services[router.Service]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, service.Weighted)
|
||||
require.Len(t, service.Weighted.Services, 2)
|
||||
|
||||
var hasError bool
|
||||
for _, wrrService := range service.Weighted.Services {
|
||||
if strings.Contains(wrrService.Name, "@") {
|
||||
continue
|
||||
}
|
||||
|
||||
lbService, ok := conf.TCP.Services[wrrService.Name]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, lbService)
|
||||
require.NotNil(t, lbService.LoadBalancer)
|
||||
|
||||
if len(lbService.LoadBalancer.Servers) == 0 {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, test.wantError, hasError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCrossProviderNamespaces_TLSRoute verifies that the option also gates
|
||||
// cross-provider TraefikService backendRefs declared on a Gateway TLSRoute.
|
||||
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, gwClient)
|
||||
client.experimentalChannel = true
|
||||
|
||||
eventCh, err := client.WatchAll(nil, make(chan struct{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
|
||||
// just wait for the first event
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
EntryPoints: map[string]Entrypoint{"tls": {Address: ":9000"}},
|
||||
CrossProviderNamespaces: test.crossProviderNamespaces,
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
|
||||
fmt.Println(conf.TCP.Routers)
|
||||
|
||||
router, ok := conf.TCP.Routers["tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb"]
|
||||
require.True(t, ok)
|
||||
|
||||
service, ok := conf.TCP.Services[router.Service]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, service.Weighted)
|
||||
require.Len(t, service.Weighted.Services, 2)
|
||||
|
||||
var hasError bool
|
||||
for _, wrrService := range service.Weighted.Services {
|
||||
if strings.Contains(wrrService.Name, "@") {
|
||||
continue
|
||||
}
|
||||
|
||||
lbService, ok := conf.TCP.Services[wrrService.Name]
|
||||
require.True(t, ok)
|
||||
require.NotNil(t, lbService)
|
||||
require.NotNil(t, lbService.LoadBalancer)
|
||||
|
||||
if len(lbService.LoadBalancer.Servers) == 0 {
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, test.wantError, hasError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@ func (p *Provider) loadTCPRoute(listener gatewayListener, route *gatev1alpha2.TC
|
||||
routerName := makeRouterName("", routeKey)
|
||||
|
||||
if len(rule.BackendRefs) == 1 && isInternalService(rule.BackendRefs[0]) {
|
||||
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
|
||||
condition = metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s: internal service reference is not allowed: TCPRoute namespace %q is not in crossProviderNamespaces", rule.BackendRefs[0].Name, route.Namespace),
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
router.Service = string(rule.BackendRefs[0].Name)
|
||||
conf.TCP.Routers[routerName] = &router
|
||||
continue
|
||||
@@ -224,7 +237,7 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
|
||||
}
|
||||
|
||||
if group != groupCore || kind != kindService {
|
||||
name, err := p.loadTCPBackendRef(backendRef)
|
||||
name, err := p.loadTCPBackendRef(route.Namespace, backendRef)
|
||||
if err != nil {
|
||||
return serviceName, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
@@ -296,10 +309,14 @@ func (p *Provider) loadTCPServers(namespace string, route *gatev1alpha2.TCPRoute
|
||||
return lb, nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadTCPBackendRef(backendRef gatev1.BackendRef) (string, error) {
|
||||
func (p *Provider) loadTCPBackendRef(routeNamespace string, backendRef gatev1.BackendRef) (string, error) {
|
||||
// Support for cross-provider references (e.g: api@internal).
|
||||
// This provides the same behavior as for IngressRoutes.
|
||||
if *backendRef.Kind == "TraefikService" && strings.Contains(string(backendRef.Name), "@") {
|
||||
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, routeNamespace) {
|
||||
return "", fmt.Errorf("TraefikService %q reference is not allowed: route namespace %q is not in crossProviderNamespaces", string(backendRef.Name), routeNamespace)
|
||||
}
|
||||
|
||||
return string(backendRef.Name), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,19 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
|
||||
routerName := makeRouterName("", routeKey)
|
||||
|
||||
if len(routeRule.BackendRefs) == 1 && isInternalService(routeRule.BackendRefs[0]) {
|
||||
if !isCrossProviderNamespaceAllowed(p.CrossProviderNamespaces, route.Namespace) {
|
||||
condition = metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s: internal service reference is not allowed: TLSRoute namespace %q is not in crossProviderNamespaces", routeRule.BackendRefs[0].Name, route.Namespace),
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
router.Service = string(routeRule.BackendRefs[0].Name)
|
||||
conf.TCP.Routers[routerName] = &router
|
||||
continue
|
||||
@@ -240,7 +253,7 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
|
||||
}
|
||||
|
||||
if group != groupCore || kind != kindService {
|
||||
name, err := p.loadTCPBackendRef(backendRef)
|
||||
name, err := p.loadTCPBackendRef(route.Namespace, backendRef)
|
||||
if err != nil {
|
||||
return serviceName, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ""
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /bar
|
||||
backend:
|
||||
service:
|
||||
name: service1
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: service1
|
||||
namespace: testing
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/service.serverstransport: foobar@file
|
||||
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
clusterIP: 10.0.0.1
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: service1-abc
|
||||
namespace: testing
|
||||
labels:
|
||||
kubernetes.io/service-name: service1
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- port: 8080
|
||||
name: ""
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
- 10.21.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ""
|
||||
namespace: testing
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
traefik.ingress.kubernetes.io/router.tls.options: foobar@file
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /bar
|
||||
backend:
|
||||
service:
|
||||
name: service1
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: service1
|
||||
namespace: testing
|
||||
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
clusterIP: 10.0.0.1
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: service1-abc
|
||||
namespace: testing
|
||||
labels:
|
||||
kubernetes.io/service-name: service1
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- port: 8080
|
||||
name: ""
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
- 10.21.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
|
||||
@@ -51,6 +51,7 @@ type Provider struct {
|
||||
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
||||
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
||||
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
|
||||
CrossProviderNamespaces []string `description:"List of namespaces from which Ingresses or Services are allowed to declare Middlewares, TLSOptions, or ServersTransport references." json:"crossProviderNamespaces,omitempty" toml:"crossProviderNamespaces,omitempty" yaml:"crossProviderNamespaces,omitempty" export:"true"`
|
||||
// Deprecated: please use DisableClusterScopeResources.
|
||||
DisableIngressClassLookup bool `description:"Disables the lookup of IngressClasses (Deprecated, please use DisableClusterScopeResources)." json:"disableIngressClassLookup,omitempty" toml:"disableIngressClassLookup,omitempty" yaml:"disableIngressClassLookup,omitempty" export:"true"`
|
||||
DisableClusterScopeResources bool `description:"Disables the lookup of cluster scope resources (incompatible with IngressClasses and NodePortLB enabled services)." json:"disableClusterScopeResources,omitempty" toml:"disableClusterScopeResources,omitempty" yaml:"disableClusterScopeResources,omitempty" export:"true"`
|
||||
@@ -92,6 +93,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
logger.Info().Msg("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
|
||||
}
|
||||
|
||||
if p.CrossProviderNamespaces != nil {
|
||||
logger.Warn().Msgf("Cross-provider Middleware, TLSOption and ServersTransport references are restricted to namespaces %v (see CrossProviderNamespaces option)", p.CrossProviderNamespaces)
|
||||
}
|
||||
|
||||
pool.GoCtx(func(ctxPool context.Context) {
|
||||
operation := func() error {
|
||||
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
|
||||
@@ -260,6 +265,19 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||
continue
|
||||
}
|
||||
|
||||
// Middlewares and TLS options always contain cross-provider references.
|
||||
if rtConfig != nil && rtConfig.Router != nil && p.CrossProviderNamespaces != nil && !slices.Contains(p.CrossProviderNamespaces, ingress.Namespace) {
|
||||
if len(rtConfig.Router.Middlewares) > 0 {
|
||||
logger.Error().Msgf("Skipping Ingress: cross-provider middleware reference is not allowed from namespace %q", ingress.Namespace)
|
||||
continue
|
||||
}
|
||||
|
||||
if rtConfig.Router.TLS != nil && rtConfig.Router.TLS.Options != "" {
|
||||
logger.Error().Msgf("Skipping Ingress: cross-provider TLS option reference is not allowed from namespace %q", ingress.Namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
err = getCertificates(ctxIngress, ingress, client, certConfigs)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Error configuring TLS")
|
||||
@@ -582,6 +600,10 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
|
||||
}
|
||||
|
||||
if svcConfig.Service.ServersTransport != "" {
|
||||
if p.CrossProviderNamespaces != nil && !slices.Contains(p.CrossProviderNamespaces, namespace) {
|
||||
return nil, fmt.Errorf("cross-provider serversTransport reference is not allowed from namespace %q", namespace)
|
||||
}
|
||||
|
||||
svc.LoadBalancer.ServersTransport = svcConfig.Service.ServersTransport
|
||||
}
|
||||
|
||||
|
||||
@@ -2480,6 +2480,153 @@ func generateTestFilename(desc string) string {
|
||||
return filepath.Join("fixtures", strings.ReplaceAll(desc, " ", "-")+".yml")
|
||||
}
|
||||
|
||||
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces verifies that an Ingress,
|
||||
// declaring a `traefik.ingress.kubernetes.io/router.middlewares` annotation,
|
||||
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
|
||||
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
path string
|
||||
wantRouter string
|
||||
}{
|
||||
{
|
||||
desc: "Ingress with middleware annotation is kept when option is unset (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
path: "fixtures/Ingress-with-annotations.yml",
|
||||
wantRouter: "testing-bar",
|
||||
},
|
||||
{
|
||||
desc: "Ingress with middleware annotation is dropped when option is empty",
|
||||
crossProviderNamespaces: []string{},
|
||||
path: "fixtures/Ingress-with-annotations.yml",
|
||||
},
|
||||
{
|
||||
desc: "Ingress with middleware annotation is kept when its namespace is allow-listed",
|
||||
crossProviderNamespaces: []string{"testing"},
|
||||
path: "fixtures/Ingress-with-annotations.yml",
|
||||
wantRouter: "testing-bar",
|
||||
},
|
||||
{
|
||||
desc: "Ingress with middleware annotation is dropped when its namespace is not allow-listed",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
path: "fixtures/Ingress-with-annotations.yml",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
|
||||
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock(test.path))
|
||||
|
||||
if test.wantRouter == "" {
|
||||
assert.Empty(t, conf.HTTP.Routers)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Contains(t, conf.HTTP.Routers, test.wantRouter)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_TLSOptions verifies that an Ingress,
|
||||
// declaring a `traefik.ingress.kubernetes.io/router.tls.options` annotation,
|
||||
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
|
||||
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_TLSOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantRouter string
|
||||
}{
|
||||
{
|
||||
desc: "Ingress with TLS options annotation is kept when option is unset (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
wantRouter: "testing-bar",
|
||||
},
|
||||
{
|
||||
desc: "Ingress with TLS options annotation is dropped when option is empty",
|
||||
crossProviderNamespaces: []string{},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with TLS options annotation is kept when its namespace is allow-listed",
|
||||
crossProviderNamespaces: []string{"testing"},
|
||||
wantRouter: "testing-bar",
|
||||
},
|
||||
{
|
||||
desc: "Ingress with TLS options annotation is dropped when its namespace is not allow-listed",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
|
||||
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock("fixtures/Ingress-with-tls-options-annotation.yml"))
|
||||
|
||||
if test.wantRouter == "" {
|
||||
assert.Empty(t, conf.HTTP.Routers)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Contains(t, conf.HTTP.Routers, test.wantRouter)
|
||||
assert.NotNil(t, conf.HTTP.Routers[test.wantRouter].TLS)
|
||||
assert.Equal(t, "foobar@file", conf.HTTP.Routers[test.wantRouter].TLS.Options)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_ServersTransport verifies that a Service referencing a cross-provider ServersTransport,
|
||||
// via the `traefik.ingress.kubernetes.io/service.serverstransport` annotation,
|
||||
// is dropped from the dynamic configuration when its namespace is not in `crossProviderNamespaces`.
|
||||
func TestLoadConfigurationFromIngressesWithCrossProviderNamespaces_ServersTransport(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
crossProviderNamespaces []string
|
||||
wantService string
|
||||
}{
|
||||
{
|
||||
desc: "Service with serversTransport annotation is kept when option is unset (backward compatible)",
|
||||
crossProviderNamespaces: nil,
|
||||
wantService: "testing-service1-80",
|
||||
},
|
||||
{
|
||||
desc: "Service with serversTransport annotation is dropped when option is empty",
|
||||
crossProviderNamespaces: []string{},
|
||||
},
|
||||
{
|
||||
desc: "Service with serversTransport annotation is kept when its namespace is allow-listed",
|
||||
crossProviderNamespaces: []string{"testing"},
|
||||
wantService: "testing-service1-80",
|
||||
},
|
||||
{
|
||||
desc: "Service with serversTransport annotation is dropped when its namespace is not allow-listed",
|
||||
crossProviderNamespaces: []string{"other"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{CrossProviderNamespaces: test.crossProviderNamespaces}
|
||||
conf := p.loadConfigurationFromIngresses(t.Context(), newClientMock("fixtures/Ingress-with-servers-transport-annotation.yml"))
|
||||
|
||||
if test.wantService == "" {
|
||||
assert.Empty(t, conf.HTTP.Services)
|
||||
assert.Empty(t, conf.HTTP.Routers)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Contains(t, conf.HTTP.Services, test.wantService)
|
||||
assert.Equal(t, "foobar@file", conf.HTTP.Services[test.wantService].LoadBalancer.ServersTransport)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCertificates(t *testing.T) {
|
||||
testIngressWithoutHostname := buildIngress(
|
||||
iNamespace("testing"),
|
||||
|
||||
Reference in New Issue
Block a user