IngressRoute: add an option to disable cross-namespace routing

Co-authored-by: Jean-Baptiste Doumenjou <925513+jbdoumenjou@users.noreply.github.com>
This commit is contained in:
Romain 2020-12-10 14:58:04 +01:00 committed by GitHub
parent c72769e2ea
commit 7ba907f261
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1123 additions and 36 deletions

View file

@ -199,3 +199,33 @@ spec:
- name: http
protocol: TCP
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: whoami-svc
namespace: cross-ns
spec:
ports:
- name: web
port: 80
selector:
app: traefiklabs
task: whoami
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoami-svc
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: web
port: 80

View file

@ -208,3 +208,33 @@ metadata:
spec:
externalName: "fe80::200:5aee:feaa:20a2"
type: ExternalName
---
apiVersion: v1
kind: Service
metadata:
name: whoamitcp-cross-ns
namespace: cross-ns
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamitcp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamitcp-cross-ns
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: myapp
port: 8000

View file

@ -0,0 +1,16 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: HostSNI(`foo.com`)
services:
- name: whoamitcp-cross-ns
namespace: cross-ns
port: 8000

View file

@ -130,3 +130,33 @@ subsets:
ports:
- name: myapp-ipv6
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: whoamiudp-cross-ns
namespace: cross-ns
spec:
ports:
- name: myapp
port: 8000
selector:
app: traefiklabs
task: whoamiudp
---
kind: Endpoints
apiVersion: v1
metadata:
name: whoamiudp-cross-ns
namespace: cross-ns
subsets:
- addresses:
- ip: 10.10.0.1
- ip: 10.10.0.2
ports:
- name: myapp
port: 8000

View file

@ -0,0 +1,15 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: whoamiudp-cross-ns
namespace: cross-ns
port: 8000

View file

@ -0,0 +1,91 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: cross-ns-route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami-svc
namespace: cross-ns
port: 80
- name: tr-svc-wrr1
kind: TraefikService
- name: tr-svc-wrr2
namespace: cross-ns
kind: TraefikService
- name: tr-svc-mirror1
kind: TraefikService
- name: tr-svc-mirror2
namespace: cross-ns
kind: TraefikService
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-wrr1
namespace: default
spec:
weighted:
services:
- name: whoami-svc
namespace: cross-ns
weight: 1
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-wrr2
namespace: cross-ns
spec:
weighted:
services:
- name: whoami-svc
weight: 1
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-mirror1
namespace: default
spec:
mirroring:
name: whoami
port: 80
mirrors:
- name: whoami-svc
namespace: cross-ns
percent: 20
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: tr-svc-mirror2
namespace: cross-ns
spec:
mirroring:
name: whoami-svc
port: 80
mirrors:
- name: whoami-svc
namespace: cross-ns
percent: 20
port: 80

View file

@ -0,0 +1,58 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test-crossnamespace.route
namespace: default
spec:
entryPoints:
- foo
routes:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
services:
- name: whoami
namespace: default
port: 80
middlewares:
- name: stripprefix
namespace: cross-ns
- match: Host(`foo.com`) && PathPrefix(`/bir`)
kind: Rule
priority: 12
services:
- name: whoami
namespace: default
port: 80
middlewares:
- name: test-errorpage
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: stripprefix
namespace: cross-ns
spec:
stripPrefix:
prefixes:
- /stripit
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-errorpage
namespace: default
spec:
errors:
status:
- 500-599
query: /{status}.html
service:
name: whoami-svc
namespace: cross-ns
port: 80

View file

