Update documentation for custom error pages (#2630)

* Update documentation for custom error pages

* custom-error-pages: Fix path to error pages

Bump version to 0.2
This commit is contained in:
Antoine Cotten 2018-06-13 00:03:48 +02:00 committed by Manuel Alejandro de Brito Fontes
parent 306910d956
commit 55679aa268
9 changed files with 1945 additions and 148 deletions

View file

@ -1,82 +1,83 @@
# Custom Errors # Custom Errors
This example shows how is possible to use a custom backend to render custom error pages. The code of this example is located here [custom-error-pages](https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/customization/custom-errors) This example demonstrates how to use a custom backend to render custom error pages.
## Customized default backend
The idea is to use the headers `X-Code` and `X-Format` that NGINX pass to the backend in case of an error to find out the best existent representation of the response to be returned. i.e. if the request contains an `Accept` header of type `json` the error should be in that format and not in `html` (the default in NGINX). First, create the custom `default-backend`. It will be used by the Ingress controller later on.
First create the custom backend to use in the Ingress controller
``` ```
$ kubectl create -f custom-default-backend.yaml $ kubectl create -f custom-default-backend.yaml
service "nginx-errors" created service "nginx-errors" created
replicationcontroller "nginx-errors" created deployment.apps "nginx-errors" created
``` ```
``` This should have created a Deployment and a Service with the name `nginx-errors`.
$ kubectl get svc
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE
echoheaders 10.3.0.7 nodes 80/TCP 23d
kubernetes 10.3.0.1 <none> 443/TCP 34d
nginx-errors 10.3.0.102 <none> 80/TCP 11s
```
``` ```
$ kubectl get rc $ kubectl get deploy,svc
CONTROLLER REPLICAS AGE NAME DESIRED CURRENT READY AGE
echoheaders 1 19d deployment.apps/nginx-errors 1 1 1 10s
nginx-errors 1 19s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginx-errors ClusterIP 10.0.0.12 <none> 80/TCP 10s
``` ```
Next create the Ingress controller executing ## Ingress controller configuration
```
$ kubectl create -f rc-custom-errors.yaml
```
Now to check if this is working we use curl: If you do not already have an instance of the the NGINX Ingress controller running, deploy it according to the
[deployment guide][deploy], then follow these steps:
1. Edit the `nginx-ingress-controller` Deployment and set the value of the `--default-backend` flag to the name of the
newly created error backend.
2. Edit the `nginx-configuration` ConfigMap and create the key `custom-http-errors` with a value of `404,503`.
3. Take note of the IP address assigned to the NGINX Ingress controller Service.
```
$ kubectl get svc ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx ClusterIP 10.0.0.13 <none> 80/TCP,443/TCP 10m
```
!!! Note
The `ingress-nginx` Service is of type `ClusterIP` in this example. This may vary depending on your environment.
Make sure you can use the Service to reach NGINX before proceeding with the rest of this example.
[deploy]: ../../../deploy/
## Testing error pages
Let us send a couple of HTTP requests using cURL and validate everything is working as expected.
A request to the default backend returns a 404 error with a custom message:
``` ```
$ curl -v http://172.17.4.99/ $ curl -D- http://10.0.0.13/
* Trying 172.17.4.99... HTTP/1.1 404 Not Found
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0) Server: nginx/1.13.12
> GET / HTTP/1.1 Date: Tue, 12 Jun 2018 19:11:24 GMT
> Host: 172.17.4.99 Content-Type: */*
> User-Agent: curl/7.43.0 Transfer-Encoding: chunked
> Accept: */* Connection: keep-alive
>
< HTTP/1.1 404 Not Found
< Server: nginx/1.10.0
< Date: Wed, 04 May 2016 02:53:45 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< Vary: Accept-Encoding
<
<span>The page you're looking for could not be found.</span> <span>The page you're looking for could not be found.</span>
* Connection #0 to host 172.17.4.99 left intact
``` ```
Specifying json as expected format: A request with a custom `Accept` header returns the corresponding document type (JSON):
``` ```
$ curl -v http://172.17.4.99/ -H 'Accept: application/json' $ curl -D- -H 'Accept: application/json' http://10.0.0.13/
* Trying 172.17.4.99... HTTP/1.1 404 Not Found
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0) Server: nginx/1.13.12
> GET / HTTP/1.1 Date: Tue, 12 Jun 2018 19:12:36 GMT
> Host: 172.17.4.99 Content-Type: application/json
> User-Agent: curl/7.43.0 Transfer-Encoding: chunked
> Accept: application/json Connection: keep-alive
> Vary: Accept-Encoding
< HTTP/1.1 404 Not Found
< Server: nginx/1.10.0
< Date: Wed, 04 May 2016 02:54:00 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< Vary: Accept-Encoding
<
{ "message": "The page you're looking for could not be found" } { "message": "The page you're looking for could not be found" }
* Connection #0 to host 172.17.4.99 left intact
``` ```
To go further with this example, feel free to deploy your own applications and Ingress objects, and validate that the
responses are still in the correct format when a backend returns 503 (eg. if you scale a Deployment down to 0 replica).

