diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 4d27d856e..2e7892296 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -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`. diff --git a/docs/content/providers/kubernetes-crd.md b/docs/content/providers/kubernetes-crd.md index b9e89a4d9..860c82ec5 100644 --- a/docs/content/providers/kubernetes-crd.md +++ b/docs/content/providers/kubernetes-crd.md @@ -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. diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index c2be26d1f..a4c0f1822 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -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, diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 5afe6e696..966d49ae7 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -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). diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 55c3d3f50..3f2f30f00 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -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). diff --git a/integration/fixtures/k8s_crd.toml b/integration/fixtures/k8s_crd.toml index 3015b6093..86e85ee91 100644 --- a/integration/fixtures/k8s_crd.toml +++ b/integration/fixtures/k8s_crd.toml @@ -17,3 +17,4 @@ [providers.kubernetesCRD] allowCrossNamespace = false + allowExternalNameServices = true diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml index 887ddc385..50450c0bf 100644 --- a/pkg/provider/kubernetes/crd/fixtures/udp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/udp/services.yml @@ -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 + diff --git a/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml new file mode 100644 index 000000000..962178ed5 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/udp/with_externalname_service.yml @@ -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 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 7e107fed4..03f0733e4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -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 } diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index b110e9467..b179082e1 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -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 diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index fe84dffbe..f8d9deb0d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -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 diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 0c1c15637..21b486feb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -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) }) diff --git a/pkg/provider/kubernetes/crd/kubernetes_udp.go b/pkg/provider/kubernetes/crd/kubernetes_udp.go index 0346084a2..e28e56e02 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_udp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_udp.go @@ -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 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml new file mode 100644 index 000000000..e41dde42f --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_ingress.yml @@ -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 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml new file mode 100644 index 000000000..67c193cfa --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-IPv6-endpoints-externalname-enabled_service.yml @@ -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" diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml new file mode 100644 index 000000000..f9645ad09 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_ingress.yml @@ -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 diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml new file mode 100644 index 000000000..972e4cdbc --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-service-with-externalName-enabled_service.yml @@ -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 + diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index c828d48d1..3a4e03d0b 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -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 diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 3b3509037..f005064d9 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -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)