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:
parent
306910d956
commit
55679aa268
9 changed files with 1945 additions and 148 deletions
|
@ -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).
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
1830
images/custom-error-pages/rootfs/etc/mime.types
Normal file
1830
images/custom-error-pages/rootfs/etc/mime.types
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue