diff --git a/docs/content/middlewares/errorpages.md b/docs/content/middlewares/errorpages.md index e3ae0c110..f1a56dd2c 100644 --- a/docs/content/middlewares/errorpages.md +++ b/docs/content/middlewares/errorpages.md @@ -29,8 +29,10 @@ spec: errors: status: - 500-599 - service: serviceError query: /{status}.html + service: + name: whoami + port: 80 ``` ```json tab="Marathon" @@ -95,6 +97,9 @@ The status code ranges are inclusive (`500-599` will trigger with every code bet The service that will serve the new requested error page. +!!! Note + In kubernetes, you need to reference a kubernetes service instead of a traefik service. + ### `query` The URL for the error page (hosted by `service`). You can use `{status}` in the query, that will be replaced by the received status code. diff --git a/pkg/provider/kubernetes/crd/fixtures/with_error_page.yml b/pkg/provider/kubernetes/crd/fixtures/with_error_page.yml new file mode 100644 index 000000000..3eea37acd --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_error_page.yml @@ -0,0 +1,15 @@ +apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: errorpage + namespace: default + +spec: + errors: + status: + - "404" + - "500" + query: query + service: + name: whoami + port: 80 diff --git a/pkg/provider/kubernetes/crd/kubernetes.go b/pkg/provider/kubernetes/crd/kubernetes.go index c8fbd61f2..c35a82ead 100644 --- a/pkg/provider/kubernetes/crd/kubernetes.go +++ b/pkg/provider/kubernetes/crd/kubernetes.go @@ -170,6 +170,18 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) continue } + errorPage, errorPageService, err := createErrorPageMiddleware(client, middleware.Namespace, middleware.Spec.Errors) + if err != nil { + log.FromContext(ctxMid).Errorf("Error while reading error page middleware: %v", err) + continue + } + + if errorPage != nil && errorPageService != nil { + serviceName := id + "-errorpage-service" + errorPage.Service = serviceName + conf.HTTP.Services[serviceName] = errorPageService + } + conf.HTTP.Middlewares[id] = &dynamic.Middleware{ AddPrefix: middleware.Spec.AddPrefix, StripPrefix: middleware.Spec.StripPrefix, @@ -179,7 +191,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) Chain: createChainMiddleware(ctxMid, middleware.Namespace, middleware.Spec.Chain), IPWhiteList: middleware.Spec.IPWhiteList, Headers: middleware.Spec.Headers, - Errors: middleware.Spec.Errors, + Errors: errorPage, RateLimit: middleware.Spec.RateLimit, RedirectRegex: middleware.Spec.RedirectRegex, RedirectScheme: middleware.Spec.RedirectScheme, @@ -199,6 +211,24 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client) return conf } +func createErrorPageMiddleware(client Client, namespace string, errorPage *v1alpha1.ErrorPage) (*dynamic.ErrorPage, *dynamic.Service, error) { + if errorPage == nil { + return nil, nil, nil + } + + errorPageMiddleware := &dynamic.ErrorPage{ + Status: errorPage.Status, + Query: errorPage.Query, + } + + balancerServerHTTP, err := createLoadBalancerServerHTTP(client, namespace, errorPage.Service) + if err != nil { + return nil, nil, err + } + + return errorPageMiddleware, balancerServerHTTP, nil +} + func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *v1alpha1.ForwardAuth) (*dynamic.ForwardAuth, error) { if auth == nil { return nil, nil diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 037191e11..bbadee6a4 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -64,7 +64,7 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli serviceName := makeID(ingressRoute.Namespace, key) for _, service := range route.Services { - balancerServerHTTP, err := createLoadBalancerServerHTTP(client, ingressRoute, service) + balancerServerHTTP, err := createLoadBalancerServerHTTP(client, ingressRoute.Namespace, service) if err != nil { logger. WithField("serviceName", service.Name). @@ -151,8 +151,8 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli return conf } -func createLoadBalancerServerHTTP(client Client, ingressRoute *v1alpha1.IngressRoute, service v1alpha1.Service) (*dynamic.Service, error) { - servers, err := loadServers(client, ingressRoute.Namespace, service) +func createLoadBalancerServerHTTP(client Client, namespace string, service v1alpha1.Service) (*dynamic.Service, error) { + servers, err := loadServers(client, namespace, service) if err != nil { return nil, err } @@ -181,7 +181,7 @@ func loadServers(client Client, namespace string, svc v1alpha1.Service) ([]dynam } if !exists { - return nil, errors.New("service not found") + return nil, fmt.Errorf("service not found %s/%s", namespace, svc.Name) } var portSpec *corev1.ServicePort diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index be7a3adef..6dc17b5e0 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -1498,6 +1498,44 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "Simple Ingress Route, with error page middleware", + paths: []string{"services.yml", "with_error_page.yml"}, + expected: &dynamic.Configuration{ + TLS: &dynamic.TLSConfiguration{}, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Services: map[string]*dynamic.TCPService{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{}, + Middlewares: map[string]*dynamic.Middleware{ + "default/errorpage": { + Errors: &dynamic.ErrorPage{ + Status: []string{"404", "500"}, + Service: "default/errorpage-errorpage-service", + Query: "query", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default/errorpage-errorpage-service": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: true, + }, + }, + }, + }, + }, + }, { desc: "port selected by name (TODO)", }, @@ -1506,6 +1544,9 @@ func TestLoadIngressRoutes(t *testing.T) { for _, test := range testCases { test := test + if test.desc != "Simple Ingress Route, with error page middleware" { + continue + } t.Run(test.desc, func(t *testing.T) { t.Parallel() diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go index 486f8987f..f87ba4b64 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/middleware.go @@ -28,7 +28,7 @@ type MiddlewareSpec struct { Chain *Chain `json:"chain,omitempty"` IPWhiteList *dynamic.IPWhiteList `json:"ipWhiteList,omitempty"` Headers *dynamic.Headers `json:"headers,omitempty"` - Errors *dynamic.ErrorPage `json:"errors,omitempty"` + Errors *ErrorPage `json:"errors,omitempty"` RateLimit *dynamic.RateLimit `json:"rateLimit,omitempty"` RedirectRegex *dynamic.RedirectRegex `json:"redirectRegex,omitempty"` RedirectScheme *dynamic.RedirectScheme `json:"redirectScheme,omitempty"` @@ -45,6 +45,15 @@ type MiddlewareSpec struct { // +k8s:deepcopy-gen=true +// ErrorPage holds the custom error page configuration. +type ErrorPage struct { + Status []string `json:"status,omitempty"` + Service Service `json:"service,omitempty"` + Query string `json:"query,omitempty"` +} + +// +k8s:deepcopy-gen=true + // Chain holds a chain of middlewares type Chain struct { Middlewares []MiddlewareRef `json:"middlewares,omitempty"` diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index b51b8c394..813b8a6b5 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -124,6 +124,28 @@ func (in *DigestAuth) DeepCopy() *DigestAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ErrorPage) DeepCopyInto(out *ErrorPage) { + *out = *in + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ErrorPage. +func (in *ErrorPage) DeepCopy() *ErrorPage { + if in == nil { + return nil + } + out := new(ErrorPage) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) { *out = *in @@ -480,7 +502,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) { } if in.Errors != nil { in, out := &in.Errors, &out.Errors - *out = new(dynamic.ErrorPage) + *out = new(ErrorPage) (*in).DeepCopyInto(*out) } if in.RateLimit != nil {