From bbee63fcf37c84940c248e5978f79e886698e4b1 Mon Sep 17 00:00:00 2001 From: Cirrith Date: Fri, 15 Jan 2021 06:54:04 -0800 Subject: [PATCH] Add named port support to Kubernetes IngressRoute CRDs --- .../routing/providers/kubernetes-crd.md | 65 ++++++------ pkg/provider/kubernetes/crd/kubernetes.go | 13 +-- .../kubernetes/crd/kubernetes_http.go | 9 +- pkg/provider/kubernetes/crd/kubernetes_tcp.go | 2 +- .../kubernetes/crd/kubernetes_test.go | 99 +++++++++++++++++-- pkg/provider/kubernetes/crd/kubernetes_udp.go | 5 +- .../crd/traefik/v1alpha1/ingressroute.go | 3 +- .../crd/traefik/v1alpha1/ingressroutetcp.go | 3 +- .../crd/traefik/v1alpha1/ingressrouteudp.go | 9 +- .../traefik/v1alpha1/zz_generated.deepcopy.go | 3 + 10 files changed, 154 insertions(+), 57 deletions(-) diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 302a28020..0e4ec863d 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -145,7 +145,7 @@ The Kubernetes Ingress Controller, The Custom Resource Way. spec: entryPoints: - - fooudp + - udpep routes: - kind: Rule services: @@ -331,7 +331,7 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne name: foo namespace: default passHostHeader: true - port: 80 + port: 80 # [9] responseForwarding: flushInterval: 1ms scheme: https @@ -343,38 +343,39 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne sameSite: none strategy: RoundRobin weight: 10 - tls: # [9] - secretName: supersecret # [10] - options: # [11] - name: opt # [12] - namespace: default # [13] - certResolver: foo # [14] - domains: # [15] - - main: example.net # [16] - sans: # [17] + tls: # [10] + secretName: supersecret # [11] + options: # [12] + name: opt # [13] + namespace: default # [14] + certResolver: foo # [15] + domains: # [16] + - main: example.net # [17] + sans: # [18] - a.example.net - b.example.net ``` -| 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) | +| 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] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | +| [10] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | +| [11] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) | +| [12] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) | +| [13] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name | +| [14] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace | +| [15] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) | +| [16] | `tls.domains` | List of [domains](../routers/index.md#domains) | +| [17] | `domains[n].main` | Defines the main domain name | +| [18] | `domains[n].sans` | List of SANs (alternative domains) | ??? example "Declaring an IngressRoute" @@ -1113,7 +1114,7 @@ Register the `IngressRouteTCP` [kind](../../reference/dynamic-configuration/kube | [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 (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/) | +| [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | [7] | `services[n].weight` | Defines the weight to apply to the server load balancing | | [8] | `services[n].terminationDelay` | corresponds to the deadline that the proxy sets, after one of its connected peers indicates it has closed the writing capability of its connection, to close the reading capability as well, hence fully terminating the connection. It is a duration in milliseconds, defaulting to 100. A negative value means an infinite deadline (i.e. the reading capability is never closed). | | [9] | `proxyProtocol` | Defines the [PROXY protocol](../services/index.md#proxy-protocol) configuration | @@ -1323,7 +1324,7 @@ Register the `IngressRouteUDP` [kind](../../reference/dynamic-configuration/kube | [2] | `routes` | List of routes | | [3] | `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions | | [4] | `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/) | +| [6] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | [7] | `services[n].weight` | Defines the weight to apply to the server load balancing | ??? example "Declaring an IngressRouteUDP" diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index ccda4a173..4b8417632 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -24,6 +24,7 @@ import ( "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -323,18 +324,18 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } -func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error) { +func getServicePort(svc *corev1.Service, port intstr.IntOrString) (*corev1.ServicePort, error) { if svc == nil { return nil, errors.New("service is not defined") } - if port == 0 { + if (port.Type == intstr.Int && port.IntVal == 0) || (port.Type == intstr.String && port.StrVal == "") { return nil, errors.New("ingressRoute service port not defined") } hasValidPort := false for _, p := range svc.Spec.Ports { - if p.Port == port { + if (port.Type == intstr.Int && port.IntVal == p.Port) || (port.Type == intstr.String && port.StrVal == p.Name) { return &p, nil } @@ -343,8 +344,8 @@ func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error } } - if svc.Spec.Type != corev1.ServiceTypeExternalName { - return nil, fmt.Errorf("service port not found: %d", port) + if svc.Spec.Type != corev1.ServiceTypeExternalName || port.Type == intstr.String { + return nil, fmt.Errorf("service port not found: %s", &port) } if hasValidPort { @@ -352,7 +353,7 @@ func getServicePort(svc *corev1.Service, port int32) (*corev1.ServicePort, error 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 + return &corev1.ServicePort{Port: port.IntVal}, nil } func (p *Provider) createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 57a16cab5..79f848035 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -14,6 +14,7 @@ import ( "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -399,7 +400,7 @@ func (c configBuilder) nameAndService(ctx context.Context, parentNamespace strin return fullName, serversLB, nil case service.Kind == "TraefikService": - return fullServiceName(svcCtx, namespace, service, 0), nil, nil + return fullServiceName(svcCtx, namespace, service, intstr.FromInt(0)), nil, nil default: return "", nil, fmt.Errorf("unsupported service kind %s", service.Kind) } @@ -414,9 +415,9 @@ func splitSvcNameProvider(name string) (string, string) { return svc, pvd } -func fullServiceName(ctx context.Context, namespace string, service v1alpha1.LoadBalancerSpec, port int32) string { - if port != 0 { - return provider.Normalize(fmt.Sprintf("%s-%s-%d", namespace, service.Name, port)) +func fullServiceName(ctx context.Context, namespace string, service v1alpha1.LoadBalancerSpec, port intstr.IntOrString) string { + if (port.Type == intstr.Int && port.IntVal != 0) || (port.Type == intstr.String && port.StrVal != "") { + return provider.Normalize(fmt.Sprintf("%s-%s-%s", namespace, service.Name, &port)) } if !strings.Contains(service.Name, providerNamespaceSeparator) { diff --git a/pkg/provider/kubernetes/crd/kubernetes_tcp.go b/pkg/provider/kubernetes/crd/kubernetes_tcp.go index fe84dffbe..115522032 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_tcp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_tcp.go @@ -71,7 +71,7 @@ func (p *Provider) loadIngressRouteTCPConfiguration(ctx context.Context, client break } - serviceKey := fmt.Sprintf("%s-%s-%d", serviceName, service.Name, service.Port) + serviceKey := fmt.Sprintf("%s-%s-%s", serviceName, service.Name, &service.Port) conf.Services[serviceKey] = balancerServerTCP srv := dynamic.TCPWRRService{Name: serviceKey} diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 43e612dc8..2c2e84e6d 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -18,6 +18,7 @@ import ( "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" kubefake "k8s.io/client-go/kubernetes/fake" ) @@ -3738,7 +3739,7 @@ func TestGetServicePort(t *testing.T) { testCases := []struct { desc string svc *corev1.Service - port int32 + port intstr.IntOrString expected *corev1.ServicePort expectError bool }{ @@ -3757,7 +3758,7 @@ func TestGetServicePort(t *testing.T) { }, }, }, - port: 80, + port: intstr.FromInt(80), expected: &corev1.ServicePort{ Port: 80, }, @@ -3785,12 +3786,57 @@ func TestGetServicePort(t *testing.T) { }, expectError: true, }, + { + desc: "Matching named port", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + }, + port: intstr.FromString("http"), + expected: &corev1.ServicePort{ + Name: "http", + Port: 80, + }, + }, + { + desc: "Matching named port (with external name)", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + }, + port: intstr.FromString("http"), + expected: &corev1.ServicePort{ + Name: "http", + Port: 80, + }, + }, { desc: "Mismatching, only port(Ingress) defined", svc: &corev1.Service{ Spec: corev1.ServiceSpec{}, }, - port: 80, + port: intstr.FromInt(80), + expectError: true, + }, + { + desc: "Mismatching, only named port(Ingress) defined", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{}, + }, + port: intstr.FromString("http"), expectError: true, }, { @@ -3800,11 +3846,21 @@ func TestGetServicePort(t *testing.T) { Type: corev1.ServiceTypeExternalName, }, }, - port: 80, + port: intstr.FromInt(80), expected: &corev1.ServicePort{ Port: 80, }, }, + { + desc: "Mismatching, only named port(Ingress) defined with external name", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + }, + }, + port: intstr.FromString("http"), + expectError: true, + }, { desc: "Mismatching, only Service port defined", svc: &corev1.Service{ @@ -3843,7 +3899,22 @@ func TestGetServicePort(t *testing.T) { }, }, }, - port: 443, + port: intstr.FromInt(443), + expectError: true, + }, + { + desc: "Two different named ports defined", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "foo", + Port: 80, + }, + }, + }, + }, + port: intstr.FromString("bar"), expectError: true, }, { @@ -3858,11 +3929,27 @@ func TestGetServicePort(t *testing.T) { }, }, }, - port: 443, + port: intstr.FromInt(443), expected: &corev1.ServicePort{ Port: 443, }, }, + { + desc: "Two different named ports defined (with external name)", + svc: &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + Ports: []corev1.ServicePort{ + { + Name: "foo", + Port: 80, + }, + }, + }, + }, + port: intstr.FromString("bar"), + expectError: true, + }, } for _, test := range testCases { test := test diff --git a/pkg/provider/kubernetes/crd/kubernetes_udp.go b/pkg/provider/kubernetes/crd/kubernetes_udp.go index 0346084a2..35825f741 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_udp.go +++ b/pkg/provider/kubernetes/crd/kubernetes_udp.go @@ -11,6 +11,7 @@ import ( "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefik/v1alpha1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client Client) *dynamic.UDPConfiguration { @@ -52,7 +53,7 @@ func (p *Provider) loadIngressRouteUDPConfiguration(ctx context.Context, client break } - serviceKey := fmt.Sprintf("%s-%s-%d", serviceName, service.Name, service.Port) + serviceKey := fmt.Sprintf("%s-%s-%s", serviceName, service.Name, &service.Port) conf.Services[serviceKey] = balancerServerUDP srv := dynamic.UDPWRRService{Name: serviceKey} @@ -114,7 +115,7 @@ func loadUDPServers(client Client, namespace string, svc v1alpha1.ServiceUDP) ([ var portSpec *corev1.ServicePort for _, p := range service.Spec.Ports { p := p - if svc.Port == p.Port { + if (svc.Port.Type == intstr.Int && svc.Port.IntVal == p.Port) || (svc.Port.Type == intstr.String && svc.Port.StrVal == p.Name) { portSpec = &p break } diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go index 670c295e6..b48bd747b 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroute.go @@ -4,6 +4,7 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) // IngressRouteSpec is a specification for a IngressRouteSpec resource. @@ -67,7 +68,7 @@ type LoadBalancerSpec struct { // Port and all the fields below are related to a servers load-balancer, // and therefore should only be specified when Name references a Kubernetes Service. - Port int32 `json:"port"` + Port intstr.IntOrString `json:"port"` Scheme string `json:"scheme,omitempty"` Strategy string `json:"strategy,omitempty"` PassHostHeader *bool `json:"passHostHeader,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go index ab38c16ea..5c11256fd 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressroutetcp.go @@ -4,6 +4,7 @@ import ( "github.com/traefik/traefik/v2/pkg/config/dynamic" "github.com/traefik/traefik/v2/pkg/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) // IngressRouteTCPSpec is a specification for a IngressRouteTCPSpec resource. @@ -56,7 +57,7 @@ type TLSStoreTCPRef struct { type ServiceTCP struct { Name string `json:"name"` Namespace string `json:"namespace"` - Port int32 `json:"port"` + Port intstr.IntOrString `json:"port"` Weight *int `json:"weight,omitempty"` TerminationDelay *int `json:"terminationDelay,omitempty"` ProxyProtocol *dynamic.ProxyProtocol `json:"proxyProtocol,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressrouteudp.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressrouteudp.go index 509de5b3c..be6140e13 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressrouteudp.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/ingressrouteudp.go @@ -2,6 +2,7 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) // IngressRouteUDPSpec is a specification for a IngressRouteUDPSpec resource. @@ -23,10 +24,10 @@ type TLSOptionUDPRef struct { // ServiceUDP defines an upstream to proxy traffic. type ServiceUDP struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Port int32 `json:"port"` - Weight *int `json:"weight,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Port intstr.IntOrString `json:"port"` + Weight *int `json:"weight,omitempty"` } // +genclient diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 987fffe09..2e6856095 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -507,6 +507,7 @@ func (in *LoadBalancerSpec) DeepCopyInto(out *LoadBalancerSpec) { *out = new(dynamic.Sticky) (*in).DeepCopyInto(*out) } + out.Port = in.Port if in.PassHostHeader != nil { in, out := &in.PassHostHeader, &out.PassHostHeader *out = new(bool) @@ -1001,6 +1002,7 @@ func (in *ServiceSpec) DeepCopy() *ServiceSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTCP) DeepCopyInto(out *ServiceTCP) { *out = *in + out.Port = in.Port if in.Weight != nil { in, out := &in.Weight, &out.Weight *out = new(int) @@ -1032,6 +1034,7 @@ func (in *ServiceTCP) DeepCopy() *ServiceTCP { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceUDP) DeepCopyInto(out *ServiceUDP) { *out = *in + out.Port = in.Port if in.Weight != nil { in, out := &in.Weight, &out.Weight *out = new(int)