Disable ExternalName Services by default on Kubernetes providers

This commit is contained in:
Daniel Tomcej 2021-07-13 04:54:09 -06:00 committed by GitHub
parent 10ab39c33b
commit 3c1ed0d9b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 637 additions and 102 deletions

View file

@ -370,3 +370,8 @@ In `v2.4.9`, we changed span error to log only server errors (>= 500).
### K8S CrossNamespace
In `v2.4.10`, the default value for `allowCrossNamespace` has been changed to `false`.
### K8S ExternalName Service
In `v2.4.10`, by default, it is no longer authorized to reference Kubernetes ExternalName services.
To allow it, the `allowExternalNameServices` option should be set to `true`.

View file

@ -281,6 +281,29 @@ providers:
--providers.kubernetescrd.allowCrossNamespace=true
```
### `allowExternalNameServices`
_Optional, Default: false_
If the parameter is set to `true`, IngressRoutes are able to reference ExternalName services.
```yaml tab="File (YAML)"
providers:
kubernetesCRD:
allowExternalNameServices: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesCRD]
allowExternalNameServices = true
# ...
```
```bash tab="CLI"
--providers.kubernetescrd.allowexternalnameservices=true
```
## Full Example
For additional information, refer to the [full example](../user-guides/crd-acme/index.md) with Let's Encrypt.

View file

@ -375,6 +375,29 @@ providers:
--providers.kubernetesingress.throttleDuration=10s
```
### `allowExternalNameServices`
_Optional, Default: false_
If the parameter is set to `true`, Ingresses are able to reference ExternalName services.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
allowExternalNameServices: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
allowExternalNameServices = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.allowexternalnameservices=true
```
### Further
To learn more about the various aspects of the Ingress specification that Traefik supports,

View file

@ -558,6 +558,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetescrd.allowcrossnamespace`:
Allow cross namespace resource reference. (Default: ```false```)
`--providers.kubernetescrd.allowexternalnameservices`:
Allow ExternalName services. (Default: ```false```)
`--providers.kubernetescrd.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).
@ -603,6 +606,9 @@ Kubernetes bearer token (not needed for in-cluster client).
`--providers.kubernetesingress`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetesingress.allowexternalnameservices`:
Allow ExternalName services. (Default: ```false```)
`--providers.kubernetesingress.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -558,6 +558,9 @@ Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWCROSSNAMESPACE`:
Allow cross namespace resource reference. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_ALLOWEXTERNALNAMESERVICES`:
Allow ExternalName services. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESCRD_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).
@ -603,6 +606,9 @@ Kubernetes bearer token (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEXTERNALNAMESERVICES`:
Allow ExternalName services. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -17,3 +17,4 @@
[providers.kubernetesCRD]
allowCrossNamespace = false
allowExternalNameServices = true

View file

@ -160,3 +160,16 @@ subsets:
ports:
- name: myapp
port: 8000
---
kind: Service
apiVersion: v1
metadata:
name: external.service.with.port
namespace: default
spec:
externalName: external.domain
type: ExternalName
ports:
- name: http
port: 80

View file

@ -0,0 +1,14 @@
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteUDP
metadata:
name: test.route
namespace: default
spec:
entryPoints:
- foo
routes:
- services:
- name: external.service.with.port
port: 80

View file

