From 29908098e47f0458b5d5f50a59fe3583f63874c7 Mon Sep 17 00:00:00 2001 From: Manuel Zapf Date: Mon, 15 Mar 2021 11:16:04 +0100 Subject: [PATCH] Upgrade Ingress Handling to work with networkingv1/Ingress --- docs/content/migration/v2.md | 10 + docs/content/providers/kubernetes-ingress.md | 69 ++++- .../routing/providers/kubernetes-ingress.md | 129 ++++++++- .../ingress/builder_ingress_test.go | 38 ++- pkg/provider/kubernetes/ingress/client.go | 197 +++++++++---- .../kubernetes/ingress/client_mock_test.go | 15 +- .../kubernetes/ingress/client_test.go | 78 +++++ ...9-Ingress-with-defaultbackend_endpoint.yml | 24 ++ ...19-Ingress-with-defaultbackend_ingress.yml | 12 + ...19-Ingress-with-defaultbackend_service.yml | 22 ++ ...9-Ingress-with-empty-pathType_endpoint.yml | 11 + ...19-Ingress-with-empty-pathType_ingress.yml | 18 ++ ...19-Ingress-with-empty-pathType_service.yml | 10 + ...9-Ingress-with-exact-pathType_endpoint.yml | 11 + ...19-Ingress-with-exact-pathType_ingress.yml | 16 ++ ...19-Ingress-with-exact-pathType_service.yml | 10 + ...plementationSpecific-pathType_endpoint.yml | 11 + ...mplementationSpecific-pathType_ingress.yml | 18 ++ ...mplementationSpecific-pathType_service.yml | 10 + ...gress-with-ingress-annotation_endpoint.yml | 11 + ...ngress-with-ingress-annotation_ingress.yml | 17 ++ ...ngress-with-ingress-annotation_service.yml | 10 + ...v19-Ingress-with-ingressClass_endpoint.yml | 11 + .../v19-Ingress-with-ingressClass_ingress.yml | 16 ++ ...Ingress-with-ingressClass_ingressclass.yml | 6 + .../v19-Ingress-with-ingressClass_service.yml | 10 + ...ess-with-missing-ingressClass_endpoint.yml | 11 + ...ress-with-missing-ingressClass_ingress.yml | 16 ++ ...ress-with-missing-ingressClass_service.yml | 10 + .../v19-Ingress-with-named-port_endpoint.yml | 12 + .../v19-Ingress-with-named-port_ingress.yml | 16 ++ .../v19-Ingress-with-named-port_service.yml | 12 + .../v19-Ingress-with-no-pathType_endpoint.yml | 11 + .../v19-Ingress-with-no-pathType_ingress.yml | 17 ++ .../v19-Ingress-with-no-pathType_service.yml | 10 + ...-Ingress-with-prefix-pathType_endpoint.yml | 11 + ...9-Ingress-with-prefix-pathType_ingress.yml | 16 ++ ...9-Ingress-with-prefix-pathType_service.yml | 10 + pkg/provider/kubernetes/ingress/kubernetes.go | 43 +-- .../kubernetes/ingress/kubernetes_test.go | 269 +++++++++++++++++- 40 files changed, 1141 insertions(+), 113 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingressclass.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_service.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_endpoint.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_ingress.yml create mode 100644 pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_service.yml diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index fc99f99f3..e88448e32 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -348,3 +348,13 @@ After deploying the new [Traefik CRDs](../reference/dynamic-configuration/kubern Please note that the unknown fields will not be pruned when migrating from `apiextensions.k8s.io/v1beta1` to `apiextensions.k8s.io/v1` CRDs. For more details check out the official [documentation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema). + +### Kubernetes Ingress + +Traefik v2.5 moves forward for the Ingress provider to support Kubernetes v1.22. + +Traefik now supports only v1.14+ Kubernetes clusters, which means the support of `extensions/v1beta1` API Version ingresses has been dropped. + +The `extensions/v1beta1` API Version should now be replaced either by `networking.k8s.io/v1beta1` or by `networking.k8s.io/v1` (as of Kubernetes v1.19+). + +The support of the `networking.k8s.io/v1beta1` API Version will stop in Kubernetes v1.22. diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index 56243c418..ccbde7969 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -6,6 +6,10 @@ The Kubernetes Ingress Controller. The Traefik Kubernetes Ingress provider is a Kubernetes Ingress controller; that is to say, it manages access to cluster services by supporting the [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) specification. +## Requirements + +Traefik supports `1.14+` Kubernetes clusters. + ## Routing Configuration See the dedicated section in [routing](../routing/providers/kubernetes-ingress.md). @@ -31,9 +35,9 @@ The provider then watches for incoming ingresses events, such as the example bel and derives the corresponding dynamic configuration from it, which in turn creates the resulting routers, services, handlers, etc. -```yaml tab="File (YAML)" +```yaml tab="Ingress" kind: Ingress -apiVersion: extensions/v1beta1 +apiVersion: networking.k8s.io/v1beta1 metadata: name: "foo" namespace: production @@ -53,6 +57,32 @@ spec: servicePort: 80 ``` +```yaml tab="Ingress Kubernetes v1.19+" +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "foo" + namespace: production + +spec: + rules: + - host: example.net + http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 + - path: /foo + backend: + service: + name: service1 + port: + number: 80 +``` + ## LetsEncrypt Support with the Ingress Provider By design, Traefik is a stateless application, @@ -220,7 +250,7 @@ Value of `kubernetes.io/ingress.class` annotation that identifies Ingress object If the parameter is set, only Ingresses containing an annotation with the same value are processed. Otherwise, Ingresses missing the annotation, having an empty value, or the value `traefik` are processed. -!!! info "Kubernetes 1.18+" +??? info "Kubernetes 1.18+" If the Kubernetes cluster version is 1.18+, the new `IngressClass` resource can be leveraged to identify Ingress objects that should be processed. @@ -256,6 +286,39 @@ Otherwise, Ingresses missing the annotation, having an empty value, or the value servicePort: 80 ``` +??? info "Kubernetes 1.19+" + + If the Kubernetes cluster version is 1.19+, + prefer using the `networking.k8s.io/v1` [apiVersion](https://v1-19.docs.kubernetes.io/docs/setup/release/notes/#api-change) of `Ingress` and `IngressClass`. + + ```yaml tab="IngressClass" + apiVersion: networking.k8s.io/v1 + kind: IngressClass + metadata: + name: traefik-lb + spec: + controller: traefik.io/ingress-controller + ``` + + ```yaml tab="Ingress" + apiVersion: "networking.k8s.io/v1" + kind: "Ingress" + metadata: + name: "example-ingress" + spec: + ingressClassName: "traefik-lb" + rules: + - host: "*.example.com" + http: + paths: + - path: "/example" + backend: + service: + name: "example-service" + port: + number: 80 + ``` + ```toml tab="File (TOML)" [providers.kubernetesIngress] ingressClass = "traefik-internal" diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index ff4542122..36f6d7dbf 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -85,6 +85,33 @@ which in turn will create the resulting routers, services, handlers, etc. servicePort: 80 ``` + ```yaml tab="Ingress Kubernetes v1.19+" + kind: Ingress + apiVersion: networking.k8s.io/v1 + metadata: + name: myingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + + spec: + rules: + - host: example.com + http: + paths: + - path: /bar + backend: + service: + name: whoami + port: + number: 80 + - path: /foo + backend: + service: + name: whoami + port: + number: 80 + ``` + ```yaml tab="Traefik" apiVersion: v1 kind: ServiceAccount @@ -434,6 +461,33 @@ This way, any Ingress attached to this Entrypoint will have TLS termination by d servicePort: 80 ``` + ```yaml tab="Ingress Kubernetes v1.19+" + kind: Ingress + apiVersion: networking.k8s.io/v1 + metadata: + name: myingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + + spec: + rules: + - host: example.com + http: + paths: + - path: /bar + backend: + service: + name: whoami + port: + number: 80 + - path: /foo + backend: + service: + name: whoami + port: + number: 80 + ``` + ```yaml tab="Traefik" apiVersion: v1 kind: ServiceAccount @@ -613,6 +667,34 @@ For more options, please refer to the available [annotations](#on-ingress). servicePort: 80 ``` + ```yaml tab="Ingress Kubernetes v1.19+" + kind: Ingress + apiVersion: networking.k8s.io/v1 + metadata: + name: myingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: true + + spec: + rules: + - host: example.com + http: + paths: + - path: /bar + backend: + service: + name: whoami + port: + number: 80 + - path: /foo + backend: + service: + name: whoami + port: + number: 80 + ``` + ```yaml tab="Traefik" apiVersion: v1 kind: ServiceAccount @@ -732,6 +814,31 @@ For more options, please refer to the available [annotations](#on-ingress). tls: - secretName: supersecret ``` + + ```yaml tab="Ingress Kubernetes v1.19+" + kind: Ingress + apiVersion: networking.k8s.io/v1 + metadata: + name: foo + namespace: production + + spec: + rules: + - host: example.net + http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 + # Only selects which certificate(s) should be loaded from the secret, in order to terminate TLS. + # Doesn't enable TLS for that ingress (hence for the underlying router). + # Please see the TLS annotations on ingress made for that purpose. + tls: + - secretName: supersecret + ``` ```yaml tab="Secret" apiVersion: v1 @@ -777,16 +884,30 @@ and will connect via TLS automatically. Ingresses can be created that look like the following: -```yaml +```yaml tab="Ingress" apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: cheese spec: - backend: - serviceName: stilton - servicePort: 80 + defaultBackend: + serviceName: stilton + serverPort: 80 +``` + +```yaml tab="Ingress Kubernetes v1.19+" +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cheese + +spec: + defaultBackend: + service: + name: stilton + port: + number: 80 ``` This ingress follows the Global Default Backend property of ingresses. diff --git a/pkg/provider/kubernetes/ingress/builder_ingress_test.go b/pkg/provider/kubernetes/ingress/builder_ingress_test.go index f08f325ac..56ba2d286 100644 --- a/pkg/provider/kubernetes/ingress/builder_ingress_test.go +++ b/pkg/provider/kubernetes/ingress/builder_ingress_test.go @@ -1,11 +1,9 @@ package ingress -import ( - "k8s.io/api/networking/v1beta1" -) +import networkingv1 "k8s.io/api/networking/v1" -func buildIngress(opts ...func(*v1beta1.Ingress)) *v1beta1.Ingress { - i := &v1beta1.Ingress{} +func buildIngress(opts ...func(*networkingv1.Ingress)) *networkingv1.Ingress { + i := &networkingv1.Ingress{} i.Kind = "Ingress" for _, opt := range opts { opt(i) @@ -13,15 +11,15 @@ func buildIngress(opts ...func(*v1beta1.Ingress)) *v1beta1.Ingress { return i } -func iNamespace(value string) func(*v1beta1.Ingress) { - return func(i *v1beta1.Ingress) { +func iNamespace(value string) func(*networkingv1.Ingress) { + return func(i *networkingv1.Ingress) { i.Namespace = value } } -func iRules(opts ...func(*v1beta1.IngressSpec)) func(*v1beta1.Ingress) { - return func(i *v1beta1.Ingress) { - s := &v1beta1.IngressSpec{} +func iRules(opts ...func(*networkingv1.IngressSpec)) func(*networkingv1.Ingress) { + return func(i *networkingv1.Ingress) { + s := &networkingv1.IngressSpec{} for _, opt := range opts { opt(s) } @@ -29,9 +27,9 @@ func iRules(opts ...func(*v1beta1.IngressSpec)) func(*v1beta1.Ingress) { } } -func iRule(opts ...func(*v1beta1.IngressRule)) func(*v1beta1.IngressSpec) { - return func(spec *v1beta1.IngressSpec) { - r := &v1beta1.IngressRule{} +func iRule(opts ...func(*networkingv1.IngressRule)) func(*networkingv1.IngressSpec) { + return func(spec *networkingv1.IngressSpec) { + r := &networkingv1.IngressRule{} for _, opt := range opts { opt(r) } @@ -39,24 +37,24 @@ func iRule(opts ...func(*v1beta1.IngressRule)) func(*v1beta1.IngressSpec) { } } -func iHost(name string) func(*v1beta1.IngressRule) { - return func(rule *v1beta1.IngressRule) { +func iHost(name string) func(*networkingv1.IngressRule) { + return func(rule *networkingv1.IngressRule) { rule.Host = name } } -func iTLSes(opts ...func(*v1beta1.IngressTLS)) func(*v1beta1.Ingress) { - return func(i *v1beta1.Ingress) { +func iTLSes(opts ...func(*networkingv1.IngressTLS)) func(*networkingv1.Ingress) { + return func(i *networkingv1.Ingress) { for _, opt := range opts { - iTLS := v1beta1.IngressTLS{} + iTLS := networkingv1.IngressTLS{} opt(&iTLS) i.Spec.TLS = append(i.Spec.TLS, iTLS) } } } -func iTLS(secret string, hosts ...string) func(*v1beta1.IngressTLS) { - return func(i *v1beta1.IngressTLS) { +func iTLS(secret string, hosts ...string) func(*networkingv1.IngressTLS) { + return func(i *networkingv1.IngressTLS) { i.SecretName = secret i.Hosts = hosts } diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go index 743f3f621..87351953a 100644 --- a/pkg/provider/kubernetes/ingress/client.go +++ b/pkg/provider/kubernetes/ingress/client.go @@ -13,11 +13,12 @@ import ( "github.com/traefik/traefik/v2/pkg/log" traefikversion "github.com/traefik/traefik/v2/pkg/version" corev1 "k8s.io/api/core/v1" - extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" kubeerror "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -54,12 +55,12 @@ func (reh *resourceEventHandler) OnDelete(obj interface{}) { // The stores can then be accessed via the Get* functions. type Client interface { WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) - GetIngresses() []*networkingv1beta1.Ingress + GetIngresses() []*networkingv1.Ingress GetIngressClasses() ([]*networkingv1beta1.IngressClass, error) GetService(namespace, name string) (*corev1.Service, bool, error) GetSecret(namespace, name string) (*corev1.Secret, bool, error) GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) - UpdateIngressStatus(ing *networkingv1beta1.Ingress, ingStatus []corev1.LoadBalancerIngress) error + UpdateIngressStatus(ing *networkingv1.Ingress, ingStatus []corev1.LoadBalancerIngress) error GetServerVersion() (*version.Version, error) } @@ -167,9 +168,20 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< opts.LabelSelector = c.ingressLabelSelector } + serverVersion, err := c.GetServerVersion() + if err != nil { + return nil, fmt.Errorf("failed to get server version: %w", err) + } + for _, ns := range namespaces { factoryIngress := informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, informers.WithNamespace(ns), informers.WithTweakListOptions(matchesLabelSelector)) - factoryIngress.Extensions().V1beta1().Ingresses().Informer().AddEventHandler(eventHandler) + + if supportsNetworkingV1Ingress(serverVersion) { + factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler) + } else { + factoryIngress.Networking().V1beta1().Ingresses().Informer().AddEventHandler(eventHandler) + } + c.factoriesIngress[ns] = factoryIngress factoryKube := informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, informers.WithNamespace(ns)) @@ -208,12 +220,6 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< } } - serverVersion, err := c.GetServerVersion() - if err != nil { - log.WithoutContext().Errorf("Failed to get server version: %v", err) - return eventCh, nil - } - if supportsIngressClass(serverVersion) { c.clusterFactory = informers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod) c.clusterFactory.Networking().V1beta1().IngressClasses().Informer().AddEventHandler(eventHandler) @@ -230,42 +236,59 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (< } // GetIngresses returns all Ingresses for observed namespaces in the cluster. -func (c *clientWrapper) GetIngresses() []*networkingv1beta1.Ingress { - var results []*networkingv1beta1.Ingress +func (c *clientWrapper) GetIngresses() []*networkingv1.Ingress { + var results []*networkingv1.Ingress + + serverVersion, err := c.GetServerVersion() + if err != nil { + log.Errorf("Failed to get server version: %v", err) + return results + } + + isNetworkingV1Supported := supportsNetworkingV1Ingress(serverVersion) for ns, factory := range c.factoriesIngress { - // extensions - ings, err := factory.Extensions().V1beta1().Ingresses().Lister().List(labels.Everything()) - if err != nil { - log.Errorf("Failed to list ingresses in namespace %s: %v", ns, err) - } - - for _, ing := range ings { - n, err := extensionsToNetworking(ing) + if isNetworkingV1Supported { + // networking + listNew, err := factory.Networking().V1().Ingresses().Lister().List(labels.Everything()) if err != nil { - log.Errorf("Failed to convert ingress %s from extensions/v1beta1 to networking/v1beta1: %v", ns, err) + log.WithoutContext().Errorf("Failed to list ingresses in namespace %s: %v", ns, err) continue } - results = append(results, n) + + results = append(results, listNew...) + continue } - // networking + // networking beta list, err := factory.Networking().V1beta1().Ingresses().Lister().List(labels.Everything()) if err != nil { - log.Errorf("Failed to list ingresses in namespace %s: %v", ns, err) + log.WithoutContext().Errorf("Failed to list ingresses in namespace %s: %v", ns, err) + continue + } + + for _, ing := range list { + n, err := toNetworkingV1(ing) + if err != nil { + log.WithoutContext().Errorf("Failed to convert ingress %s from networking/v1beta1 to networking/v1: %v", ns, err) + continue + } + + addServiceFromV1Beta1(n, *ing) + + results = append(results, n) } - results = append(results, list...) } return results } -func extensionsToNetworking(ing marshaler) (*networkingv1beta1.Ingress, error) { +func toNetworkingV1(ing marshaler) (*networkingv1.Ingress, error) { data, err := ing.Marshal() if err != nil { return nil, err } - ni := &networkingv1beta1.Ingress{} + ni := &networkingv1.Ingress{} err = ni.Unmarshal(data) if err != nil { return nil, err @@ -274,16 +297,95 @@ func extensionsToNetworking(ing marshaler) (*networkingv1beta1.Ingress, error) { return ni, nil } +func addServiceFromV1Beta1(ing *networkingv1.Ingress, old networkingv1beta1.Ingress) { + if old.Spec.Backend != nil { + port := networkingv1.ServiceBackendPort{} + if old.Spec.Backend.ServicePort.Type == intstr.Int { + port.Number = old.Spec.Backend.ServicePort.IntVal + } else { + port.Name = old.Spec.Backend.ServicePort.StrVal + } + + if old.Spec.Backend.ServiceName != "" { + ing.Spec.DefaultBackend = &networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: old.Spec.Backend.ServiceName, + Port: port, + }, + } + } + } + + for rc, rule := range ing.Spec.Rules { + if rule.HTTP == nil { + continue + } + for pc, path := range rule.HTTP.Paths { + if path.Backend.Service == nil { + oldBackend := old.Spec.Rules[rc].HTTP.Paths[pc].Backend + + port := networkingv1.ServiceBackendPort{} + if oldBackend.ServicePort.Type == intstr.Int { + port.Number = oldBackend.ServicePort.IntVal + } else { + port.Name = oldBackend.ServicePort.StrVal + } + + svc := networkingv1.IngressServiceBackend{ + Name: oldBackend.ServiceName, + Port: port, + } + + ing.Spec.Rules[rc].HTTP.Paths[pc].Backend.Service = &svc + } + } + } +} + // UpdateIngressStatus updates an Ingress with a provided status. -func (c *clientWrapper) UpdateIngressStatus(src *networkingv1beta1.Ingress, ingStatus []corev1.LoadBalancerIngress) error { +func (c *clientWrapper) UpdateIngressStatus(src *networkingv1.Ingress, ingStatus []corev1.LoadBalancerIngress) error { if !c.isWatchedNamespace(src.Namespace) { return fmt.Errorf("failed to get ingress %s/%s: namespace is not within watched namespaces", src.Namespace, src.Name) } - if src.GetObjectKind().GroupVersionKind().Group != "networking.k8s.io" { + serverVersion, err := c.GetServerVersion() + if err != nil { + log.WithoutContext().Errorf("Failed to get server version: %v", err) + return err + } + + if !supportsNetworkingV1Ingress(serverVersion) { return c.updateIngressStatusOld(src, ingStatus) } + ing, err := c.factoriesIngress[c.lookupNamespace(src.Namespace)].Networking().V1().Ingresses().Lister().Ingresses(src.Namespace).Get(src.Name) + if err != nil { + return fmt.Errorf("failed to get ingress %s/%s: %w", src.Namespace, src.Name, err) + } + + logger := log.WithoutContext().WithField("namespace", ing.Namespace).WithField("ingress", ing.Name) + + if isLoadBalancerIngressEquals(ing.Status.LoadBalancer.Ingress, ingStatus) { + logger.Debug("Skipping ingress status update") + return nil + } + + ingCopy := ing.DeepCopy() + ingCopy.Status = networkingv1.IngressStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: ingStatus}} + + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + _, err = c.clientset.NetworkingV1().Ingresses(ingCopy.Namespace).UpdateStatus(ctx, ingCopy, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update ingress status %s/%s: %w", src.Namespace, src.Name, err) + } + + logger.Info("Updated ingress status") + return nil +} + +func (c *clientWrapper) updateIngressStatusOld(src *networkingv1.Ingress, ingStatus []corev1.LoadBalancerIngress) error { ing, err := c.factoriesIngress[c.lookupNamespace(src.Namespace)].Networking().V1beta1().Ingresses().Lister().Ingresses(src.Namespace).Get(src.Name) if err != nil { return fmt.Errorf("failed to get ingress %s/%s: %w", src.Namespace, src.Name, err) @@ -306,35 +408,6 @@ func (c *clientWrapper) UpdateIngressStatus(src *networkingv1beta1.Ingress, ingS if err != nil { return fmt.Errorf("failed to update ingress status %s/%s: %w", src.Namespace, src.Name, err) } - - logger.Info("Updated ingress status") - return nil -} - -func (c *clientWrapper) updateIngressStatusOld(src *networkingv1beta1.Ingress, ingStatus []corev1.LoadBalancerIngress) error { - ing, err := c.factoriesIngress[c.lookupNamespace(src.Namespace)].Extensions().V1beta1().Ingresses().Lister().Ingresses(src.Namespace).Get(src.Name) - if err != nil { - return fmt.Errorf("failed to get ingress %s/%s: %w", src.Namespace, src.Name, err) - } - - logger := log.WithoutContext().WithField("namespace", ing.Namespace).WithField("ingress", ing.Name) - - if isLoadBalancerIngressEquals(ing.Status.LoadBalancer.Ingress, ingStatus) { - logger.Debug("Skipping ingress status update") - return nil - } - - ingCopy := ing.DeepCopy() - ingCopy.Status = extensionsv1beta1.IngressStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: ingStatus}} - - ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) - defer cancel() - - _, err = c.clientset.ExtensionsV1beta1().Ingresses(ingCopy.Namespace).UpdateStatus(ctx, ingCopy, metav1.UpdateOptions{}) - if err != nil { - return fmt.Errorf("failed to update ingress status %s/%s: %w", src.Namespace, src.Name, err) - } - logger.Info("Updated ingress status") return nil } @@ -488,3 +561,11 @@ func filterIngressClassByName(ingressClassName string, ics []*networkingv1beta1. return ingressClasses } + +// Ingress in networking.k8s.io/v1 is supported starting 1.19. +// thus, we query it in K8s starting 1.19. +func supportsNetworkingV1Ingress(serverVersion *version.Version) bool { + ingressNetworkingVersion := version.Must(version.NewVersion("1.19")) + + return serverVersion.GreaterThanOrEqual(ingressNetworkingVersion) +} diff --git a/pkg/provider/kubernetes/ingress/client_mock_test.go b/pkg/provider/kubernetes/ingress/client_mock_test.go index 94a282990..493bdf5ec 100644 --- a/pkg/provider/kubernetes/ingress/client_mock_test.go +++ b/pkg/provider/kubernetes/ingress/client_mock_test.go @@ -7,14 +7,14 @@ import ( "github.com/hashicorp/go-version" "github.com/traefik/traefik/v2/pkg/provider/kubernetes/k8s" corev1 "k8s.io/api/core/v1" - extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" ) var _ Client = (*clientMock)(nil) type clientMock struct { - ingresses []*networkingv1beta1.Ingress + ingresses []*networkingv1.Ingress services []*corev1.Service secrets []*corev1.Secret endpoints []*corev1.Endpoints @@ -51,13 +51,14 @@ func newClientMock(serverVersion string, paths ...string) clientMock { case *corev1.Endpoints: c.endpoints = append(c.endpoints, o) case *networkingv1beta1.Ingress: - c.ingresses = append(c.ingresses, o) - case *extensionsv1beta1.Ingress: - ing, err := extensionsToNetworking(o) + ing, err := toNetworkingV1(o) if err != nil { panic(err) } + addServiceFromV1Beta1(ing, *o) c.ingresses = append(c.ingresses, ing) + case *networkingv1.Ingress: + c.ingresses = append(c.ingresses, o) case *networkingv1beta1.IngressClass: c.ingressClasses = append(c.ingressClasses, o) default: @@ -69,7 +70,7 @@ func newClientMock(serverVersion string, paths ...string) clientMock { return c } -func (c clientMock) GetIngresses() []*networkingv1beta1.Ingress { +func (c clientMock) GetIngresses() []*networkingv1.Ingress { return c.ingresses } @@ -125,6 +126,6 @@ func (c clientMock) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-cha return c.watchChan, nil } -func (c clientMock) UpdateIngressStatus(_ *networkingv1beta1.Ingress, _ []corev1.LoadBalancerIngress) error { +func (c clientMock) UpdateIngressStatus(_ *networkingv1.Ingress, _ []corev1.LoadBalancerIngress) error { return c.apiIngressStatusError } diff --git a/pkg/provider/kubernetes/ingress/client_test.go b/pkg/provider/kubernetes/ingress/client_test.go index d65484219..9e32a618b 100644 --- a/pkg/provider/kubernetes/ingress/client_test.go +++ b/pkg/provider/kubernetes/ingress/client_test.go @@ -8,9 +8,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "k8s.io/api/networking/v1beta1" kubeerror "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" + fakediscovery "k8s.io/client-go/discovery/fake" kubefake "k8s.io/client-go/kubernetes/fake" ) @@ -149,6 +153,11 @@ func TestClientIgnoresHelmOwnedSecrets(t *testing.T) { kubeClient := kubefake.NewSimpleClientset(helmSecret, secret) + discovery, _ := kubeClient.Discovery().(*fakediscovery.FakeDiscovery) + discovery.FakedServerVersion = &version.Info{ + GitVersion: "v1.19", + } + client := newClientImpl(kubeClient) stopCh := make(chan struct{}) @@ -180,3 +189,72 @@ func TestClientIgnoresHelmOwnedSecrets(t *testing.T) { require.NoError(t, err) assert.False(t, found) } + +func TestClientUsesCorrectServerVersion(t *testing.T) { + ingressV1Beta := &v1beta1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "ingress-v1beta", + }, + } + + ingressV1 := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "ingress-v1", + }, + } + + kubeClient := kubefake.NewSimpleClientset(ingressV1Beta, ingressV1) + + discovery, _ := kubeClient.Discovery().(*fakediscovery.FakeDiscovery) + discovery.FakedServerVersion = &version.Info{ + GitVersion: "v1.18.12+foobar", + } + + stopCh := make(chan struct{}) + + client := newClientImpl(kubeClient) + + eventCh, err := client.WatchAll(nil, stopCh) + require.NoError(t, err) + + select { + case event := <-eventCh: + ingress, ok := event.(*v1beta1.Ingress) + require.True(t, ok) + + assert.Equal(t, "ingress-v1beta", ingress.Name) + case <-time.After(50 * time.Millisecond): + assert.Fail(t, "expected to receive event for ingress") + } + + select { + case <-eventCh: + assert.Fail(t, "received more than one event") + case <-time.After(50 * time.Millisecond): + } + + discovery.FakedServerVersion = &version.Info{ + GitVersion: "v1.19", + } + + eventCh, err = client.WatchAll(nil, stopCh) + require.NoError(t, err) + + select { + case event := <-eventCh: + ingress, ok := event.(*networkingv1.Ingress) + require.True(t, ok) + + assert.Equal(t, "ingress-v1", ingress.Name) + case <-time.After(50 * time.Millisecond): + assert.Fail(t, "expected to receive event for ingress") + } + + select { + case <-eventCh: + assert.Fail(t, "received more than one event") + case <-time.After(50 * time.Millisecond): + } +} diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_endpoint.yml new file mode 100644 index 000000000..0e64b4434 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_endpoint.yml @@ -0,0 +1,24 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 80 + +--- +kind: Endpoints +apiVersion: v1 +metadata: + name: defaultservice + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_ingress.yml new file mode 100644 index 000000000..58b7aac63 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_ingress.yml @@ -0,0 +1,12 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: defaultbackend + namespace: testing + +spec: + defaultBackend: + service: + name: defaultservice + port: + number: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_service.yml new file mode 100644 index 000000000..1bca9be2b --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-defaultbackend_service.yml @@ -0,0 +1,22 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 +--- + +kind: Service +apiVersion: v1 +metadata: + name: defaultservice + namespace: testing + +spec: + ports: + - port: 8080 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_ingress.yml new file mode 100644 index 000000000..2ea3bf486 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_ingress.yml @@ -0,0 +1,18 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: "" + backend: + service: + name: service1 + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-empty-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_ingress.yml new file mode 100644 index 000000000..ca25ad48e --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Exact + backend: + service: + name: service1 + port: + number: 80 \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-exact-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_ingress.yml new file mode 100644 index 000000000..9804e7eba --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_ingress.yml @@ -0,0 +1,18 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + pathType: ImplementationSpecific + backend: + service: + name: service1 + port: + number: 80 \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-implementationSpecific-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_ingress.yml new file mode 100644 index 000000000..1e8f60949 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_ingress.yml @@ -0,0 +1,17 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + kubernetes.io/ingress.class: traefik +spec: + rules: + - http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingress-annotation_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingress.yml new file mode 100644 index 000000000..22c268983 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing +spec: + ingressClassName: traefik-lb + rules: + - http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 \ No newline at end of file diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingressclass.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingressclass.yml new file mode 100644 index 000000000..b96f42518 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_ingressclass.yml @@ -0,0 +1,6 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: IngressClass +metadata: + name: traefik-lb +spec: + controller: traefik.io/ingress-controller diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-ingressClass_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_ingress.yml new file mode 100644 index 000000000..b14b1a4bb --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing +spec: + ingressClassName: traefik-lb + rules: + - http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-missing-ingressClass_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_endpoint.yml new file mode 100644 index 000000000..bf2e7526e --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_endpoint.yml @@ -0,0 +1,12 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: + - addresses: + - ip: 10.10.0.1 + ports: + - name: foobar + port: 4711 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_ingress.yml new file mode 100644 index 000000000..12f17bc54 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Prefix + backend: + service: + name: service1 + port: + name: foobar diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_service.yml new file mode 100644 index 000000000..c064e5872 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-named-port_service.yml @@ -0,0 +1,12 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - name: foobar + port: 4711 + clusterIP: 10.0.0.1 + diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_ingress.yml new file mode 100644 index 000000000..6327a89a5 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_ingress.yml @@ -0,0 +1,17 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing + annotations: + traefik.ingress.kubernetes.io/router.pathmatcher: Path +spec: + rules: + - http: + paths: + - path: /bar + backend: + service: + name: service1 + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-no-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_endpoint.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_endpoint.yml new file mode 100644 index 000000000..6ed60d79c --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_endpoint.yml @@ -0,0 +1,11 @@ +kind: Endpoints +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +subsets: +- addresses: + - ip: 10.10.0.1 + ports: + - port: 8080 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_ingress.yml new file mode 100644 index 000000000..bc88fc058 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_ingress.yml @@ -0,0 +1,16 @@ +kind: Ingress +apiVersion: networking.k8s.io/v1 +metadata: + name: "" + namespace: testing +spec: + rules: + - http: + paths: + - path: /bar + pathType: Prefix + backend: + service: + name: service1 + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_service.yml b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_service.yml new file mode 100644 index 000000000..0ec7e2269 --- /dev/null +++ b/pkg/provider/kubernetes/ingress/fixtures/v19-Ingress-with-prefix-pathType_service.yml @@ -0,0 +1,10 @@ +kind: Service +apiVersion: v1 +metadata: + name: service1 + namespace: testing + +spec: + ports: + - port: 80 + clusterIP: 10.0.0.1 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 5cb6da5ca..a1386a6ef 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -23,9 +23,9 @@ import ( "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/tls" corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" networkingv1beta1 "k8s.io/api/networking/v1beta1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/intstr" ) const ( @@ -226,17 +226,17 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl log.FromContext(ctx).Errorf("Error configuring TLS: %v", err) } - if len(ingress.Spec.Rules) == 0 && ingress.Spec.Backend != nil { + if len(ingress.Spec.Rules) == 0 && ingress.Spec.DefaultBackend != nil { if _, ok := conf.HTTP.Services["default-backend"]; ok { log.FromContext(ctx).Error("The default backend already exists.") continue } - service, err := loadService(client, ingress.Namespace, *ingress.Spec.Backend) + service, err := loadService(client, ingress.Namespace, *ingress.Spec.DefaultBackend) if err != nil { log.FromContext(ctx). - WithField("serviceName", ingress.Spec.Backend.ServiceName). - WithField("servicePort", ingress.Spec.Backend.ServicePort.String()). + WithField("serviceName", ingress.Spec.DefaultBackend.Service.Name). + WithField("servicePort", ingress.Spec.DefaultBackend.Service.Port.String()). Errorf("Cannot create service: %v", err) continue } @@ -272,13 +272,19 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl service, err := loadService(client, ingress.Namespace, pa.Backend) if err != nil { log.FromContext(ctx). - WithField("serviceName", pa.Backend.ServiceName). - WithField("servicePort", pa.Backend.ServicePort.String()). + WithField("serviceName", pa.Backend.Service.Name). + WithField("servicePort", pa.Backend.Service.Port.String()). Errorf("Cannot create service: %v", err) continue } - serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.ServiceName + "-" + pa.Backend.ServicePort.String()) + portString := pa.Backend.Service.Port.Name + + if len(pa.Backend.Service.Port.Name) == 0 { + portString = fmt.Sprint(pa.Backend.Service.Port.Number) + } + + serviceName := provider.Normalize(ingress.Namespace + "-" + pa.Backend.Service.Name + "-" + portString) conf.HTTP.Services[serviceName] = service routerKey := strings.TrimPrefix(provider.Normalize(ingress.Name+"-"+ingress.Namespace+"-"+rule.Host+pa.Path), "-") @@ -316,7 +322,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl return conf } -func (p *Provider) updateIngressStatus(ing *networkingv1beta1.Ingress, k8sClient Client) error { +func (p *Provider) updateIngressStatus(ing *networkingv1.Ingress, k8sClient Client) error { // Only process if an EndpointIngress has been configured. if p.IngressEndpoint == nil { return nil @@ -355,7 +361,7 @@ func (p *Provider) updateIngressStatus(ing *networkingv1beta1.Ingress, k8sClient return k8sClient.UpdateIngressStatus(ing, service.Status.LoadBalancer.Ingress) } -func (p *Provider) shouldProcessIngress(ingress *networkingv1beta1.Ingress, ingressClasses []*networkingv1beta1.IngressClass) bool { +func (p *Provider) shouldProcessIngress(ingress *networkingv1.Ingress, ingressClasses []*networkingv1beta1.IngressClass) bool { // configuration through the new kubernetes ingressClass if ingress.Spec.IngressClassName != nil { for _, ic := range ingressClasses { @@ -379,7 +385,7 @@ func buildHostRule(host string) string { return "Host(`" + host + "`)" } -func getCertificates(ctx context.Context, ingress *networkingv1beta1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { +func getCertificates(ctx context.Context, ingress *networkingv1.Ingress, k8sClient Client, tlsConfigs map[string]*tls.CertAndStores) error { for _, t := range ingress.Spec.TLS { if t.SecretName == "" { log.FromContext(ctx).Debugf("Skipping TLS sub-section: No secret name provided") @@ -464,8 +470,8 @@ func getTLSConfig(tlsConfigs map[string]*tls.CertAndStores) []*tls.CertAndStores return configs } -func loadService(client Client, namespace string, backend networkingv1beta1.IngressBackend) (*dynamic.Service, error) { - service, exists, err := client.GetService(namespace, backend.ServiceName) +func loadService(client Client, namespace string, backend networkingv1.IngressBackend) (*dynamic.Service, error) { + service, exists, err := client.GetService(namespace, backend.Service.Name) if err != nil { return nil, err } @@ -478,8 +484,7 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr var portSpec corev1.ServicePort var match bool for _, p := range service.Spec.Ports { - if (backend.ServicePort.Type == intstr.Int && backend.ServicePort.IntVal == p.Port) || - (backend.ServicePort.Type == intstr.String && backend.ServicePort.StrVal == p.Name) { + if backend.Service.Port.Number == p.Port || (backend.Service.Port.Name == p.Name && len(p.Name) > 0) { portName = p.Name portSpec = p match = true @@ -520,7 +525,7 @@ func loadService(client Client, namespace string, backend networkingv1beta1.Ingr return svc, nil } - endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.ServiceName) + endpoints, endpointsExists, endpointsErr := client.GetEndpoints(namespace, backend.Service.Name) if endpointsErr != nil { return nil, endpointsErr } @@ -584,7 +589,7 @@ func makeRouterKeyWithHash(key, rule string) (string, error) { return dupKey, nil } -func loadRouter(rule networkingv1beta1.IngressRule, pa networkingv1beta1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router { +func loadRouter(rule networkingv1.IngressRule, pa networkingv1.HTTPIngressPath, rtConfig *RouterConfig, serviceName string) *dynamic.Router { var rules []string if len(rule.Host) > 0 { rules = []string{buildHostRule(rule.Host)} @@ -593,11 +598,11 @@ func loadRouter(rule networkingv1beta1.IngressRule, pa networkingv1beta1.HTTPIng if len(pa.Path) > 0 { matcher := defaultPathMatcher - if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == networkingv1beta1.PathTypeImplementationSpecific { + if pa.PathType == nil || *pa.PathType == "" || *pa.PathType == networkingv1.PathTypeImplementationSpecific { if rtConfig != nil && rtConfig.Router != nil && rtConfig.Router.PathMatcher != "" { matcher = rtConfig.Router.PathMatcher } - } else if *pa.PathType == networkingv1beta1.PathTypeExact { + } else if *pa.PathType == networkingv1.PathTypeExact { matcher = "Path" } diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 4483c6b32..4a47fb731 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -14,7 +14,7 @@ import ( "github.com/traefik/traefik/v2/pkg/tls" "github.com/traefik/traefik/v2/pkg/types" corev1 "k8s.io/api/core/v1" - "k8s.io/api/networking/v1beta1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -1302,6 +1302,271 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { }, }, }, + { + desc: "v19 Ingress with prefix pathType", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with no pathType", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with empty pathType", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with exact pathType", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with implementationSpecific pathType", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "Path(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with ingress annotation", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with ingressClass", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-80", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with named port", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "testing-bar": { + Rule: "PathPrefix(`/bar`)", + Service: "testing-service1-foobar", + }, + }, + Services: map[string]*dynamic.Service{ + "testing-service1-foobar": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:4711", + }, + }, + }, + }, + }, + }, + }, + }, + { + desc: "v19 Ingress with missing ingressClass", + serverVersion: "v1.19", + 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: "v19 Ingress with defaultbackend", + serverVersion: "v1.19", + expected: &dynamic.Configuration{ + TCP: &dynamic.TCPConfiguration{}, + HTTP: &dynamic.HTTPConfiguration{ + Middlewares: map[string]*dynamic.Middleware{}, + Routers: map[string]*dynamic.Router{ + "default-router": { + Rule: "PathPrefix(`/`)", + Priority: math.MinInt32, + Service: "default-backend", + }, + }, + Services: map[string]*dynamic.Service{ + "default-backend": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + PassHostHeader: Bool(true), + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + }, + }, + }, + }, + }, + }, + }, } for _, test := range testCases { @@ -1375,7 +1640,7 @@ func TestGetCertificates(t *testing.T) { testCases := []struct { desc string - ingress *v1beta1.Ingress + ingress *networkingv1.Ingress client Client result map[string]*tls.CertAndStores errResult string