diff --git a/docs/content/providers/kubernetes-ingress.md b/docs/content/providers/kubernetes-ingress.md index ccbde7969..a3b85e876 100644 --- a/docs/content/providers/kubernetes-ingress.md +++ b/docs/content/providers/kubernetes-ingress.md @@ -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, diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 55ed8220c..4d8628cc7 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -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). diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index dc291158c..85cbe0106 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -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). diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 84c5c97f5..361b8ef3f 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -106,6 +106,7 @@ labelSelector = "foobar" ingressClass = "foobar" throttleDuration = "42s" + allowEmptyServices = true [providers.kubernetesIngress.ingressEndpoint] ip = "foobar" hostname = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 6a550e236..fa49b9db3 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -114,6 +114,7 @@ providers: labelSelector: foobar ingressClass: foobar throttleDuration: 42s + allowEmptyServices: true ingressEndpoint: ip: foobar hostname: foobar diff --git a/integration/fixtures/k8s_default.toml b/integration/fixtures/k8s_default.toml index 3c0d9e790..f94ecf45b 100644 --- a/integration/fixtures/k8s_default.toml +++ b/integration/fixtures/k8s_default.toml @@ -13,3 +13,4 @@ address = ":8000" [providers.kubernetesIngress] + allowEmptyServices = true diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-one-service-without-endpoint_ingress.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-one-service-without-endpoint_ingress.yml deleted file mode 100644 index 4e27d7f07..000000000 --- a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-one-service-without-endpoint_ingress.yml +++ /dev/null @@ -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 diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index 31fe71aa0..778067c0e 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -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 } diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 66d05c9f5..663cec314 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -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) diff --git a/webui/src/components/_commons/PanelServers.vue b/webui/src/components/_commons/PanelServers.vue index dcd89b725..78f148f89 100644 --- a/webui/src/components/_commons/PanelServers.vue +++ b/webui/src/components/_commons/PanelServers.vue @@ -1,6 +1,6 @@ @@ -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; + } + } } diff --git a/webui/src/pages/_commons/ServiceDetail.vue b/webui/src/pages/_commons/ServiceDetail.vue index 68c158bbc..0fc9b40ef 100644 --- a/webui/src/pages/_commons/ServiceDetail.vue +++ b/webui/src/pages/_commons/ServiceDetail.vue @@ -45,7 +45,7 @@ -
+
Servers