@ -38,15 +38,16 @@ 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 string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
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"`
lastConfiguration safe.Safe
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
AllowCrossNamespace bool `description:"Allow cross namespace resource reference." json:"allowCrossNamespace,omitempty" toml:"allowCrossNamespace,omitempty" yaml:"allowCrossNamespace,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
lastConfiguration safe.Safe
}
func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
@ -102,6 +103,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
logger.Warn("Cross-namespace reference between IngressRoutes and resources is enabled, please ensure that this is expected (see AllowCrossNamespace option)")
}
if p.AllowExternalNameServices {
logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@ -240,7 +245,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
}
}
cb := configBuilder{client, p.AllowCrossNamespace}
cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}
for _, service := range client.GetTraefikServices() {
err := cb.buildTraefikService(ctx, service, conf.HTTP.Services)
@ -360,7 +365,7 @@ func (p *Provider) createErrorPageMiddleware(client Client, namespace string, er
Query: errorPage.Query,
}
balancerServerHTTP, err := configBuilder{client, p.AllowCrossNamespace}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
balancerServerHTTP, err := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}.buildServersLB(namespace, errorPage.Service.LoadBalancerSpec)
if err != nil {
return nil, nil, err
}

View file

@ -49,7 +49,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
ingressName = ingressRoute.GenerateName
}
cb := configBuilder{client, p.AllowCrossNamespace}
cb := configBuilder{client: client, allowCrossNamespace: p.AllowCrossNamespace, allowExternalNameServices: p.AllowExternalNameServices}
for _, route := range ingressRoute.Spec.Routes {
if route.Kind != "Rule" {
@ -172,8 +172,9 @@ func (p *Provider) makeMiddlewareKeys(ctx context.Context, ingRouteNamespace str
}
type configBuilder struct {
client Client
allowCrossNamespace bool
client Client
allowCrossNamespace bool
allowExternalNameServices bool
}
// buildTraefikService creates the configuration for the traefik service defined in tService,
@ -322,6 +323,10 @@ func (c configBuilder) loadServers(parentNamespace string, svc v1alpha1.LoadBala
var servers []dynamic.Server
if service.Spec.Type == corev1.ServiceTypeExternalName {
if !c.allowExternalNameServices {
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, sanitizedName)
}
protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port)
if err != nil {
return nil, err

View file

@ -135,7 +135,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st
ns = service.Namespace
}
servers, err := loadTCPServers(client, ns, service)
servers, err := p.loadTCPServers(client, ns, service)
if err != nil {
return nil, err
}
@ -162,7 +162,7 @@ func (p *Provider) createLoadBalancerServerTCP(client Client, parentNamespace st
return tcpService, nil
}
func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) {
func (p *Provider) loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([]dynamic.TCPServer, error) {
service, exists, err := client.GetService(namespace, svc.Name)
if err != nil {
return nil, err
@ -172,6 +172,10 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([
return nil, errors.New("service not found")
}
if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices {
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name)
}
svcPort, err := getServicePort(service, svc.Port)
if err != nil {
return nil, err

View file

@ -1153,7 +1153,7 @@ func TestLoadIngressRouteTCPs(t *testing.T) {
return
}
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true}
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true}
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
@ -3337,7 +3337,7 @@ func TestLoadIngressRoutes(t *testing.T) {
return
}
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true}
p := Provider{IngressClass: test.ingressClass, AllowCrossNamespace: true, AllowExternalNameServices: true}
clientMock := newClientMock(test.paths...)
conf := p.loadConfigurationFromCRD(context.Background(), clientMock)
@ -4435,9 +4435,292 @@ func TestCrossNamespace(t *testing.T) {
<-eventCh
}
p := Provider{}
p := Provider{AllowCrossNamespace: test.allowCrossNamespace}
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)
})
}
}
func TestExternalNameService(t *testing.T) {
testCases := []struct {
desc string
allowExternalNameService 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{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP ExternalName services allowed",
paths: []string{"services.yml", "with_externalname_with_http.yml"},
allowExternalNameService: 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{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{
"default-test-route-6f97418635c7e18853da": {
EntryPoints: []string{"foo"},
Service: "default-test-route-6f97418635c7e18853da",
Rule: "Host(`foo.com`)",
Priority: 0,
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-test-route-6f97418635c7e18853da": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://external.domain:80",
},
},
PassHostHeader: Bool(true),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "HTTP Externalname services disallowed",
paths: []string{"services.yml", "with_externalname_with_http.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{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP ExternalName services allowed",
paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.yml"},
allowExternalNameService: true,
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
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: "external.domain:80",
Port: "",
},
},
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "TCP ExternalName services disallowed",
paths: []string{"tcp/services.yml", "tcp/with_externalname_with_port.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{
ServersTransports: map[string]*dynamic.ServersTransport{},
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "UDP ExternalName services allowed",
paths: []string{"udp/services.yml", "udp/with_externalname_service.yml"},
allowExternalNameService: 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: "external.domain:80",
Port: "",
},
},
},
},
},
},
HTTP: &dynamic.HTTPConfiguration{
ServersTransports: map[string]*dynamic.ServersTransport{},
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 ExternalName service disallowed",
paths: []string{"udp/services.yml", "udp/with_externalname_service.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{
ServersTransports: map[string]*dynamic.ServersTransport{},
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 := os.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{AllowExternalNameServices: test.allowExternalNameService}
p.AllowCrossNamespace = test.allowCrossNamespace
conf := p.loadConfigurationFromCRD(context.Background(), client)
assert.Equal(t, test.expected, conf)
})

View file

@ -87,7 +87,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st
ns = service.Namespace
}
servers, err := loadUDPServers(client, ns, service)
servers, err := p.loadUDPServers(client, ns, service)
if err != nil {
return nil, err
}
@ -101,7 +101,7 @@ func (p *Provider) createLoadBalancerServerUDP(client Client, parentNamespace st
return udpService, nil
}
func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) {
func (p *Provider) loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([]dynamic.UDPServer, error) {
service, exists, err := client.GetService(namespace, svc.Name)
if err != nil {
return nil, err
@ -111,6 +111,10 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([
return nil, errors.New("service not found")
}
if service.Spec.Type == corev1.ServiceTypeExternalName && !p.AllowExternalNameServices {
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, svc.Name)
}
var portSpec *corev1.ServicePort
for _, p := range service.Spec.Ports {
p := p

View file

@ -0,0 +1,14 @@
kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata:
name: example.com
namespace: testing
spec:
rules:
- http:
paths:
- path: /foo
backend:
serviceName: service-foo
servicePort: 8080

View file

@ -0,0 +1,13 @@
---
kind: Service
apiVersion: v1
metadata:
name: service-foo
namespace: testing
spec:
ports:
- name: http
port: 8080
type: ExternalName
externalName: "2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b"

View file

@ -0,0 +1,15 @@
kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: traefik.tchouk
http:
paths:
- path: /bar
backend:
serviceName: service1
servicePort: 8080

View file

@ -0,0 +1,13 @@
kind: Service
apiVersion: v1
metadata:
name: service1
namespace: testing
spec:
ports:
- port: 8080
clusterIP: 10.0.0.1
type: ExternalName
externalName: traefik.wtf

View file

@ -37,15 +37,16 @@ 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 string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes Ingress 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"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
lastConfiguration safe.Safe
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes Ingress 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"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
lastConfiguration safe.Safe
}
// EndpointIngress holds the endpoint information for the Kubernetes provider.
@ -107,6 +108,10 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
return err
}
if p.AllowExternalNameServices {
logger.Warn("ExternalName service loading is enabled, please ensure that this is expected (see AllowExternalNameServices option)")
}
pool.GoCtx(func(ctxPool context.Context) {
operation := func() error {
eventsChan, err := k8sClient.WatchAll(p.Namespaces, ctxPool.Done())
@ -228,7 +233,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
service, err := loadService(client, ingress.Namespace, *ingress.Spec.Backend)
service, err := p.loadService(client, ingress.Namespace, *ingress.Spec.Backend)
if err != nil {
log.FromContext(ctx).
WithField("serviceName", ingress.Spec.Backend.ServiceName).
@ -265,7 +270,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
}
for _, pa := range rule.HTTP.Paths {
service, err := loadService(client, ingress.Namespace, pa.Backend)
service, err := p.loadService(client, ingress.Namespace, pa.Backend)
if err != nil {
log.FromContext(ctx).
WithField("serviceName", pa.Backend.ServiceName).
@ -460,7 +465,7 @@ func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores
return configs
}
func loadService(client Client, namespace string, backend networkingv1beta1.IngressBackend) (*dynamic.Service, error) {
func (p *Provider) loadService(client Client, namespace string, backend networkingv1beta1.IngressBackend) (*dynamic.Service, error) {
service, exists, err := client.GetService(namespace, backend.ServiceName)
if err != nil {
return nil, err
@ -470,6 +475,10 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr
return nil, errors.New("service not found")
}
if !p.AllowExternalNameServices && service.Spec.Type == corev1.ServiceTypeExternalName {
return nil, fmt.Errorf("externalName services not allowed: %s/%s", namespace, backend.ServiceName)
}
var portName string
var portSpec corev1.ServicePort
var match bool

View file

@ -732,33 +732,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
},
},
{
desc: "Ingress with service with externalName",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"testing-traefik-tchouk-bar": {
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
Service: "testing-service1-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true),
Servers: []dynamic.Server{
{
URL: "http://traefik.wtf:8080",
},
},
},
},
},
},
},
},
{
desc: "Ingress with port invalid for one service",
expected: &dynamic.Configuration{
@ -786,47 +759,6 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
},
},
{
desc: "Ingress with IPv6 endpoints",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"example-com-testing-bar": {
Rule: "PathPrefix(`/bar`)",
Service: "testing-service-bar-8080",
},
"example-com-testing-foo": {
Rule: "PathPrefix(`/foo`)",
Service: "testing-service-foo-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service-bar-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
"testing-service-foo-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
},
},
},
},
{
desc: "TLS support",
expected: &dynamic.Configuration{
@ -1332,6 +1264,152 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var paths []string
_, err := os.Stat(generateTestFilename("_ingress", test.desc))
if err == nil {
paths = append(paths, generateTestFilename("_ingress", test.desc))
}
_, err = os.Stat(generateTestFilename("_endpoint", test.desc))
if err == nil {
paths = append(paths, generateTestFilename("_endpoint", test.desc))
}
_, err = os.Stat(generateTestFilename("_service", test.desc))
if err == nil {
paths = append(paths, generateTestFilename("_service", test.desc))
}
_, err = os.Stat(generateTestFilename("_secret", test.desc))
if err == nil {
paths = append(paths, generateTestFilename("_secret", test.desc))
}
_, err = os.Stat(generateTestFilename("_ingressclass", test.desc))
if err == nil {
paths = append(paths, generateTestFilename("_ingressclass", test.desc))
}
serverVersion := test.serverVersion
if serverVersion == "" {
serverVersion = "v1.17"
}
clientMock := newClientMock(serverVersion, paths...)
p := Provider{IngressClass: test.ingressClass}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)
})
}
}
func TestLoadConfigurationFromIngressesWithExternalNameServices(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
serverVersion string
allowExternalNameServices bool
expected *dynamic.Configuration
}{
{
desc: "Ingress with service with externalName",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
},
},
},
{
desc: "Ingress with service with externalName enabled",
allowExternalNameServices: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"testing-traefik-tchouk-bar": {
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
Service: "testing-service1-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service1-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true),
Servers: []dynamic.Server{
{
URL: "http://traefik.wtf:8080",
},
},
},
},
},
},
},
},
{
desc: "Ingress with IPv6 endpoints",
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"example-com-testing-bar": {
Rule: "PathPrefix(`/bar`)",
Service: "testing-service-bar-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service-bar-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
},
},
},
},
{
desc: "Ingress with IPv6 endpoints externalname enabled",
allowExternalNameServices: true,
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{
"example-com-testing-foo": {
Rule: "PathPrefix(`/foo`)",
Service: "testing-service-foo-8080",
},
},
Services: map[string]*dynamic.Service{
"testing-service-foo-8080": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://[2001:0db8:3c4d:0015:0000:0000:1a2f:2a3b]:8080",
},
},
PassHostHeader: Bool(true),
},
},
},
},
},
},
}
for _, test := range testCases {
test := test
@ -1368,6 +1446,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
clientMock := newClientMock(serverVersion, paths...)
p := Provider{IngressClass: test.ingressClass}
p.AllowExternalNameServices = test.allowExternalNameServices
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)