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

@ -250,6 +250,34 @@ providers:
--providers.kubernetescrd.throttleDuration=10s
```
### `allowCrossNamespace`
_Optional, Default: true_
```toml tab="File (TOML)"
[providers.kubernetesCRD]
allowCrossNamespace = false
# ...
```
```yaml tab="File (YAML)"
providers:
kubernetesCRD:
allowCrossNamespace: false
# ...
```
```bash tab="CLI"
--providers.kubernetescrd.allowCrossNamespace=false
```
If the parameter is set to `false`, an IngressRoute will not be able to reference any resources
in another namespace than the IngressRoute namespace.
!!! warning "Deprecation"
Please notice that the default value for this option will be set to `false` in a future version.
## Further
Also see the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt.

View file

@ -540,6 +540,9 @@ TLS key
`--providers.kubernetescrd`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetescrd.allowcrossnamespace`:
Allow cross namespace resource reference. (Default: ```true```)
`--providers.kubernetescrd.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -540,6 +540,9 @@ TLS key
`TRAEFIK_PROVIDERS_KUBERNETESCRD`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
Allow cross namespace resource reference. (Default: ```true```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -113,6 +113,7 @@
certAuthFilePath = "foobar"
disablePassHostHeaders = true
namespaces = ["foobar", "foobar"]
allowCrossNamespace = true
labelSelector = "foobar"
ingressClass = "foobar"
throttleDuration = 42

View file

@ -123,6 +123,7 @@ providers:
namespaces:
- foobar
- foobar
allowCrossNamespace: true
labelSelector: foobar
ingressClass: foobar
throttleDuration: 42s

View file

@ -0,0 +1,120 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: other-ns
---
apiVersion: v1
kind: Service
metadata:
name: whoami
namespace: other-ns
spec:
ports:
- name: http
port: 80
selector:
app: traefiklabs
task: whoami
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: test6.route
namespace: other-ns
spec:
entryPoints:
- web
routes:
- match: Host(`foo.com`) && PathPrefix(`/a`)
kind: Rule
services:
- name: whoami
namespace: default
port: 80
- match: Host(`foo.com`) && PathPrefix(`/b`)
kind: Rule
services:
- name: wrr2
namespace: default
kind: TraefikService
- match: Host(`foo.com`) && PathPrefix(`/c`)
kind: Rule
services:
- name: wrr3
kind: TraefikService
- match: Host(`foo.com`) && PathPrefix(`/d`)
kind: Rule
services:
- name: whoami
namespace: other-ns
port: 80
middlewares:
- name: stripprefix2
namespace: default
- match: Host(`foo.com`) && PathPrefix(`/e`)
kind: Rule
services:
- name: whoami
port: 80
middlewares:
- name: test-errorpage
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: wrr2
namespace: default
spec:
weighted:
services:
- name: whoami
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: TraefikService
metadata:
name: wrr3
namespace: other-ns
spec:
weighted:
services:
- name: whoami
namespace: default
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: stripprefix2
namespace: default
spec:
stripPrefix:
prefixes:
- /tobestripped
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-errorpage
spec:
errors:
status:
- 500-599
query: /{status}.html
service:
name: whoami
namespace: other-ns
port: 80

View file

@ -16,3 +16,4 @@
address = ":8000"
[providers.kubernetesCRD]
allowCrossNamespace = false

View file

@ -53,6 +53,7 @@ func (s *K8sSuite) TearDownSuite(c *check.C) {
"./fixtures/k8s/coredns.yaml",
"./fixtures/k8s/rolebindings.yaml",
"./fixtures/k8s/traefik.yaml",
"./fixtures/k8s/ccm.yaml",
}
for _, filename := range generatedFiles {

View file

@ -50,6 +50,20 @@
"using": [
"web"
]
},
"other-ns-test6-route-482e4988e134701d8cc8@kubernetescrd": {
"entryPoints": [
"web"
],
"service": "other-ns-wrr3",
"rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/c`)",
"error": [
"the service \"other-ns-wrr3@kubernetescrd\" does not exist"
],
"status": "disabled",
"using": [
"web"
]
}
},
"middlewares": {
@ -64,6 +78,14 @@
"default-test2-route-23c7f4c450289ee29016@kubernetescrd"
]
},
"default-stripprefix2@kubernetescrd": {
"stripPrefix": {
"prefixes": [
"/tobestripped"
]
},
"status": "enabled"
},
"default-stripprefix@kubernetescrd": {
"stripPrefix": {
"prefixes": [
@ -172,6 +194,17 @@
"default-test3-route-7d0ac22d3d8db4b82618@kubernetescrd"
]
},
"default-wrr2@kubernetescrd": {
"weighted": {
"services": [
{
"name": "default-whoami-80",
"weight": 1
}
]
},
"status": "enabled"
},
"noop@internal": {
"status": "enabled"
}

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
}