From 6d3a685d5a09575f1b29d95779eb1c6578e9dab1 Mon Sep 17 00:00:00 2001
From: mlec <42201667+mlec1@users.noreply.github.com>
Date: Fri, 3 Jan 2025 16:10:04 +0100
Subject: [PATCH] Add ingress status for ClusterIP and NodePort Service Type
---
docs/content/providers/kubernetes-ingress.md | 12 +-
.../kubernetes/kubernetes-ingress.md | 70 +++++++---
.../kubernetes/ingress/annotations_test.go | 4 +-
pkg/provider/kubernetes/ingress/client.go | 19 +--
.../fixtures/Published-Service-ClusterIP.yml | 72 ++++++++++
.../Published-Service-LoadBalancer.yml | 84 ++++++++++++
.../fixtures/Published-Service-NodePort.yml | 92 +++++++++++++
pkg/provider/kubernetes/ingress/kubernetes.go | 81 +++++++++--
.../kubernetes/ingress/kubernetes_test.go | 129 +++++++++++++++++-
9 files changed, 505 insertions(+), 58 deletions(-)
create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Published-Service-ClusterIP.yml
create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Published-Service-LoadBalancer.yml
create mode 100644 pkg/provider/kubernetes/ingress/fixtures/Published-Service-NodePort.yml
diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md
index c4cf03348..e4c5ea284 100644
--- a/docs/content/providers/kubernetes-ingress.md
+++ b/docs/content/providers/kubernetes-ingress.md
@@ -398,11 +398,17 @@ providers:
_Optional, Default: ""_
-The Kubernetes service to copy status from.
-When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the ingresses.
-
Format: `namespace/servicename`.
+The Kubernetes service to copy status from,
+depending on the service type:
+
+- **ClusterIP:** The ExternalIPs of the service will be propagated to the ingress status.
+- **NodePort:** The ExternalIP addresses of the nodes in the cluster will be propagated to the ingress status.
+- **LoadBalancer:** The IPs from the service's `loadBalancer.status` field (which contains the endpoints provided by the load balancer) will be propagated to the ingress status.
+
+When using third-party tools such as External-DNS, this option enables the copying of external service IPs to the ingress resources.
+
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
diff --git a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
index 7f7ec3455..74747acdc 100644
--- a/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
+++ b/docs/content/reference/install-configuration/providers/kubernetes/kubernetes-ingress.md
@@ -39,25 +39,25 @@ which in turn creates the resulting routers, services, handlers, etc.
## Configuration Options
-| Field | Description | Default | Required |
-|:------|:----------------------------------------------------------|:---------------------|:---------|
-| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
-| `providers.kubernetesIngress.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
-| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesCRD.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No |
-| `providers.kubernetesIngress.labelselector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
-| `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
-| `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.hostname` | Hostname used for Kubernetes Ingress endpoints. | "" | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.ip` | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
-| `providers.kubernetesIngress.`
`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.
When using third parties tools like External-DNS, this option can be used to copy the service `loadbalancer.status` (containing the service's endpoints IPs) to the ingresses. | "" | No |
-| `providers.kubernetesIngress.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
-| `providers.kubernetesIngress.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
-| `providers.kubernetesIngress.allowCrossNamespace` | Allows the `Ingress` to reference resources in namespaces other than theirs. | false | No |
-| `providers.kubernetesIngress.allowExternalNameServices` | Allows the `Ingress` to reference ExternalName services. | false | No |
-| `providers.kubernetesIngress.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.
It can br overridden in the [`ServerTransport`](../../../../routing/services/index.md#serverstransport). | false | No |
-| `providers.kubernetesIngress.disableClusterScopeResources` | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).
By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.
Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).
This will also prevent from using the `NodePortLB` options on services. | false | No |
+| Field | Description | Default | Required |
+|:-----------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|:---------|
+| `providers.providersThrottleDuration` | Minimum amount of time to wait for, after a configuration reload, before taking into account any new configuration refresh event.
If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.
**This option cannot be set per provider, but the throttling algorithm applies to each of them independently.** | 2s | No |
+| `providers.kubernetesIngress.endpoint` | Server endpoint URL.
More information [here](#endpoint). | "" | No |
+| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesIngress.certAuthFilePath` | Path to the certificate authority file.
Used for the Kubernetes client configuration. | "" | No |
+| `providers.kubernetesCRD.namespaces` | Array of namespaces to watch.
If left empty, watch all namespaces. | | No |
+| `providers.kubernetesIngress.labelselector` | Allow filtering on Ingress objects using label selectors.
No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.
See [label-selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) for details. | "" | No |
+| `providers.kubernetesIngress.ingressClass` | The `IngressClass` resource name or the `kubernetes.io/ingress.class` annotation value that identifies resource objects to be processed.
If empty, resources missing the annotation, having an empty value, or the value `traefik` are processed. | "" | No |
+| `providers.kubernetesIngress.disableIngressClassLookup` | Prevent to discover IngressClasses in the cluster.
It alleviates the requirement of giving Traefik the rights to look IngressClasses up.
Ignore Ingresses with IngressClass.
Annotations are not affected by this option. | false | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.hostname` | Hostname used for Kubernetes Ingress endpoints. | "" | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.ip` | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
+| `providers.kubernetesIngress.`
`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.
More information [here](#ingressendpointpublishedservice). | "" | No |
+| `providers.kubernetesIngress.throttleDuration` | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.
This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.
If empty, every event is caught. | 0s | No |
+| `providers.kubernetesIngress.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.
It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
+| `providers.kubernetesIngress.allowCrossNamespace` | Allows the `Ingress` to reference resources in namespaces other than theirs. | false | No |
+| `providers.kubernetesIngress.allowExternalNameServices` | Allows the `Ingress` to reference ExternalName services. | false | No |
+| `providers.kubernetesIngress.nativeLBByDefault` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.
It can br overridden in the [`ServerTransport`](../../../../routing/services/index.md#serverstransport). | false | No |
+| `providers.kubernetesIngress.disableClusterScopeResources` | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).
By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.
Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).
This will also prevent from using the `NodePortLB` options on services. | false | No |
@@ -99,6 +99,38 @@ providers:
--providers.kubernetesingress.endpoint=http://localhost:8080
```
+### `ingressEndpoint.publishedService`
+
+Format: `namespace/servicename`.
+
+The Kubernetes service to copy status from,
+depending on the service type:
+
+- **ClusterIP:** The ExternalIPs of the service will be propagated to the ingress status.
+- **NodePort:** The ExternalIP addresses of the nodes in the cluster will be propagated to the ingress status.
+- **LoadBalancer:** The IPs from the service's `loadBalancer.status` field (which contains the endpoints provided by the load balancer) will be propagated to the ingress status.
+
+When using third-party tools such as External-DNS, this option enables the copying of external service IPs to the ingress resources.
+
+```yaml tab="File (YAML)"
+providers:
+ kubernetesIngress:
+ ingressEndpoint:
+ publishedService: "namespace/foo-service"
+ # ...
+```
+
+```toml tab="File (TOML)"
+[providers.kubernetesIngress.ingressEndpoint]
+ publishedService = "namespace/foo-service"
+ # ...
+```
+
+```bash tab="CLI"
+--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
+```
+
+
## Routing Configuration
See the dedicated section in [routing](../../../../routing/providers/kubernetes-ingress.md).
diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go
index 8f011e416..84fc2c4fb 100644
--- a/pkg/provider/kubernetes/ingress/annotations_test.go
+++ b/pkg/provider/kubernetes/ingress/annotations_test.go
@@ -131,7 +131,7 @@ func Test_parseServiceConfig(t *testing.T) {
Secure: true,
HTTPOnly: true,
SameSite: "none",
- Path: String("foobar"),
+ Path: pointer("foobar"),
},
},
ServersScheme: "protocol",
@@ -150,7 +150,7 @@ func Test_parseServiceConfig(t *testing.T) {
Service: &ServiceIng{
Sticky: &dynamic.Sticky{
Cookie: &dynamic.Cookie{
- Path: String("/"),
+ Path: pointer("/"),
},
},
PassHostHeader: pointer(true),
diff --git a/pkg/provider/kubernetes/ingress/client.go b/pkg/provider/kubernetes/ingress/client.go
index 7eaff4307..00b5397da 100644
--- a/pkg/provider/kubernetes/ingress/client.go
+++ b/pkg/provider/kubernetes/ingress/client.go
@@ -10,7 +10,6 @@ import (
"slices"
"time"
- "github.com/hashicorp/go-version"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v3/pkg/types"
@@ -58,7 +57,6 @@ type clientWrapper struct {
disableIngressClassInformer bool // Deprecated.
disableClusterScopeInformer bool
watchedNamespaces []string
- serverVersion *version.Version
}
// newInClusterClient returns a new Provider client that is expected to run
@@ -141,19 +139,6 @@ func newClientImpl(clientset kclientset.Interface) *clientWrapper {
// WatchAll starts namespace-specific controllers for all relevant kinds.
func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) {
- // Get and store the serverVersion for future use.
- serverVersionInfo, err := c.clientset.Discovery().ServerVersion()
- if err != nil {
- return nil, fmt.Errorf("could not retrieve server version: %w", err)
- }
-
- serverVersion, err := version.NewVersion(serverVersionInfo.GitVersion)
- if err != nil {
- return nil, fmt.Errorf("could not parse server version: %w", err)
- }
-
- c.serverVersion = serverVersion
-
eventCh := make(chan interface{}, 1)
eventHandler := &k8s.ResourceEventHandler{Ev: eventCh}
@@ -175,7 +160,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
for _, ns := range namespaces {
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector))
- _, err = factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
+ _, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
@@ -230,7 +215,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
- _, err = c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
+ _, err := c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Published-Service-ClusterIP.yml b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-ClusterIP.yml
new file mode 100644
index 000000000..d6898be41
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-ClusterIP.yml
@@ -0,0 +1,72 @@
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: published-service
+ namespace: default
+
+spec:
+ type: ClusterIP
+ externalIPs:
+ - 1.2.3.4
+ - 5.6.7.8
+
+ ports:
+ - port: 9090
+ protocol: TCP
+ name: first
+
+ - port: 9091
+ protocol: TCP
+ name: second
+
+---
+kind: Ingress
+apiVersion: networking.k8s.io/v1
+metadata:
+ name: foo
+ namespace: default
+
+spec:
+ rules:
+ - host: "*.foo.com"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: service1
+ port:
+ number: 80
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: service1
+ namespace: default
+
+spec:
+ ports:
+ - port: 80
+
+ clusterIP: 10.0.0.1
+
+---
+kind: EndpointSlice
+apiVersion: discovery.k8s.io/v1
+metadata:
+ name: service1-abc
+ labels:
+ kubernetes.io/service-name: service1
+
+addressType: IPv4
+ports:
+ - port: 8080
+ name: ""
+endpoints:
+ - addresses:
+ - 10.10.0.1
+ conditions:
+ ready: true
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Published-Service-LoadBalancer.yml b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-LoadBalancer.yml
new file mode 100644
index 000000000..c0ae05ba5
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-LoadBalancer.yml
@@ -0,0 +1,84 @@
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: published-service
+ namespace: default
+
+spec:
+ type: LoadBalancer
+ ports:
+ - port: 9090
+ protocol: TCP
+ name: first
+
+ - port: 9091
+ protocol: TCP
+ name: second
+
+status:
+ loadBalancer:
+ ingress:
+ - ip: 1.2.3.4
+ ports:
+ - port: 9090
+ protocol: TCP
+ - port: 9091
+ protocol: TCP
+
+ - ip: 5.6.7.8
+ ports:
+ - port: 9090
+ protocol: TCP
+ - port: 9091
+ protocol: TCP
+---
+kind: Ingress
+apiVersion: networking.k8s.io/v1
+metadata:
+ name: foo
+ namespace: default
+
+spec:
+ rules:
+ - host: "*.foo.com"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: service1
+ port:
+ number: 80
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: service1
+ namespace: default
+
+spec:
+ ports:
+ - port: 80
+
+ clusterIP: 10.0.0.1
+
+---
+kind: EndpointSlice
+apiVersion: discovery.k8s.io/v1
+metadata:
+ name: service1-abc
+ labels:
+ kubernetes.io/service-name: service1
+
+addressType: IPv4
+ports:
+ - port: 8080
+ name: ""
+endpoints:
+ - addresses:
+ - 10.10.0.1
+ conditions:
+ ready: true
diff --git a/pkg/provider/kubernetes/ingress/fixtures/Published-Service-NodePort.yml b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-NodePort.yml
new file mode 100644
index 000000000..220bfec7f
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress/fixtures/Published-Service-NodePort.yml
@@ -0,0 +1,92 @@
+---
+kind: Node
+apiVersion: v1
+metadata:
+ name: node1
+
+status:
+ addresses:
+ - type: ExternalIP
+ address: 1.2.3.4
+
+---
+kind: Node
+apiVersion: v1
+metadata:
+ name: node2
+
+status:
+ addresses:
+ - type: ExternalIP
+ address: 5.6.7.8
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: published-service
+ namespace: default
+
+spec:
+ type: NodePort
+ ports:
+ - port: 9190
+ protocol: TCP
+ name: first
+ nodePort: 9090
+
+ - port: 9191
+ protocol: TCP
+ name: second
+ nodePort: 9091
+
+---
+kind: Ingress
+apiVersion: networking.k8s.io/v1
+metadata:
+ name: foo
+ namespace: default
+
+spec:
+ rules:
+ - host: "*.foo.com"
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: service1
+ port:
+ number: 80
+
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: service1
+ namespace: default
+
+spec:
+ ports:
+ - port: 80
+
+ clusterIP: 10.0.0.1
+
+---
+kind: EndpointSlice
+apiVersion: discovery.k8s.io/v1
+metadata:
+ name: service1-abc
+ labels:
+ kubernetes.io/service-name: service1
+
+addressType: IPv4
+ports:
+ - port: 8080
+ name: ""
+endpoints:
+ - addresses:
+ - 10.10.0.1
+ conditions:
+ ready: true
diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go
index e7507572a..819c24032 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes.go
@@ -247,6 +247,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
+ if err := p.updateIngressStatus(ingress, client); err != nil {
+ logger.Error().Err(err).Msg("Error while updating ingress status")
+ }
+
rtConfig, err := parseRouterConfig(ingress.Annotations)
if err != nil {
logger.Error().Err(err).Msg("Failed to parse annotations")
@@ -305,10 +309,6 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
routers := map[string][]*dynamic.Router{}
for _, rule := range ingress.Spec.Rules {
- if err := p.updateIngressStatus(ingress, client); err != nil {
- logger.Error().Err(err).Msg("Error while updating ingress status")
- }
-
if rule.HTTP == nil {
continue
}
@@ -407,22 +407,75 @@ func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) err
return fmt.Errorf("cannot get service %s, received error: %w", p.IngressEndpoint.PublishedService, err)
}
- if exists && service.Status.LoadBalancer.Ingress == nil {
- // service exists, but has no Load Balancer status
- log.Debug().Msgf("Skipping updating Ingress %s/%s due to service %s having no status set", ing.Namespace, ing.Name, p.IngressEndpoint.PublishedService)
- return nil
- }
-
if !exists {
return fmt.Errorf("missing service: %s", p.IngressEndpoint.PublishedService)
}
- ingresses, err := convertSlice[netv1.IngressLoadBalancerIngress](service.Status.LoadBalancer.Ingress)
- if err != nil {
- return err
+ var ingressStatus []netv1.IngressLoadBalancerIngress
+
+ switch service.Spec.Type {
+ case corev1.ServiceTypeLoadBalancer:
+ if service.Status.LoadBalancer.Ingress == nil {
+ // service exists, but has no Load Balancer status
+ log.Debug().Msgf("Skipping updating Ingress %s/%s due to service %s having no status set", ing.Namespace, ing.Name, p.IngressEndpoint.PublishedService)
+ return nil
+ }
+
+ ingressStatus, err = convertSlice[netv1.IngressLoadBalancerIngress](service.Status.LoadBalancer.Ingress)
+ if err != nil {
+ return fmt.Errorf("converting ingress loadbalancer status: %w", err)
+ }
+
+ case corev1.ServiceTypeClusterIP:
+ var ports []netv1.IngressPortStatus
+ for _, port := range service.Spec.Ports {
+ ports = append(ports, netv1.IngressPortStatus{
+ Port: port.Port,
+ Protocol: port.Protocol,
+ })
+ }
+
+ for _, ip := range service.Spec.ExternalIPs {
+ ingressStatus = append(ingressStatus, netv1.IngressLoadBalancerIngress{
+ IP: ip,
+ Ports: ports,
+ })
+ }
+
+ case corev1.ServiceTypeNodePort:
+ if p.DisableClusterScopeResources {
+ return errors.New("node port service type is not supported when cluster scope resources lookup is disabled")
+ }
+
+ nodes, _, err := k8sClient.GetNodes()
+ if err != nil {
+ return fmt.Errorf("getting nodes: %w", err)
+ }
+
+ var ports []netv1.IngressPortStatus
+ for _, port := range service.Spec.Ports {
+ ports = append(ports, netv1.IngressPortStatus{
+ Port: port.NodePort,
+ Protocol: port.Protocol,
+ })
+ }
+
+ for _, node := range nodes {
+ for _, address := range node.Status.Addresses {
+ if address.Type == corev1.NodeExternalIP {
+ ingressStatus = append(ingressStatus, netv1.IngressLoadBalancerIngress{
+ IP: address.Address,
+ Ports: ports,
+ })
+ }
+ }
+ }
+
+ default:
+ return fmt.Errorf("unsupported service type: %s", service.Spec.Type)
}
- return k8sClient.UpdateIngressStatus(ing, ingresses)
+ return k8sClient.UpdateIngressStatus(ing, ingressStatus)
}
func (p *Provider) shouldProcessIngress(ingress *netv1.Ingress, ingressClasses []*netv1.IngressClass) bool {
diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index 596d6c361..f8bf37037 100644
--- a/pkg/provider/kubernetes/ingress/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go
@@ -4,28 +4,31 @@ import (
"context"
"errors"
"math"
+ "os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider"
+ "github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ kubefake "k8s.io/client-go/kubernetes/fake"
)
var _ provider.Provider = (*Provider)(nil)
func pointer[T any](v T) *T { return &v }
-func String(v string) *string { return &v }
-
func TestLoadConfigurationFromIngresses(t *testing.T) {
testCases := []struct {
desc string
@@ -134,7 +137,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Name: "foobar",
Secure: true,
HTTPOnly: true,
- Path: String("/"),
+ Path: pointer("/"),
},
},
Servers: []dynamic.Server{
@@ -2082,3 +2085,123 @@ func TestLoadConfigurationFromIngressesWithNativeLBByDefault(t *testing.T) {
})
}
}
+
+func TestIngressEndpointPublishedService(t *testing.T) {
+ testCases := []struct {
+ desc string
+ disableClusterScopeResources bool
+ expected []netv1.IngressLoadBalancerIngress
+ }{
+ {
+ desc: "Published Service ClusterIP",
+ expected: []netv1.IngressLoadBalancerIngress{
+ {
+ IP: "1.2.3.4",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ {
+ IP: "5.6.7.8",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ },
+ },
+ {
+ desc: "Published Service LoadBalancer",
+ expected: []netv1.IngressLoadBalancerIngress{
+ {
+ IP: "1.2.3.4",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ {
+ IP: "5.6.7.8",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ },
+ },
+ {
+ desc: "Published Service NodePort",
+ disableClusterScopeResources: true,
+ },
+ {
+ desc: "Published Service NodePort",
+ expected: []netv1.IngressLoadBalancerIngress{
+ {
+ IP: "1.2.3.4",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ {
+ IP: "5.6.7.8",
+ Ports: []netv1.IngressPortStatus{
+ {Port: 9090, Protocol: "TCP"},
+ {Port: 9091, Protocol: "TCP"},
+ },
+ },
+ },
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.desc, func(t *testing.T) {
+ t.Parallel()
+
+ k8sObjects := readResources(t, []string{generateTestFilename(test.desc)})
+ kubeClient := kubefake.NewClientset(k8sObjects...)
+
+ client := newClientImpl(kubeClient)
+
+ stopCh := make(chan struct{})
+ eventCh, err := client.WatchAll(nil, stopCh)
+ require.NoError(t, err)
+
+ if k8sObjects != nil {
+ // just wait for the first event
+ <-eventCh
+ }
+
+ p := Provider{
+ DisableClusterScopeResources: test.disableClusterScopeResources,
+ IngressEndpoint: &EndpointIngress{
+ PublishedService: "default/published-service",
+ },
+ }
+ p.loadConfigurationFromIngresses(context.Background(), client)
+
+ ingress, err := kubeClient.NetworkingV1().Ingresses(metav1.NamespaceDefault).Get(context.Background(), "foo", metav1.GetOptions{})
+ require.NoError(t, err)
+
+ assert.Equal(t, test.expected, ingress.Status.LoadBalancer.Ingress)
+ })
+ }
+}
+
+func readResources(t *testing.T, paths []string) []runtime.Object {
+ t.Helper()
+
+ var k8sObjects []runtime.Object
+ for _, path := range paths {
+ yamlContent, err := os.ReadFile(filepath.FromSlash(path))
+ if err != nil {
+ panic(err)
+ }
+
+ objects := k8s.MustParseYaml(yamlContent)
+ k8sObjects = append(k8sObjects, objects...)
+ }
+
+ return k8sObjects
+}