View file

@ -1,3 +1,4 @@
---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
@ -5,27 +6,30 @@ metadata:
labels: labels:
app: nginx-errors app: nginx-errors
spec: spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector: selector:
app: nginx-errors app: nginx-errors
ports:
- port: 80
targetPort: 8080
name: http
--- ---
apiVersion: v1 apiVersion: apps/v1beta2
kind: ReplicationController kind: Deployment
apiVersion: apps/v1beta2
metadata: metadata:
name: nginx-errors name: nginx-errors
spec: spec:
replicas: 1 replicas: 1
selector:
matchLabels:
app: nginx-errors
template: template:
metadata: metadata:
labels: labels:
app: nginx-errors app: nginx-errors
spec: spec:
containers: containers:
- name: nginx-errors - name: nginx-error-server
image: aledbf/nginx-error-server:0.1 image: quay.io/kubernetes-ingress-controller/custom-error-pages-amd64:0.2
ports: ports:
- containerPort: 80 - containerPort: 8080

View file

@ -1,53 +0,0 @@
apiVersion: v1
kind: ReplicationController
metadata:
name: nginx-ingress-controller
labels:
k8s-app: nginx-ingress-lb
spec:
replicas: 1
selector:
k8s-app: nginx-ingress-lb
template:
metadata:
labels:
k8s-app: nginx-ingress-lb
name: nginx-ingress-lb
spec:
terminationGracePeriodSeconds: 60
containers:
- image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.15.0
name: nginx-ingress-lb
imagePullPolicy: Always
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
# use downward API
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/nginx-errors
securityContext:
runAsNonRoot: false

View file

@ -13,7 +13,7 @@ Auth | [OAuth external auth](auth/oauth-external-auth/README.md) | TODO | TODO
Customization | [Configuration snippets](customization/configuration-snippets/README.md) | customize nginx location configuration using annotations | Advanced Customization | [Configuration snippets](customization/configuration-snippets/README.md) | customize nginx location configuration using annotations | Advanced
Customization | [Custom configuration](customization/custom-configuration/README.md) | TODO | TODO Customization | [Custom configuration](customization/custom-configuration/README.md) | TODO | TODO
Customization | [Custom DH parameters for perfect forward secrecy](customization/ssl-dh-param/README.md) | TODO | TODO Customization | [Custom DH parameters for perfect forward secrecy](customization/ssl-dh-param/README.md) | TODO | TODO
Customization | [Custom errors](customization/custom-errors/README.md) | TODO | TODO Customization | [Custom errors](customization/custom-errors/README.md) | serve custom error pages from the default backend | Intermediate
Customization | [Custom headers](customization/custom-headers/README.md) | set custom headers before sending traffic to backends | Advanced Customization | [Custom headers](customization/custom-headers/README.md) | set custom headers before sending traffic to backends | Advanced
Customization | [Custom upstream check](customization/custom-upstream-check/README.md) | TODO | TODO Customization | [Custom upstream check](customization/custom-upstream-check/README.md) | TODO | TODO
Customization | [Custom VTS metrics with Prometheus](customization/custom-vts-metrics-prometheus/README.md) | TODO | TODO Customization | [Custom VTS metrics with Prometheus](customization/custom-vts-metrics-prometheus/README.md) | TODO | TODO

View file

@ -1,19 +1,30 @@
# Custom errors # Custom errors
In case of an error in a request the body of the response is obtained from the `default backend`. When the [`custom-http-errors`][cm-custom-http-errors] option is enabled, the Ingress controller configures NGINX so
Each request to the default backend includes two headers: that it passes several HTTP headers down to its `default-backend` in case of error:
- `X-Code` indicates the HTTP code to be returned to the client. | Header | Value |
- `X-Format` the value of the `Accept` header. | ---------------- | ------------------------------------------------ |
| `X-Code` | HTTP status code retuned by the request |
| `X-Format` | Value of the `Accept` header sent by the client |
| `X-Original-URI` | URI that caused the error |
| `X-Namespace` | Namespace where the backend Service is located |
| `X-Ingress-Name` | Name of the Ingress where the backend is defined |
| `X-Service-Name` | Name of the Service backing the backend |
| `X-Service-Port` | Port number of the Service backing the backend |
A custom error backend can use this information to return the best possible representation of an error page. For
example, if the value of the `Accept` header send by the client was `application/json`, a carefully crafted backend
could decide to return the error payload as a JSON document instead of HTML.
!!! Important !!! Important
The custom backend must return the correct HTTP status code to be returned. NGINX does not change the response from the custom default backend. The custom backend is expected to return the correct HTTP status code instead of `200`. NGINX does not change
the response from the custom default backend.
Using these two headers it's possible to use a custom backend service like [this one](https://github.com/kubernetes/ingress-nginx/tree/master/images/custom-error-pages) that inspects each request and returns a custom error page with the format expected by the client. Please check the example [custom-errors](https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/customization/custom-errors). An example of such custom backend is available inside the source repository at [images/custom-error-pages][img-custom-error-pages].
NGINX sends additional headers that can be used to build custom response: See also the [Custom errors][example-custom-errors] example.
- X-Original-URI [cm-custom-http-errors]: ./nginx-configuration/configmap.md#custom-http-errors
- X-Namespace [img-custom-error-pages]: https://github.com/kubernetes/ingress-nginx/tree/master/images/custom-error-pages
- X-Ingress-Name [example-custom-errors]: ../examples/customization/custom-errors
- X-Service-Name

