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
```
### `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
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`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`--providers.kubernetesingress.allowemptyservices`:
Allow creation of services without endpoints. (Default: ```false```)
`--providers.kubernetesingress.certauthfilepath`:
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`:
Enable Kubernetes backend with default settings. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_ALLOWEMPTYSERVICES`:
Allow creation of services without endpoints. (Default: ```false```)
`TRAEFIK_PROVIDERS_KUBERNETESINGRESS_CERTAUTHFILEPATH`:
Kubernetes certificate authority file path (not needed for in-cluster client).

View file

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

View file

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

View file

@ -13,3 +13,4 @@
address = ":8000"
[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.
type Provider struct {
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"`
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"`
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"`
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
lastConfiguration safe.Safe
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"`
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"`
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"`
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"`
lastConfiguration safe.Safe
}
// EndpointIngress holds the endpoint information for the Kubernetes provider.
@ -241,6 +242,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
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{
Rule: "PathPrefix(`/`)",
Priority: math.MinInt32,
@ -278,6 +287,14 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
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
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")
}
if len(endpoints.Subsets) == 0 {
return nil, errors.New("subset not found")
}
var port int32
for _, subset := range endpoints.Subsets {
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
}

View file

@ -24,10 +24,11 @@ func Bool(v bool) *bool { return &v }
func TestLoadConfigurationFromIngresses(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
serverVersion string
expected *dynamic.Configuration
desc string
ingressClass string
serverVersion string
expected *dynamic.Configuration
allowEmptyServices bool
}{
{
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{
TCP: &dynamic.TCPConfiguration{},
HTTP: &dynamic.HTTPConfiguration{
Middlewares: map[string]*dynamic.Middleware{},
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
Routers: map[string]*dynamic.Router{
"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...)
p := Provider{IngressClass: test.ingressClass}
p := Provider{IngressClass: test.ingressClass, AllowEmptyServices: test.allowEmptyServices}
conf := p.loadConfigurationFromIngresses(context.Background(), clientMock)
assert.Equal(t, test.expected, conf)

View file

@ -1,6 +1,6 @@
<template>
<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>
<div class="row items-start no-wrap">
<div class="col-3" v-if="showStatus">
@ -33,6 +33,18 @@
<q-separator />
</div>
</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>
</template>
@ -121,6 +133,19 @@ export default {
letter-spacing: normal;
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>

View file

@ -45,7 +45,7 @@
</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">
<q-icon name="eva-globe-outline"></q-icon>
<div class="app-title-label">Servers</div>