Add k8s provider option to create services without endpoints

This commit is contained in:
Luca Berneking 2021-05-06 18:12:10 +02:00 committed by GitHub
parent 0b48d5d0d2
commit 32e08f3510
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 107 additions and 42 deletions

View file

@ -440,6 +440,30 @@ providers:
--providers.kubernetesingress.throttleDuration=10s --providers.kubernetesingress.throttleDuration=10s
``` ```
### `allowEmptyServices`
_Optional, Default: false
```toml tab="File (TOML)"
[providers.kubernetesIngress]
allowEmptyServices = true
# ...
```
```yaml tab="File (YAML)"
providers:
kubernetesIngress:
allowEmptyServices: true
# ...
```
```bash tab="CLI"
--providers.kubernetesingress.allowEmptyServices=true
```
Allow the creation of services if there are no endpoints available.
This results in `503` http responses instead of `404`.
### Further ### Further
To learn more about the various aspects of the Ingress specification that Traefik supports, To learn more about the various aspects of the Ingress specification that Traefik supports,

View file

@ -624,6 +624,9 @@ Kubernetes bearer token (not needed for in-cluster client).
`--providers.kubernetesingress`: `--providers.kubernetesingress`:
Enable Kubernetes backend with default settings. (Default: ```false```) Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetesingress.allowemptyservices`:
Allow creation of services without endpoints. (Default: ```false```)
`--providers.kubernetesingress.certauthfilepath`: `--providers.kubernetesingress.certauthfilepath`:
Kubernetes certificate authority file path (not needed for in-cluster client). Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -624,6 +624,9 @@ Kubernetes bearer token (not needed for in-cluster client).
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS`: `TRAEFIK_PROVIDERS_KUBERNETESINGRESS`:
Enable Kubernetes backend with default settings. (Default: ```false```) Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEMPTYSERVICES`:
Allow creation of services without endpoints. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`: `TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client). Kubernetes certificate authority file path (not needed for in-cluster client).

View file

@ -106,6 +106,7 @@
labelSelector = "foobar" labelSelector = "foobar"
ingressClass = "foobar" ingressClass = "foobar"
throttleDuration = "42s" throttleDuration = "42s"
allowEmptyServices = true
[providers.kubernetesIngress.ingressEndpoint] [providers.kubernetesIngress.ingressEndpoint]
ip = "foobar" ip = "foobar"
hostname = "foobar" hostname = "foobar"

View file

@ -114,6 +114,7 @@ providers:
labelSelector: foobar labelSelector: foobar
ingressClass: foobar ingressClass: foobar
throttleDuration: 42s throttleDuration: 42s
allowEmptyServices: true
ingressEndpoint: ingressEndpoint:
ip: foobar ip: foobar
hostname: foobar hostname: foobar

View file

@ -13,3 +13,4 @@
address = ":8000" address = ":8000"
[providers.kubernetesIngress] [providers.kubernetesIngress]
allowEmptyServices = true

View file

@ -1,15 +0,0 @@
kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata:
name: ""
namespace: testing
spec:
rules:
- host: traefik.tchouk
http:
paths:
- path: /bar
backend:
serviceName: service1
servicePort: 80

View file

@ -37,15 +37,16 @@ const (
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"` Token string `description:"Kubernetes bearer token (not needed for in-cluster client)." json:"token,omitempty" toml:"token,omitempty" yaml:"token,omitempty"`
CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)." json:"certAuthFilePath,omitempty" toml:"certAuthFilePath,omitempty" yaml:"certAuthFilePath,omitempty"`
Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"` Namespaces []string `description:"Kubernetes namespaces." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty" export:"true"`
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"` 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"` 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"` IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"` ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
lastConfiguration safe.Safe AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
lastConfiguration safe.Safe
} }
// EndpointIngress holds the endpoint information for the Kubernetes provider. // EndpointIngress holds the endpoint information for the Kubernetes provider.
@ -241,6 +242,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue continue
} }
if len(service.LoadBalancer.Servers) == 0 && !p.AllowEmptyServices {
log.FromContext(ctx).
WithField("serviceName", ingress.Spec.DefaultBackend.Service.Name).
WithField("servicePort", ingress.Spec.DefaultBackend.Service.Port.String()).
Errorf("Skipping service: no endpoints found")
continue
}
rt := &dynamic.Router{ rt := &dynamic.Router{
Rule: "PathPrefix(`/`)", Rule: "PathPrefix(`/`)",
Priority: math.MinInt32, Priority: math.MinInt32,
@ -278,6 +287,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
continue continue
} }
if len(service.LoadBalancer.Servers) == 0 && !p.AllowEmptyServices {
log.FromContext(ctx).
WithField("serviceName", pa.Backend.Service.Name).
WithField("servicePort", pa.Backend.Service.Port.String()).
Errorf("Skipping service: no endpoints found")
continue
}
portString := pa.Backend.Service.Port.Name portString := pa.Backend.Service.Port.Name
if len(pa.Backend.Service.Port.Name) == 0 { if len(pa.Backend.Service.Port.Name) == 0 {
@ -534,10 +551,6 @@ func loadService(client Client, namespace string, backend networkingv1.IngressBa
return nil, errors.New("endpoints not found") return nil, errors.New("endpoints not found")
} }
if len(endpoints.Subsets) == 0 {
return nil, errors.New("subset not found")
}
var port int32 var port int32
for _, subset := range endpoints.Subsets { for _, subset := range endpoints.Subsets {
for _, p := range subset.Ports { for _, p := range subset.Ports {
@ -562,10 +575,6 @@ func loadService(client Client, namespace string, backend networkingv1.IngressBa
} }
} }
if len(svc.LoadBalancer.Servers) == 0 {
return nil, errors.New("no valid subset found")
}
return svc, nil return svc, nil
} }

