Add ingress status for ClusterIP and NodePort Service Type
This commit is contained in:
parent
845d0b5ac7
commit
6d3a685d5a
9 changed files with 505 additions and 58 deletions
|
@ -398,11 +398,17 @@ providers:
|
||||||
|
|
||||||
_Optional, Default: ""_
|
_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`.
|
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)"
|
```yaml tab="File (YAML)"
|
||||||
providers:
|
providers:
|
||||||
kubernetesIngress:
|
kubernetesIngress:
|
||||||
|
|
|
@ -40,7 +40,7 @@ which in turn creates the resulting routers, services, handlers, etc.
|
||||||
<!-- markdownlint-disable MD013 -->
|
<!-- markdownlint-disable MD013 -->
|
||||||
|
|
||||||
| Field | Description | Default | Required |
|
| 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.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.endpoint` | Server endpoint URL.<br />More information [here](#endpoint). | "" | No |
|
||||||
| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
|
| `providers.kubernetesIngress.token` | Bearer token used for the Kubernetes client configuration. | "" | No |
|
||||||
|
@ -51,7 +51,7 @@ which in turn creates the resulting routers, services, handlers, etc.
|
||||||
| `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.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.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.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.`<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.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.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.allowCrossNamespace` | Allows the `Ingress` to reference resources in namespaces other than theirs. | false | No |
|
||||||
|
@ -99,6 +99,38 @@ providers:
|
||||||
--providers.kubernetesingress.endpoint=http://localhost:8080
|
--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
|
## Routing Configuration
|
||||||
|
|
||||||
See the dedicated section in [routing](../../../../routing/providers/kubernetes-ingress.md).
|
See the dedicated section in [routing](../../../../routing/providers/kubernetes-ingress.md).
|
||||||
|
|
|
@ -131,7 +131,7 @@ func Test_parseServiceConfig(t *testing.T) {
|
||||||
Secure: true,
|
Secure: true,
|
||||||
HTTPOnly: true,
|
HTTPOnly: true,
|
||||||
SameSite: "none",
|
SameSite: "none",
|
||||||
Path: String("foobar"),
|
Path: pointer("foobar"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ServersScheme: "protocol",
|
ServersScheme: "protocol",
|
||||||
|
@ -150,7 +150,7 @@ func Test_parseServiceConfig(t *testing.T) {
|
||||||
Service: &ServiceIng{
|
Service: &ServiceIng{
|
||||||
Sticky: &dynamic.Sticky{
|
Sticky: &dynamic.Sticky{
|
||||||
Cookie: &dynamic.Cookie{
|
Cookie: &dynamic.Cookie{
|
||||||
Path: String("/"),
|
Path: pointer("/"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PassHostHeader: pointer(true),
|
PassHostHeader: pointer(true),
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
|
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
|
||||||
"github.com/traefik/traefik/v3/pkg/types"
|
"github.com/traefik/traefik/v3/pkg/types"
|
||||||
|
@ -58,7 +57,6 @@ type clientWrapper struct {
|
||||||
disableIngressClassInformer bool // Deprecated.
|
disableIngressClassInformer bool // Deprecated.
|
||||||
disableClusterScopeInformer bool
|
disableClusterScopeInformer bool
|
||||||
watchedNamespaces []string
|
watchedNamespaces []string
|
||||||
serverVersion *version.Version
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newInClusterClient returns a new Provider client that is expected to run
|
// 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.
|
// WatchAll starts namespace-specific controllers for all relevant kinds.
|
||||||
func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<-chan interface{}, error) {
|
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)
|
eventCh := make(chan interface{}, 1)
|
||||||
eventHandler := &k8s.ResourceEventHandler{Ev: eventCh}
|
eventHandler := &k8s.ResourceEventHandler{Ev: eventCh}
|
||||||
|
|
||||||
|
@ -175,7 +160,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||||
for _, ns := range namespaces {
|
for _, ns := range namespaces {
|
||||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector))
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -230,7 +215,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||||
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
|
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
|
||||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -247,6 +247,10 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := p.updateIngressStatus(ingress, client); err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Error while updating ingress status")
|
||||||
|
}
|
||||||
|
|
||||||
rtConfig, err := parseRouterConfig(ingress.Annotations)
|
rtConfig, err := parseRouterConfig(ingress.Annotations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Err(err).Msg("Failed to parse annotations")
|
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{}
|
routers := map[string][]*dynamic.Router{}
|
||||||
|
|
||||||
for _, rule := range ingress.Spec.Rules {
|
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 {
|
if rule.HTTP == nil {
|
||||||
continue
|
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)
|
return fmt.Errorf("cannot get service %s, received error: %w", p.IngressEndpoint.PublishedService, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists && service.Status.LoadBalancer.Ingress == nil {
|
if !exists {
|
||||||
|
return fmt.Errorf("missing service: %s", p.IngressEndpoint.PublishedService)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// 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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
ingressStatus, err = convertSlice[netv1.IngressLoadBalancerIngress](service.Status.LoadBalancer.Ingress)
|
||||||
return fmt.Errorf("missing service: %s", p.IngressEndpoint.PublishedService)
|
|
||||||
}
|
|
||||||
|
|
||||||
ingresses, err := convertSlice[netv1.IngressLoadBalancerIngress](service.Status.LoadBalancer.Ingress)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("converting ingress loadbalancer status: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return k8sClient.UpdateIngressStatus(ing, ingresses)
|
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, ingressStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) shouldProcessIngress(ingress *netv1.Ingress, ingressClasses []*netv1.IngressClass) bool {
|
func (p *Provider) shouldProcessIngress(ingress *netv1.Ingress, ingressClasses []*netv1.IngressClass) bool {
|
||||||
|
|
|
@ -4,28 +4,31 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
ptypes "github.com/traefik/paerser/types"
|
ptypes "github.com/traefik/paerser/types"
|
||||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||||
"github.com/traefik/traefik/v3/pkg/provider"
|
"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/tls"
|
||||||
"github.com/traefik/traefik/v3/pkg/types"
|
"github.com/traefik/traefik/v3/pkg/types"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
netv1 "k8s.io/api/networking/v1"
|
netv1 "k8s.io/api/networking/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/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)
|
var _ provider.Provider = (*Provider)(nil)
|
||||||
|
|
||||||
func pointer[T any](v T) *T { return &v }
|
func pointer[T any](v T) *T { return &v }
|
||||||
|
|
||||||
func String(v string) *string { return &v }
|
|
||||||
|
|
||||||
func TestLoadConfigurationFromIngresses(t *testing.T) {
|
func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
@ -134,7 +137,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
|
||||||
Name: "foobar",
|
Name: "foobar",
|
||||||
Secure: true,
|
Secure: true,
|
||||||
HTTPOnly: true,
|
HTTPOnly: true,
|
||||||
Path: String("/"),
|
Path: pointer("/"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Servers: []dynamic.Server{
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue