Repository: traefik/traefik (language: go)

Issue:
Publish IPs from ClusterIP and NodePort services on Ingresses
<!-- PLEASE FOLLOW THE ISSUE TEMPLATE TO HELP TRIAGE AND SUPPORT! -->

### Do you want to request a *feature* or report a *bug*?

<!--
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.

The issue tracker is for reporting bugs and feature requests only.
For end-user related support questions, please refer to one of the following:

- the Traefik community forum: https://community.containo.us/

-->

Feature

### What did you expect to see?

<!--

HOW TO WRITE A GOOD ISSUE?

- Respect the issue template as much as possible.
- The title should be short and descriptive.
- Explain the conditions which led you to report this issue: the context.
- The context should lead to something, an idea or a problem that you’re facing.
- Remain clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown

-->

I'm trying to setup ArgoCD, which is using the status of an Ingress for its health checks.

In the Traefik config there is the option to publish the service status into all Ingresses by adding the following to the static config:

```yaml
providers:
  kubernetesIngress:
    ingressEndpoint:
      publishedService: <NAMESPACE>/<TRAEFIK_SERVICE_NAME>
```

This is only doing anything for `LoadBalancer` type services though, as the status is not set on other types of services.

When not running in the cloud it is pretty common, that the traefik service is not of type `LoadBalancer`, but a `ClusterIP` service with external IPs configured or even a `NodePort` service.

A working solution is, to configure one of the the following instead:

#### `ClusterIP` service with external IP `80.70.60.50`

```yaml
providers:
  kubernetesIngress:
    ingressEndpoint:
      ip: 80.70.60.50
```

#### `NodePort` service with one node having IP `10.20.30.40` and hostname `node1.my-domain.com`

```yaml
providers:
  kubernetesIngress:
    ingressEndpoint:
      ip: 10.20.30.40
```

or 

```yaml
providers:
  kubernetesIngress:
    ingressEndpoint:
      hostname: node1.my-domain.com
```

There are multiple problems with this solution:
* It seems that the configured IP and/or hostname is completely arbitrary. So you can configure anything (that is a valid hostname or valid IP) and it does not need to have any connection to your cluster whatsoever.
* In case of the `ClusterIP` service you have to duplicate the IP and manually keep both places in sync to keep it consistent.
* You cannot reflect, that a `ClusterIP` service has multiple external IPs or a `NodePort` is open on multiple nodes.

### Proposal

In case the service, that is configured on the `publishedService` field, is a `ClusterIP` service all IPs given in the list of `externaIPs` are copied to the Ingress status.

In case the service, that is configured on the `publishedService` field, is a `NodePort` service all IPs of all nodes are set in the Ingress status.

### Links

The original feature request: https://github.com/traefik/traefik/issues/2173
The question I asked on the community forums: https://community.traefik.io/t/usage-of-publishedservice-in-externalip-setup/9971



Discussion / hints:
Unfortunately I have no experience in Go yet, but I would gladly help implementing this feature.

I already had a look at the code and I think I found the place to adapt: https://github.com/traefik/traefik/blob/v2.4/pkg/provider/kubernetes/ingress/kubernetes.go#L315

And here's an idea on how to do this:

```golang
func (p *Provider) updateIngressStatus(ing *networkingv1beta1.Ingress, k8sClient Client) error {
	[ ... ]

	service, exists, err := k8sClient.GetService(serviceNamespace, serviceName)
	if err != nil {
		return fmt.Errorf("cannot get service %s, received error: %w", p.IngressEndpoint.PublishedService, err)
	}

	if !exists {
		return fmt.Errorf("missing service: %s", p.IngressEndpoint.PublishedService)
	}

	if service.Spec.Type == corev1.ServiceTypeClusterIP {
		loadBalancerIngresses := make([]corev1.LoadBalancerIngress, len(service.Spec.ExternalIPs))
		for i, ip := range service.Spec.ExternalIPs {
			loadBalancerIngresses[i] = corev1.LoadBalancerIngress{ IP: ip }
		}
		return k8sClient.UpdateIngressStatus(ing, loadBalancerIngresses)
	} else if service.Spec.Type == corev1.ServiceTypeNodePort {
		// TODO this is only an idea to get all nodes and their (internal) IPs
		// maybe this also misses a filter for all nodes with taint `node-role.kubernetes.io/master: NoSchedule`
		// or maybe something like all workers only (all without label `node-role.kubernetes.io/master`)
		nodes := k8sClient.GetNodes() // this does not exist
		loadBalancerIngresses := make([]corev1.LoadBalancerIngress, len(nodes))
		for i, node := range nodes {
			loadBalancerIngresses[i] = corev1.LoadBalancerIngress{ IP: node.InternalIP }
		}
		return k8sClient.UpdateIngressStatus(ing, loadBalancerIngresses)
	} else if service.Status.LoadBalancer.Ingress != nil {
		return k8sClient.UpdateIngressStatus(ing, service.Status.LoadBalancer.Ingress)
	} else {
		// service exists, but has no Load Balancer status
		log.Debugf("Skipping updating Ingress %s/%s due to service %s having no status set", ing.Namespace, ing.Name, p.IngressEndpoint.PublishedService)
		return nil
	}
}
```

I could not find any feature to get all nodes (yet). But I tried to roughly describe my idea there.

What do you think? Would this be a viable solution?