@ -43,6 +43,7 @@ type Provider struct {
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers." json:"disablePassHostHeaders,omitempty" toml:"disablePassHostHeaders,omitempty" yaml:"disablePassHostHeaders,omitempty" export:"true"`
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"`
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"`
@ -82,6 +83,11 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
return client, nil
}
// SetDefaults sets the default values.
func (p *Provider) SetDefaults() {
p.AllowCrossNamespace = func(b bool) *bool { return &b }(true)
}
// Init the provider.
func (p *Provider) Init() error {
return nil
@ -98,6 +104,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
return err
}
if p.AllowCrossNamespace == nil || *p.AllowCrossNamespace {
logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)")
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@ -197,7 +207,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
continue
}
errorPage, errorPageService, err := createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors)
errorPage, errorPageService, err := p.createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors)
if err != nil {
log.FromContext(ctxMid).Errorf("Error while reading error page middleware: %v", err)
continue
@ -236,7 +246,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
}
}
cb := configBuilder{client}
cb := configBuilder{client, p.AllowCrossNamespace}
for _, service := range client.GetTraefikServices() {
err := cb.buildTraefikService(ctx, service, conf.HTTP.Services)
if err != nil {
@ -281,7 +291,7 @@ func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error
return &corev1.ServicePort{Port: port}, nil
}
func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) {
func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) {
if errorPage == nil {
return nil, nil, nil
}
@ -291,7 +301,7 @@ func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alp
Query: errorPage.Query,
}
balancerServerHTTP, err := configBuilder{client}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
balancerServerHTTP, err := configBuilder{client, p.AllowCrossNamespace}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
if err != nil {
return nil, nil, err
}
@ -749,3 +759,8 @@ func throttleEvents(ctx context.Context, throttleDuration time.Duration, pool *s
return eventsChanBuffered
}
func isNamespaceAllowed(allowCrossNamespace *bool, parentNamespace, namespace string) bool {
// If allowCrossNamespace option is not defined the default behavior is to allow cross namespace references.
return allowCrossNamespace == nil || *allowCrossNamespace || parentNamespace == namespace
}

View file