View file

@ -24,10 +24,11 @@ func Bool(v bool) *bool { return &v }
func TestLoadConfigurationFromIngresses(t *testing.T) { func TestLoadConfigurationFromIngresses(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
ingressClass string ingressClass string
serverVersion string serverVersion string
expected *dynamic.Configuration expected *dynamic.Configuration
allowEmptyServices bool
}{ }{
{ {
desc: "Empty ingresses", desc: "Empty ingresses",
@ -444,13 +445,25 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
}, },
}, },
{ {
desc: "Ingress with one service without endpoints subset", desc: "Ingress with one service without endpoints subset",
allowEmptyServices: true,
expected: &dynamic.Configuration{ expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{}, TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{ HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{}, Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{}, Routers: map[string]*dynamic.Router{
Services: map[string]*dynamic.Service{}, "testing-traefik-tchouk-bar": {
Rule: "Host(`traefik.tchouk`) && PathPrefix(`/bar`)",
Service: "testing-service1-80",
},
},
Services: map[string]*dynamic.Service{
"testing-service1-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
PassHostHeader: Bool(true),
},
},
},
}, },
}, },
}, },
@ -1631,7 +1644,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
clientMock := newClientMock(serverVersion, paths...) clientMock := newClientMock(serverVersion, paths...)
p := Provider{IngressClass: test.ingressClass} p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock) conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf) assert.Equal(t, test.expected, conf)

View file

@ -1,6 +1,6 @@
<template> <template>
<q-card flat bordered v-bind:class="['panel-servers', {'panel-servers-dense':isDense}]"> <q-card flat bordered v-bind:class="['panel-servers', {'panel-servers-dense':isDense}]">
<q-scroll-area :thumb-style="appThumbStyle" style="height:100%;"> <q-scroll-area v-if="data.loadBalancer.servers" :thumb-style="appThumbStyle" style="height:100%;">
<q-card-section> <q-card-section>
<div class="row items-start no-wrap"> <div class="row items-start no-wrap">
<div class="col-3" v-if="showStatus"> <div class="col-3" v-if="showStatus">
@ -33,6 +33,18 @@
<q-separator /> <q-separator />
</div> </div>
</q-scroll-area> </q-scroll-area>
<q-card-section v-else style="height: 100%">
<div class="row items-center" style="height: 100%">
<div class="col-12">
<div class="block-empty"></div>
<div class="q-pb-lg block-empty-logo">
<img v-if="$q.dark.isActive" alt="empty" src="~assets/middlewares-empty-dark.svg">
<img v-else alt="empty" src="~assets/middlewares-empty.svg">
</div>
<div class="block-empty-label">There is no<br>Server available</div>
</div>
</div>
</q-card-section>
</q-card> </q-card>
</template> </template>
@ -121,6 +133,19 @@ export default {
letter-spacing: normal; letter-spacing: normal;
text-transform: none; text-transform: none;
} }
.block-empty {
&-logo {
text-align: center;
}
&-label {
font-size: 20px;
font-weight: 700;
color: #b8b8b8;
text-align: center;
line-height: 1.2;
}
}
} }
</style> </style>

View file

@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div v-if="serviceByName.item.loadBalancer && serviceByName.item.loadBalancer.servers" class="col-12 col-md-4 q-mb-lg path-block"> <div v-if="serviceByName.item.loadBalancer" class="col-12 col-md-4 q-mb-lg path-block">
<div class="row no-wrap items-center q-mb-lg app-title"> <div class="row no-wrap items-center q-mb-lg app-title">
<q-icon name="eva-globe-outline"></q-icon> <q-icon name="eva-globe-outline"></q-icon>
<div class="app-title-label">Servers</div> <div class="app-title-label">Servers</div>