Add reportNodeInternalIPs option to report node internal IPs in Ingress status

This commit is contained in:
qwerty8811
2026-06-12 11:26:07 +03:00
committed by GitHub
parent bcf768ee09
commit 6cc3dd8d40
5 changed files with 211 additions and 5 deletions
@@ -395,6 +395,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-labelselector" href="#opt-providers-kubernetesingress-labelselector" title="#opt-providers-kubernetesingress-labelselector">providers.kubernetesingress.labelselector</a> | Kubernetes Ingress label selector to use. | |
| <a id="opt-providers-kubernetesingress-namespaces" href="#opt-providers-kubernetesingress-namespaces" title="#opt-providers-kubernetesingress-namespaces">providers.kubernetesingress.namespaces</a> | Kubernetes namespaces. | |
| <a id="opt-providers-kubernetesingress-nativelbbydefault" href="#opt-providers-kubernetesingress-nativelbbydefault" title="#opt-providers-kubernetesingress-nativelbbydefault">providers.kubernetesingress.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing mode by default. | false |
| <a id="opt-providers-kubernetesingress-reportnodeinternalips" href="#opt-providers-kubernetesingress-reportnodeinternalips" title="#opt-providers-kubernetesingress-reportnodeinternalips">providers.kubernetesingress.reportnodeinternalips</a> | Report node internal IPs in Ingress status. | false |
| <a id="opt-providers-kubernetesingress-strictprefixmatching" href="#opt-providers-kubernetesingress-strictprefixmatching" title="#opt-providers-kubernetesingress-strictprefixmatching">providers.kubernetesingress.strictprefixmatching</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). | false |
| <a id="opt-providers-kubernetesingress-throttleduration" href="#opt-providers-kubernetesingress-throttleduration" title="#opt-providers-kubernetesingress-throttleduration">providers.kubernetesingress.throttleduration</a> | Ingress refresh throttle duration | 0 |
| <a id="opt-providers-kubernetesingress-token" href="#opt-providers-kubernetesingress-token" title="#opt-providers-kubernetesingress-token">providers.kubernetesingress.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
@@ -58,12 +58,13 @@ which in turn creates the resulting routers, services, handlers, etc.
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-hostname" href="#opt-providers-kubernetesIngress-ingressEndpoint-hostname" title="#opt-providers-kubernetesIngress-ingressEndpoint-hostname">`providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`</a> | Hostname used for Kubernetes Ingress endpoints. | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-ip" href="#opt-providers-kubernetesIngress-ingressEndpoint-ip" title="#opt-providers-kubernetesIngress-ingressEndpoint-ip">`providers.kubernetesIngress.`<br />`ingressEndpoint.ip`</a> | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-publishedService" href="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService" title="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService">`providers.kubernetesIngress.`<br />`ingressEndpoint.publishedService`</a> | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice). | "" | No |
| <a id="opt-providers-kubernetesIngress-reportNodeInternalIPs" href="#opt-providers-kubernetesIngress-reportNodeInternalIPs" title="#opt-providers-kubernetesIngress-reportNodeInternalIPs">`providers.kubernetesIngress.reportNodeInternalIPs`</a> | Report node internal IPs in Ingress status.<br />Incompatible with `ingressEndpoint` and `disableClusterScopeResources`.<br />More information [here](#reportnodeinternalips). | false | No |
| <a id="opt-providers-kubernetesIngress-throttleDuration" href="#opt-providers-kubernetesIngress-throttleDuration" title="#opt-providers-kubernetesIngress-throttleDuration">`providers.kubernetesIngress.throttleDuration`</a> | 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 |
| <a id="opt-providers-kubernetesIngress-allowEmptyServices" href="#opt-providers-kubernetesIngress-allowEmptyServices" title="#opt-providers-kubernetesIngress-allowEmptyServices">`providers.kubernetesIngress.allowEmptyServices`</a> | 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 |
| <a id="opt-providers-kubernetesIngress-allowExternalNameServices" href="#opt-providers-kubernetesIngress-allowExternalNameServices" title="#opt-providers-kubernetesIngress-allowExternalNameServices">`providers.kubernetesIngress.allowExternalNameServices`</a> | Allows the `Ingress` to reference ExternalName services. | false | No |
| <a id="opt-providers-kubernetesIngress-crossProviderNamespaces" href="#opt-providers-kubernetesIngress-crossProviderNamespaces" title="#opt-providers-kubernetesIngress-crossProviderNamespaces">`providers.kubernetesIngress.crossProviderNamespaces`</a> | List of namespaces from which Ingresses or Services are allowed to use `traefik.ingress.kubernetes.io/router.middlewares`, `traefik.ingress.kubernetes.io/router.tls.options`, or `traefik.ingress.kubernetes.io/service.serverstransport` annotations.<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
| <a id="opt-providers-kubernetesIngress-nativeLBByDefault" href="#opt-providers-kubernetesIngress-nativeLBByDefault" title="#opt-providers-kubernetesIngress-nativeLBByDefault">`providers.kubernetesIngress.nativeLBByDefault`</a> | 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 be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | 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 |
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | 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 and is incompatible with `reportNodeInternalIPs`. | false | No |
| <a id="opt-providers-kubernetesIngress-strictPrefixMatching" href="#opt-providers-kubernetesIngress-strictPrefixMatching" title="#opt-providers-kubernetesIngress-strictPrefixMatching">`providers.kubernetesIngress.strictPrefixMatching`</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
<!-- markdownlint-enable MD013 -->
@@ -138,6 +139,31 @@ providers:
--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
```
### `reportNodeInternalIPs`
When set to `true`, Traefik reports the internal IPs of all nodes in the cluster into the `status.loadBalancer.ingress` field of each managed Ingress resource.
This is the equivalent of ingress-nginx's `--report-node-internal-ip-address` flag and is the recommended approach for bare-metal Kubernetes deployments where Traefik runs as a DaemonSet without a cloud LoadBalancer or MetalLB.
This option requires cluster-scope access to Node resources and is mutually exclusive with `ingressEndpoint` and `disableClusterScopeResources`.
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
reportNodeInternalIPs: true
# ...
```
```toml tab="File (TOML)"
[providers.kubernetesIngress]
reportNodeInternalIPs = true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.reportnodeinternalips=true
```
## Routing Configuration
See the dedicated section in [routing](../../../../reference/routing-configuration/kubernetes/ingress.md).
@@ -0,0 +1,75 @@
---
kind: Node
apiVersion: v1
metadata:
name: node1
status:
addresses:
- type: InternalIP
address: 10.0.0.1
- type: ExternalIP
address: 1.2.3.4
---
kind: Node
apiVersion: v1
metadata:
name: node2
status:
addresses:
- type: InternalIP
address: 10.0.0.2
---
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
namespace: default
labels:
kubernetes.io/service-name: service1
addressType: IPv4
ports:
- port: 8080
name: ""
endpoints:
- addresses:
- 10.10.0.1
conditions:
ready: true
+37 -4
View File
@@ -48,6 +48,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ReportNodeInternalIPs bool `description:"Report node internal IPs in Ingress status." json:"reportNodeInternalIPs,omitempty" toml:"reportNodeInternalIPs,omitempty" yaml:"reportNodeInternalIPs,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
@@ -72,6 +73,14 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
// Init the provider.
func (p *Provider) Init() error {
if p.ReportNodeInternalIPs && p.IngressEndpoint != nil {
return errors.New("reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
}
if p.ReportNodeInternalIPs && p.DisableClusterScopeResources {
return errors.New("reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
}
return nil
}
@@ -246,6 +255,26 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
ingresses := client.GetIngresses()
var nodeIngressStatus []netv1.IngressLoadBalancerIngress
if p.ReportNodeInternalIPs {
nodes, _, err := client.GetNodes()
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("Error while getting nodes for ingress status")
} else {
for _, node := range nodes {
for _, address := range node.Status.Addresses {
if address.Type == corev1.NodeInternalIP {
nodeIngressStatus = append(nodeIngressStatus, netv1.IngressLoadBalancerIngress{IP: address.Address})
}
}
}
if len(nodeIngressStatus) == 0 {
log.Ctx(ctx).Error().Msg("No nodes with internal IP address found for ingress status")
}
}
}
certConfigs := make(map[string]*tls.CertAndStores)
for _, ingress := range ingresses {
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
@@ -255,7 +284,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue
}
if err := p.updateIngressStatus(ingress, client); err != nil {
if err := p.updateIngressStatus(ingress, client, nodeIngressStatus); err != nil {
logger.Error().Err(err).Msg("Error while updating ingress status")
}
@@ -442,7 +471,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
return conf
}
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) error {
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client, nodeIngressStatus []netv1.IngressLoadBalancerIngress) error {
if len(nodeIngressStatus) > 0 {
return k8sClient.UpdateIngressStatus(ing, nodeIngressStatus)
}
// Only process if an EndpointIngress has been configured.
if p.IngressEndpoint == nil {
return nil
@@ -623,12 +656,12 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
return nil, errors.New("nodes lookup is disabled")
}
nodes, nodesExists, nodesErr := client.GetNodes()
nodes, _, nodesErr := client.GetNodes()
if nodesErr != nil {
return nil, nodesErr
}
if !nodesExists || len(nodes) == 0 {
if len(nodes) == 0 {
return nil, fmt.Errorf("nodes not found in namespace %s", namespace)
}
@@ -3094,6 +3094,77 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
return k8sObjects
}
func TestProviderInit(t *testing.T) {
p := Provider{
ReportNodeInternalIPs: true,
IngressEndpoint: &EndpointIngress{IP: "1.2.3.4"},
}
assert.EqualError(t, p.Init(), "reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
p2 := Provider{
ReportNodeInternalIPs: true,
DisableClusterScopeResources: true,
}
assert.EqualError(t, p2.Init(), "reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
p3 := Provider{ReportNodeInternalIPs: true}
assert.NoError(t, p3.Init())
}
func TestReportNodeInternalIPs(t *testing.T) {
testCases := []struct {
desc string
client clientMock
expectedEmpty bool
}{
{
desc: "nodes present",
client: newClientMock(generateTestFilename("Node Internal IP")),
},
{
desc: "GetNodes API error",
client: clientMock{apiNodesError: errors.New("api nodes error")},
expectedEmpty: true,
},
{
desc: "no nodes found",
client: clientMock{nodes: []*corev1.Node{}},
expectedEmpty: true,
},
{
desc: "nodes exist but none have an internal IP",
client: clientMock{
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "1.2.3.4"},
},
},
},
},
},
expectedEmpty: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := Provider{ReportNodeInternalIPs: true}
conf := p.loadConfigurationFromIngresses(t.Context(), test.client)
if test.expectedEmpty {
assert.Empty(t, conf.HTTP.Routers)
assert.Empty(t, conf.HTTP.Services)
} else {
assert.NotEmpty(t, conf.HTTP.Routers)
assert.NotEmpty(t, conf.HTTP.Services)
}
})
}
}
func TestStrictPrefixMatchingRule(t *testing.T) {
tests := []struct {
path string