@ -48,7 +48,8 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
ingressName = ingressRoute.GenerateName
}
cb := configBuilder{client}
cb := configBuilder{client, p.AllowCrossNamespace}
for _, route := range ingressRoute.Spec.Routes {
if route.Kind != "Rule" {
logger.Errorf("Unsupported match kind: %s. Only \"Rule\" is supported for now.", route.Kind)
@ -66,23 +67,10 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
continue
}
var mds []string
for _, mi := range route.Middlewares {
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
logger.
WithField(log.MiddlewareName, mi.Name).
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 = ingressRoute.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
mds, err := p.makeMiddlewareKeys(ctx, ingressRoute.Namespace, route.Middlewares)
if err != nil {
logger.Errorf("Failed to create middleware keys: %v", err)
continue
}
normalized := provider.Normalize(makeID(ingressRoute.Namespace, serviceKey))
@ -153,8 +141,38 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
return conf
}
func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace string, middlewares []v1alpha1.MiddlewareRef) ([]string, error) {
var mds []string
for _, mi := range middlewares {
if strings.Contains(mi.Name, providerNamespaceSeparator) {
if len(mi.Namespace) > 0 {
log.FromContext(ctx).
WithField(log.MiddlewareName, mi.Name).
Warnf("namespace %q is ignored in cross-provider context", mi.Namespace)
}
mds = append(mds, mi.Name)
continue
}
ns := ingRouteNamespace
if len(mi.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, ingRouteNamespace, mi.Namespace) {
return nil, fmt.Errorf("middleware %s/%s is not in the IngressRoute namespace %s", mi.Namespace, mi.Name, ingRouteNamespace)
}
ns = mi.Namespace
}
mds = append(mds, makeID(ns, mi.Name))
}
return mds, nil
}
type configBuilder struct {
client Client
client Client
allowCrossNamespace *bool
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@ -270,7 +288,7 @@ func (c configBuilder) buildServersLB(namespace string, svc v1alpha1.LoadBalance
return &dynamic.Service{LoadBalancer: lb}, nil
}
func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) {
func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBalancerSpec) ([]dynamic.Server, error) {
strategy := svc.Strategy
if strategy == "" {
strategy = roundRobinStrategy
@ -279,7 +297,11 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
return nil, fmt.Errorf("load balancing strategy %s is not supported", strategy)
}
namespace := namespaceOrFallback(svc, fallbackNamespace)
namespace := namespaceOrFallback(svc, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) {
return nil, fmt.Errorf("load balancer service %s/%s is not in the parent resource namespace %s", svc.Namespace, svc.Name, parentNamespace)
}
// If the service uses explicitly the provider suffix
sanitizedName := strings.TrimSuffix(svc.Name, providerNamespaceSeparator+providerName)
@ -355,10 +377,14 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa
// In addition, if the service is a Kubernetes one,
// it generates and returns the configuration part for such a service,
// so that the caller can add it to the global config map.
func (c configBuilder) nameAndService(ctx context.Context, namespaceService string, service v1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
func (c configBuilder) nameAndService(ctx context.Context, parentNamespace string, service v1alpha1.LoadBalancerSpec) (string, *dynamic.Service, error) {
svcCtx := log.With(ctx, log.Str(log.ServiceName, service.Name))
namespace := namespaceOrFallback(service, namespaceService)
namespace := namespaceOrFallback(service, parentNamespace)
if !isNamespaceAllowed(c.allowCrossNamespace, parentNamespace, namespace) {
return "", nil, fmt.Errorf("service %s/%s not in the parent resource namespace %s", service.Namespace, service.Name, parentNamespace)
}
switch {
case service.Kind == "" || service.Kind == "Service":

View file

@ -55,7 +55,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
serviceName := makeID(ingressRouteTCP.Namespace, key)
for _, service := range route.Services {
balancerServerTCP, err := createLoadBalancerServerTCP(client, ingressRouteTCP.Namespace, service)
balancerServerTCP, err := p.createLoadBalancerServerTCP(client, ingressRouteTCP.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
@ -125,9 +125,13 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client
return conf
}
func createLoadBalancerServerTCP(client Client, namespace string, service v1alpha1.ServiceTCP) (*dynamic.TCPService, error) {
ns := namespace
func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace string, service v1alpha1.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 = service.Namespace
}

View file

@ -2,13 +2,21 @@ package crd
import (
"context"
"io/ioutil"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v2/pkg/config/dynamic"
"github.com/traefik/traefik/v2/pkg/provider"
crdfake "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/generated/clientset/versioned/fake"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1"
"github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v2/pkg/tls"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
)
var _ provider.Provider = (*Provider)(nil)
@ -1122,7 +1130,10 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3187,7 +3198,10 @@ func TestLoadIngressRoutes(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3495,7 +3509,10 @@ func TestLoadIngressRouteUDPs(t *testing.T) {
}
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromCRD(context.Background(), newClientMock(test.paths...))
p.SetDefaults()
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
@ -3714,3 +3731,563 @@ func TestGetServicePort(t *testing.T) {
})
}
}
func TestCrossNamespace(t *testing.T) {
testCases := []struct {
desc string
allowCrossNamespace bool
ingressClass string
paths []string
expected *dynamic.Configuration
}{
{
desc: "Empty",
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP middleware cross namespace disallowed",
paths: []string{"services.yml", "with_middleware_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-9313b71dbe6a649d5049",
Rule: "Host(`foo.com`) && PathPrefix(`/bir`)",
Priority: 12,
Middlewares: []string{"default-test-errorpage"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
StripPrefix: &dynamic.StripPrefix{
Prefixes: []string{"/stripit"},
ForceSlash: false,
},
},
},
Services: map[string]*dynamic.Service{
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP middleware cross namespace allowed",
paths: []string{"services.yml", "with_middleware_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-test-crossnamespace-route-6b204d94623b3df4370c": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
Middlewares: []string{
"cross-ns-stripprefix",
},
},
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
EntryPoints: []string{"foo"},
Service: "default-test-crossnamespace-route-9313b71dbe6a649d5049",
Rule: "Host(`foo.com`) && PathPrefix(`/bir`)",
Priority: 12,
Middlewares: []string{"default-test-errorpage"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"cross-ns-stripprefix": {
StripPrefix: &dynamic.StripPrefix{
Prefixes: []string{"/stripit"},
ForceSlash: false,
},
},
"default-test-errorpage": {
Errors: &dynamic.ErrorPage{
Status: []string{"500-599"},
Service: "default-test-errorpage-errorpage-service",
Query: "/{status}.html",
},
},
},
Services: map[string]*dynamic.Service{
"default-test-crossnamespace-route-6b204d94623b3df4370c": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-test-crossnamespace-route-9313b71dbe6a649d5049": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-test-errorpage-errorpage-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP cross namespace allowed",
paths: []string{"services.yml", "with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-cross-ns-route-6b204d94623b3df4370c": {
EntryPoints: []string{"foo"},
Service: "default-cross-ns-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-cross-ns-route-6b204d94623b3df4370c": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-tr-svc-wrr1",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "cross-ns-tr-svc-wrr2",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "default-tr-svc-mirror1",
Weight: func(i int) *int { return &i }(1),
},
{
Name: "cross-ns-tr-svc-mirror2",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-whoami-svc-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"default-tr-svc-wrr1": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-tr-svc-wrr2": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"default-tr-svc-mirror1": {
Mirroring: &dynamic.Mirroring{
Service: "default-whoami-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"cross-ns-tr-svc-mirror2": {
Mirroring: &dynamic.Mirroring{
Service: "cross-ns-whoami-svc-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP cross namespace disallowed",
paths: []string{"services.yml", "with_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"cross-ns-tr-svc-wrr2": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "cross-ns-whoami-svc-80",
Weight: func(i int) *int { return &i }(1),
},
},
},
},
"cross-ns-whoami-svc-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
"cross-ns-tr-svc-mirror2": {
Mirroring: &dynamic.Mirroring{
Service: "cross-ns-whoami-svc-80",
Mirrors: []dynamic.MirrorService{
{
Name: "cross-ns-whoami-svc-80",
Percent: 20,
},
},
},
},
"default-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP cross namespace allowed",
paths: []string{"tcp/services.yml", "tcp/with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Services: map[string]*dynamic.TCPService{
"default-test.route-fdd3e9338e47a45efefc": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
{
Address: "10.10.0.2:8000",
Port: "",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP cross namespace disallowed",
paths: []string{"tcp/services.yml", "tcp/with_cross_namespace.yml"},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
// The router that references the invalid service will be discarded.
Routers: map[string]*dynamic.TCPRouter{
"default-test.route-fdd3e9338e47a45efefc": {
EntryPoints: []string{"foo"},
Service: "default-test.route-fdd3e9338e47a45efefc",
Rule: "HostSNI(`foo.com`)",
},
},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP cross namespace allowed",
paths: []string{"udp/services.yml", "udp/with_cross_namespace.yml"},
allowCrossNamespace: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{
"default-test.route-0": {
LoadBalancer: &dynamic.UDPServersLoadBalancer{
Servers: []dynamic.UDPServer{
{
Address: "10.10.0.1:8000",
Port: "",
},
{
Address: "10.10.0.2:8000",
Port: "",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP cross namespace disallowed",
paths: []string{"udp/services.yml", "udp/with_cross_namespace.yml"},
expected: &dynamic.Configuration{
// The router that references the invalid service will be discarded.
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{
"default-test.route-0": {
EntryPoints: []string{"foo"},
Service: "default-test.route-0",
},
},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var k8sObjects []runtime.Object
var crdObjects []runtime.Object
for _, path := range test.paths {
yamlContent, err := ioutil.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch o := obj.(type) {
case *corev1.Service, *corev1.Endpoints, *corev1.Secret:
k8sObjects = append(k8sObjects, o)
case *v1alpha1.IngressRoute:
crdObjects = append(crdObjects, o)
case *v1alpha1.IngressRouteTCP:
crdObjects = append(crdObjects, o)
case *v1alpha1.IngressRouteUDP:
crdObjects = append(crdObjects, o)
case *v1alpha1.Middleware:
crdObjects = append(crdObjects, o)
case *v1alpha1.TraefikService:
crdObjects = append(crdObjects, o)
case *v1alpha1.TLSOption:
crdObjects = append(crdObjects, o)
case *v1alpha1.TLSStore:
crdObjects = append(crdObjects, o)
default:
}
}
}
kubeClient := kubefake.NewSimpleClientset(k8sObjects...)
crdClient := crdfake.NewSimpleClientset(crdObjects...)
client := newClientImpl(kubeClient, crdClient)
stopCh := make(chan struct{})
eventCh, err := client.WatchAll([]string{"default", "cross-ns"}, stopCh)
require.NoError(t, err)
if k8sObjects != nil || crdObjects != nil {
// just wait for the first event
<-eventCh
}
p := Provider{}
p.SetDefaults()
p.AllowCrossNamespace = func(b bool) *bool { return &b }(test.allowCrossNamespace)
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)
})
}
}

View file

@ -36,7 +36,7 @@ func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client
serviceName := makeID(ingressRouteUDP.Namespace, key)
for _, service := range route.Services {
balancerServerUDP, err := createLoadBalancerServerUDP(client, ingressRouteUDP.Namespace, service)
balancerServerUDP, err := p.createLoadBalancerServerUDP(client, ingressRouteUDP.Namespace, service)
if err != nil {
logger.
WithField("serviceName", service.Name).
@ -77,9 +77,13 @@ func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client
return conf
}
func createLoadBalancerServerUDP(client Client, namespace string, service v1alpha1.ServiceUDP) (*dynamic.UDPService, error) {
ns := namespace
func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace string, service v1alpha1.ServiceUDP) (*dynamic.UDPService, error) {
ns := parentNamespace
if len(service.Namespace) > 0 {
if !isNamespaceAllowed(p.AllowCrossNamespace, parentNamespace, service.Namespace) {
return nil, fmt.Errorf("udp service %s/%s is not in the parent resource namespace %s", service.Namespace, service.Name, ns)
}
ns = service.Namespace
}