Is this feature a good idea in general? Or is this somehow misusing the field for something it is not designed for?
Hi All,
I've now run into this issue on my own cluster.
I've tried the workarounds but they don't seem to work at the moment - the status does not get updated on the svc. (and then I get the message: `Skipping updating Ingress xxx due to service kube-system/traefik having no status set`
Has something changed again since this issue was written? I was going to write on the community forums, however the original thread was locked and can no longer be updated...
Kind Regards;
Dave
> What do you think? Would this be a viable solution?
> 
> Is this feature a good idea in general? Or is this somehow misusing the field for something it is not designed for?

I think this is a very good idea. In my case, the benefit is to use the K8s as a Configuration Discovery Provider for an external Traefik host. This allows me to centralize all the hosts in my network into one instance of Traefik with all the auto-configuration utilities it has. 
The draft code you add seems good and explains well your idea. 

Can we have a confirmation of a member or a contributor this is in the Traefik Roadmap? Or should this be added as a plugin?

EDIT: I've seen this issue has already been marked as in the v2 roadmap, probably best to start. ~I will soon add a PR for the Feature.~

EDIT2: This end up getting out of my radar.I Found an alternative solution for using K8s with external traefik.
whenever you try setting the ip setting for the ingressEndpoint, make sure you are not setting publishedService, because it will override the IP setting and then it won't work
Hey, we think this proposal might make sense but unfortunately, this would not make it to our roadmap for a while as we are focused elsewhere.  If you or another community member would like to build it, let us know and we will work with you to make sure you have all the information needed.

We prefer to work with our community members at the beginning of the design process to make sure we are aligned and can move quickly with the review and merge process.

If you feel the current design is ready for a PR feel free to open and we can continue iterating from there just don’t forget to check out the [contributor docs](https://github.com/traefik/contributors-guide/blob/master/pr_guidelines) and to link the PR to this issue.
@ddtmachado "this would not make it to our roadmap for a while as we are focused elsewhere.", makes me refrain from advocating Traefik Enterprise in our company and to others.
For support multiple Addresses (e.g. ipv4 and ipv6), ingressEndpoint should be an array:
```yaml
providers:
  kubernetesIngress:
    ingressEndpoint:
      - ip: 10.20.30.40
      - ip: 2a01:4f8::1
  ```
Still no solution for this? I'm banging my head to solve it somehow...
There are solutions (rather Workarounds?) for ClusterIP and NodePort services posted in my initial question.

But afaik there is no built-in feature to automatically propagate those to  Ingresses, no.
@maxnitze I was able to use your workaround, but still it's a bit odd that after 3 years this is still a thing... 
I guess most people either use a public cloud provider or on-premise clusters with something like MetalLB, so having non-LoadBalancer services for your ingress controller is not really a thing anymore.

I am also not using any such service anymore. So this is not an issue for me anymore.

Fix patch:
diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md
index c4cf033487..e4c5ea284f 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 7f7ec34556..74747acdcd 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
 <!-- markdownlint-disable MD013 -->
 
-| 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.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**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.<br />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.<br />Used for the Kubernetes client configuration. | "" | No |
-| `providers.kubernetesCRD.namespaces` | Array of namespaces to watch.<br />If left empty, watch all namespaces. | | No |
-| `providers.kubernetesIngress.labelselector` | Allow filtering on Ingress objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />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.<br />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.<br />It alleviates the requirement of giving Traefik the rights to look IngressClasses up.<br />Ignore Ingresses with IngressClass.<br />Annotations are not affected by this option. | false  | No |
-| `providers.kubernetesIngress.`<br />`ingressEndpoint.hostname` | Hostname used for Kubernetes Ingress endpoints. | ""  | No |
-| `providers.kubernetesIngress.`<br />`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.`<br />`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.<br />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.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
-| `providers.kubernetesIngress.allowEmptyServices` | Allows creating a route to reach a service that has no endpoint available.<br />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.<br />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`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />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).<br />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.<br />If multiple events occur within this time, only the most recent one is taken into account, and all others are discarded.<br />**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.<br />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.<br />Used for the Kubernetes client configuration.                                                                                                                                                                                                                                                                                                                                                                             | ""      | No       |
+| `providers.kubernetesCRD.namespaces`                                   | Array of namespaces to watch.<br />If left empty, watch all namespaces.                                                                                                                                                                                                                                                                                                                                                                                                |         | No       |
+| `providers.kubernetesIngress.labelselector`                            | Allow filtering on Ingress objects using label selectors.<br />No effect on Kubernetes `Secrets`, `EndpointSlices` and `Services`.<br />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.<br />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.<br />It alleviates the requirement of giving Traefik the rights to look IngressClasses up.<br />Ignore Ingresses with IngressClass.<br />Annotations are not affected by this option.                                                                                                                                                                                                                               | false   | No       |
+| `providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`         | Hostname used for Kubernetes Ingress endpoints.                                                                                                                                                                                                                                                                                                                                                                                                                        | ""      | No       |
+| `providers.kubernetesIngress.`<br />`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.`<br />`ingressEndpoint.publishedService` | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice).                                                                                                                                                                                                                                                                                                                                                            | ""      | No       |
+| `providers.kubernetesIngress.throttleDuration`                         | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught.                                                                                                                                                                                             | 0s      | No       |
+| `providers.kubernetesIngress.allowEmptyServices`                       | Allows creating a route to reach a service that has no endpoint available.<br />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.<br />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`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />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).<br />This will also prevent from using the `NodePortLB` options on services. | false   | No       |
 
 <!-- markdownlint-enable MD013 -->
 
@@ -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/client.go b/pkg/provider/kubernetes/ingress/client.go
index 7eaff4307b..00b5397dad 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 0000000000..d6898be41d
--- /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 0000000000..c0ae05ba54
--- /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 0000000000..220bfec7f3
--- /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 e7507572a4..819c24032b 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 {


Test patch:
diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go
index 8f011e4163..84fc2c4fb5 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/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go
index 596d6c3614..f8bf37037b 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
+}