diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index dd43b9c70..772e3e748 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -355,25 +355,25 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne - b.foo.com ``` -| Ref | Attribute | Purpose | -|------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names | -| [2] | `routes` | List of routes | -| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. | -| [4] | `routes[n].priority` | [Disambiguate](../routers/index.md#priority) rules of the same length, for route matching | -| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) | -| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name | -| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace | -| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | -| [9] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | -| [10] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | -| [11] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | -| [12] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | -| [13] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | -| [14] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) | -| [15] | `tls.domains` | List of [domains](../routers/index.md#domains) | -| [16] | `domains[n].main` | Defines the main domain name | -| [17] | `domains[n].sans` | List of SANs (alternative domains) | +| Ref | Attribute | Purpose | +|------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [1] | `entryPoints` | List of [entry points](../routers/index.md#entrypoints) names | +| [2] | `routes` | List of routes | +| [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule) corresponding to an underlying router. | +| [4] | `routes[n].priority` | [Disambiguate](../routers/index.md#priority) rules of the same length, for route matching | +| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) | +| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name | +| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace | +| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) | +| [9] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | +| [10] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | +| [11] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | +| [12] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | +| [13] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | +| [14] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) | +| [15] | `tls.domains` | List of [domains](../routers/index.md#domains) | +| [16] | `domains[n].main` | Defines the main domain name | +| [17] | `domains[n].sans` | List of SANs (alternative domains) | ??? example "Declaring an IngressRoute" @@ -467,6 +467,112 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne - Setting the kubernetes service port to use port 443 (https) If you do not configure the above, Traefik will assume an http connection. + + +!!! important "Using Kubernetes ExternalName Service" + + Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/fr/docs/concepts/services-networking/service/#externalname) could be defined without any port. + Accordingly, Traefik supports defining a port in two ways: + + - only on `IngressRoute` service + - on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used + + Thus, in case of two sides port definition, Traefik expects a match between ports. + + ??? example "Examples" + + ```yaml tab="IngressRoute" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ``` + + ```yaml tab="ExternalName Service" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + + ```yaml tab="Both sides" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` ### Kind: `Middleware` @@ -853,7 +959,7 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube | [1] | `entryPoints` | List of [entrypoints](../routers/index.md#entrypoints_1) names | | [2] | `routes` | List of routes | | [3] | `routes[n].match` | Defines the [rule](../routers/index.md#rule_1) corresponding to an underlying router | -| [4] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions | +| [4] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions (See below for `ExternalName Service` setup) | | [5] | `services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | | [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) | | [7] | `services[n].weight` | Defines the weight to apply to the server load balancing | @@ -928,6 +1034,111 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= ``` +!!! important "Using Kubernetes ExternalName Service" + + Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/fr/docs/concepts/services-networking/service/#externalname) could be defined without any port. + Accordingly, Traefik supports defining a port in two ways: + + - only on `IngressRouteTCP` service + - on both sides, you'll be warned if the ports don't match, and the `IngressRouteTCP` service port is used + + Thus, in case of two sides port definition, Traefik expects a match between ports. + + ??? example "Examples" + + ```yaml tab="IngressRouteTCP" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRouteTCP + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`*`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ``` + + ```yaml tab="ExternalName Service" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRouteTCP + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`*`) + kind: Rule + services: + - name: external-svc + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + + ```yaml tab="Both sides" + --- + apiVersion: traefik.containo.us/v1alpha1 + kind: IngressRouteTCP + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`*`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + ### Kind `IngressRouteUDP` `IngressRouteUDP` is the CRD implementation of a [Traefik UDP router](../routers/index.md#configuring-udp-routers). diff --git a/integration/fixtures/k8s/02-services.yml b/integration/fixtures/k8s/02-services.yml index e02661c7c..836a91d26 100644 --- a/integration/fixtures/k8s/02-services.yml +++ b/integration/fixtures/k8s/02-services.yml @@ -126,3 +126,13 @@ spec: selector: app: containous task: whoamiudp + +--- +apiVersion: v1 +kind: Service +metadata: + name: externalname-svc + namespace: default +spec: + externalName: domain.com + type: ExternalName \ No newline at end of file diff --git a/integration/fixtures/k8s/05-ingressroutetcp.yml b/integration/fixtures/k8s/05-ingressroutetcp.yml index ee4eedc51..a717a1999 100644 --- a/integration/fixtures/k8s/05-ingressroutetcp.yml +++ b/integration/fixtures/k8s/05-ingressroutetcp.yml @@ -13,6 +13,8 @@ spec: - name: whoamitcp namespace: default port: 8080 + - name: externalname-svc + port: 9090 tls: options: name: mytlsoption diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index d4ad9550d..570bee4d4 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -194,18 +194,44 @@ } }, "tcpServices": { - "default-test3.route-673acf455cb2dab0b43a@kubernetescrd": { + "default-test3.route-673acf455cb2dab0b43a-externalname-svc-9090@kubernetescrd": { "loadBalancer": { "terminationDelay": 100, "servers": [ { - "address": "10.42.0.4:8080" + "address": "domain.com:9090" + } + ] + }, + "status": "enabled" + }, + "default-test3.route-673acf455cb2dab0b43a-whoamitcp-8080@kubernetescrd": { + "loadBalancer": { + "terminationDelay": 100, + "servers": [ + { + "address": "10.42.0.3:8080" }, { "address": "10.42.0.8:8080" } ] }, + "status": "enabled" + }, + "default-test3.route-673acf455cb2dab0b43a@kubernetescrd": { + "weighted": { + "services": [ + { + "name": "default-test3.route-673acf455cb2dab0b43a-whoamitcp-8080", + "weight": 1 + }, + { + "name": "default-test3.route-673acf455cb2dab0b43a-externalname-svc-9090", + "weight": 1 + } + ] + }, "status": "enabled", "usedBy": [ "default-test3.route-673acf455cb2dab0b43a@kubernetescrd" diff --git a/pkg/provider/kubernetes/crd/fixtures/services.yml b/pkg/provider/kubernetes/crd/fixtures/services.yml index 967e9d96a..0616140dd 100644 --- a/pkg/provider/kubernetes/crd/fixtures/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/services.yml @@ -118,3 +118,44 @@ subsets: ports: - name: websecure2 port: 8443 + +--- +apiVersion: v1 +kind: Service +metadata: + name: external-svc + namespace: default +spec: + externalName: external.domain + type: ExternalName + +--- +apiVersion: v1 +kind: Service +metadata: + name: external-svc-with-http + namespace: default +spec: + externalName: external.domain + type: ExternalName + ports: + - name: http + protocol: TCP + port: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: external-svc-with-https + namespace: default +spec: + externalName: external.domain + type: ExternalName + ports: + - name: https + protocol: TCP + port: 443 + + + diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml index 00018456c..373d8bb9a 100644 --- a/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/services.yml @@ -131,3 +131,40 @@ subsets: ports: - name: myapp4 port: 8084 + +--- +apiVersion: v1 +kind: Service +metadata: + name: external-svc + namespace: default +spec: + externalName: external.domain + type: ExternalName + +--- +apiVersion: v1 +kind: Service +metadata: + name: external.service.with.port + namespace: default +spec: + externalName: external.domain + type: ExternalName + ports: + - name: http + protocol: TCP + port: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: external.service.without.port + namespace: default +spec: + externalName: external.domain + type: ExternalName + ports: + - name: http + protocol: TCP \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml new file mode 100644 index 000000000..43226be28 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: external-svc + port: 8000 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml new file mode 100644 index 000000000..3645601c5 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_with_port.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: external.service.with.port + port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml new file mode 100644 index 000000000..376d82469 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/tcp/with_externalname_without_ports.yml @@ -0,0 +1,14 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRouteTCP +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: HostSNI(`foo.com`) + services: + - name: external-svc diff --git a/pkg/provider/kubernetes/crd/fixtures/with_externalname.yml b/pkg/provider/kubernetes/crd/fixtures/with_externalname.yml new file mode 100644 index 000000000..b70ec012e --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_externalname.yml @@ -0,0 +1,16 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc + port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_http.yml b/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_http.yml new file mode 100644 index 000000000..1f3e3a3f7 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_http.yml @@ -0,0 +1,16 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc-with-http + port: 80 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_https.yml b/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_https.yml new file mode 100644 index 000000000..21c39a1c3 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_externalname_with_https.yml @@ -0,0 +1,16 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc-with-https + port: 443 diff --git a/pkg/provider/kubernetes/crd/fixtures/with_externalname_without_ports.yml b/pkg/provider/kubernetes/crd/fixtures/with_externalname_without_ports.yml new file mode 100644 index 000000000..25f230574 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_externalname_without_ports.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - foo + + routes: + - match: Host(`foo.com`) + kind: Rule + services: + - name: external-svc \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index 731ce23a4..0ddaf4a6f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "crypto/sha256" + "errors" "fmt" "os" "sort" @@ -248,6 +249,38 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } +func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error) { + if svc == nil { + return nil, errors.New("service is not defined") + } + + if port == 0 { + return nil, errors.New("ingressRoute service port not defined") + } + + hasValidPort := false + for _, p := range svc.Spec.Ports { + if p.Port == port { + return &p, nil + } + + if p.Port != 0 { + hasValidPort = true + } + } + + if svc.Spec.Type != corev1.ServiceTypeExternalName { + return nil, fmt.Errorf("service port not found: %d", port) + } + + if hasValidPort { + log.WithoutContext(). + Warning("The port %d from IngressRoute doesn't match with ports defined in the ExternalName service %s/%s.", port, svc.Namespace, svc.Name) + } + + return &corev1.ServicePort{Port: port}, nil +} + func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { if errorPage == nil { return nil, nil, nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index d77b18fc6..776e7e4fb 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -294,27 +294,20 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa return nil, fmt.Errorf("kubernetes service not found: %s/%s", namespace, sanitizedName) } - confPort := svc.Port - var portSpec *corev1.ServicePort - for _, p := range service.Spec.Ports { - if confPort == p.Port { - portSpec = &p - break - } - } - if portSpec == nil { - return nil, errors.New("service port not found") + svcPort, err := getServicePort(service, svc.Port) + if err != nil { + return nil, err } var servers []dynamic.Server if service.Spec.Type == corev1.ServiceTypeExternalName { - protocol, err := parseServiceProtocol(svc.Scheme, portSpec.Name, portSpec.Port) + protocol, err := parseServiceProtocol(svc.Scheme, svcPort.Name, svcPort.Port) if err != nil { return nil, err } return append(servers, dynamic.Server{ - URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, portSpec.Port), + URL: fmt.Sprintf("%s://%s:%d", protocol, service.Spec.ExternalName, svcPort.Port), }), nil } @@ -332,7 +325,7 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa var port int32 for _, subset := range endpoints.Subsets { for _, p := range subset.Ports { - if portSpec.Name == p.Name { + if svcPort.Name == p.Name { port = p.Port break } @@ -342,7 +335,7 @@ func (c configBuilder) loadServers(fallbackNamespace string, svc v1alpha1.LoadBa return nil, fmt.Errorf("cannot define a port for %s/%s", namespace, sanitizedName) } - protocol, err := parseServiceProtocol(svc.Scheme, portSpec.Name, portSpec.Port) + 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 95e6fb9e7..9229cc4ce 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -162,22 +162,15 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([ return nil, errors.New("service not found") } - var portSpec *corev1.ServicePort - for _, p := range service.Spec.Ports { - if svc.Port == p.Port { - portSpec = &p - break - } - } - - if portSpec == nil { - return nil, errors.New("service port not found") + svcPort, err := getServicePort(service, svc.Port) + if err != nil { + return nil, err } var servers []dynamic.TCPServer if service.Spec.Type == corev1.ServiceTypeExternalName { servers = append(servers, dynamic.TCPServer{ - Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, portSpec.Port), + Address: fmt.Sprintf("%s:%d", service.Spec.ExternalName, svcPort.Port), }) } else { endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, svc.Name) @@ -196,7 +189,7 @@ func loadTCPServers(client Client, namespace string, svc v1alpha1.ServiceTCP) ([ var port int32 for _, subset := range endpoints.Subsets { for _, p := range subset.Ports { - if portSpec.Name == p.Name { + if svcPort.Name == p.Name { port = p.Port break } diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 195705a8b..6aa982a0f 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + corev1 "k8s.io/api/core/v1" + "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/provider" "github.com/containous/traefik/v2/pkg/tls" @@ -906,6 +908,106 @@ func TestLoadIngressRouteTCPs(t *testing.T) { }, }, }, + { + desc: "Simple Ingress Route, with externalName service", + paths: []string{"tcp/services.yml", "tcp/with_externalname.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{ + "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:8000", + Port: "", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Ingress Route, externalName service with port", + 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{ + 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: "", + }, + }, + }, + }, + }, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Ingress Route, externalName service without port", + paths: []string{"tcp/services.yml", "tcp/with_externalname_without_ports.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{ + "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{}, + }, + }, } for _, test := range testCases { @@ -2860,6 +2962,134 @@ func TestLoadIngressRoutes(t *testing.T) { { desc: "port selected by name (TODO)", }, + { + desc: "Simple Ingress Route, with externalName service", + paths: []string{"services.yml", "with_externalname.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-route-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + }}, + 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: "Ingress Route, externalName service with http", + 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{ + Routers: map[string]*dynamic.Router{ + "default-test-route-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + }}, + 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: "Ingress Route, externalName service with https", + paths: []string{"services.yml", "with_externalname_with_https.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-route-6f97418635c7e18853da": { + EntryPoints: []string{"foo"}, + Service: "default-test-route-6f97418635c7e18853da", + Rule: "Host(`foo.com`)", + }}, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test-route-6f97418635c7e18853da": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "https://external.domain:443", + }, + }, + PassHostHeader: Bool(true), + }, + }, + }, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, + { + desc: "Ingress Route, externalName service without ports", + paths: []string{"services.yml", "with_externalname_without_ports.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{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, } for _, test := range testCases { @@ -3255,3 +3485,148 @@ func TestParseServiceProtocol(t *testing.T) { }) } } + +func TestGetServicePort(t *testing.T) { + testCases := []struct { + desc string + svc *corev1.Service + port int32 + expected *corev1.ServicePort + expectError bool + }{ + { + desc: "Basic", + expectError: true, + }, + { + desc: "Matching ports, with no service type", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + port: 80, + expected: &corev1.ServicePort{ + Port: 80, + }, + }, + { + desc: "Matching ports 0", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {}, + }, + }, + }, + expectError: true, + }, + { + desc: "Matching ports 0 (with external name)", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + Ports: []corev1.ServicePort{ + {}, + }, + }, + }, + expectError: true, + }, + { + desc: "Mismatching, only port(Ingress) defined", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{}, + }, + port: 80, + expectError: true, + }, + { + desc: "Mismatching, only port(Ingress) defined with external name", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + }, + }, + port: 80, + expected: &corev1.ServicePort{ + Port: 80, + }, + }, + { + desc: "Mismatching, only Service port defined", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + expectError: true, + }, + { + desc: "Mismatching, only Service port defined with external name", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + expectError: true, + }, + { + desc: "Two different ports defined", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + port: 443, + expectError: true, + }, + { + desc: "Two different ports defined (with external name)", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + port: 443, + expected: &corev1.ServicePort{ + Port: 443, + }, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual, err := getServicePort(test.svc, test.port) + if test.expectError { + assert.Error(t, err) + } else { + assert.Equal(t, test.expected, actual) + } + }) + } +}