View file

@ -3,7 +3,7 @@ all: all-container
BUILDTAGS= BUILDTAGS=
# Use the 0.0 tag for testing, it shouldn't clobber any release builds # Use the 0.0 tag for testing, it shouldn't clobber any release builds
TAG?=0.1 TAG?=0.2
REGISTRY?=quay.io/kubernetes-ingress-controller REGISTRY?=quay.io/kubernetes-ingress-controller
GOOS?=linux GOOS?=linux
DOCKER?=docker DOCKER?=docker

View file

@ -1,2 +1,3 @@
# custom-error-pages
Example of Custom error pages for the NGINX Ingress controller Example of Custom error pages for the NGINX Ingress controller

View file

@ -39,15 +39,19 @@ const (
// ContentType name of the header that defines the format of the reply // ContentType name of the header that defines the format of the reply
ContentType = "Content-Type" ContentType = "Content-Type"
// ErrFilesPathVar is the name of the environment variable indicating
// the location on disk of files served by the handler.
ErrFilesPathVar = "ERROR_FILES_PATH"
) )
func main() { func main() {
path := "/www" errFilesPath := "/www"
if os.Getenv("PATH") != "" { if os.Getenv(ErrFilesPathVar) != "" {
path = os.Getenv("PATH") errFilesPath = os.Getenv(ErrFilesPathVar)
} }
http.HandleFunc("/", errorHandler(path)) http.HandleFunc("/", errorHandler(errFilesPath))
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
@ -66,15 +70,14 @@ func errorHandler(path string) func(http.ResponseWriter, *http.Request) {
format := r.Header.Get(FormatHeader) format := r.Header.Get(FormatHeader)
if format == "" { if format == "" {
format = "text/html" format = "text/html"
log.Printf("forma not specified. Using %v\n", format) log.Printf("format not specified. Using %v", format)
} }
mediaType, _, _ := mime.ParseMediaType(format) cext, err := mime.ExtensionsByType(format)
cext, err := mime.ExtensionsByType(mediaType)
if err != nil { if err != nil {
log.Printf("unexpected error reading media type extension: %v. Using %v\n", err, ext) log.Printf("unexpected error reading media type extension: %v. Using %v", err, ext)
} else if len(cext) == 0 { } else if len(cext) == 0 {
log.Printf("couldn't get media type extension. Using %v\n", ext) log.Printf("couldn't get media type extension. Using %v", ext)
} else { } else {
ext = cext[0] ext = cext[0]
} }
@ -84,7 +87,7 @@ func errorHandler(path string) func(http.ResponseWriter, *http.Request) {
code, err := strconv.Atoi(errCode) code, err := strconv.Atoi(errCode)
if err != nil { if err != nil {
code = 404 code = 404
log.Printf("unexpected error reading return code: %v. Using %v\n", err, code) log.Printf("unexpected error reading return code: %v. Using %v", err, code)
} }
w.WriteHeader(code) w.WriteHeader(code)
@ -94,22 +97,22 @@ func errorHandler(path string) func(http.ResponseWriter, *http.Request) {
file := fmt.Sprintf("%v/%v%v", path, code, ext) file := fmt.Sprintf("%v/%v%v", path, code, ext)
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
log.Printf("unexpected error opening file: %v\n", err) log.Printf("unexpected error opening file: %v", err)
scode := strconv.Itoa(code) scode := strconv.Itoa(code)
file := fmt.Sprintf("%v/%cxx%v", path, scode[0], ext) file := fmt.Sprintf("%v/%cxx%v", path, scode[0], ext)
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
log.Printf("unexpected error opening file: %v\n", err) log.Printf("unexpected error opening file: %v", err)
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
defer f.Close() defer f.Close()
log.Printf("serving custom error response for code %v and format %v from file %v\n", code, format, file) log.Printf("serving custom error response for code %v and format %v from file %v", code, format, file)
io.Copy(w, f) io.Copy(w, f)
return return
} }
defer f.Close() defer f.Close()
log.Printf("serving custom error response for code %v and format %v from file %v\n", code, format, file) log.Printf("serving custom error response for code %v and format %v from file %v", code, format, file)
io.Copy(w, f) io.Copy(w, f)
duration := time.Now().Sub(start).Seconds() duration := time.Now().Sub(start).Seconds()

File diff suppressed because it is too large Load diff