Skip to content

Commit

Permalink
Go templates support, XML, JSON, Ingress (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarampampam authored Jan 27, 2022
1 parent f75bf15 commit bed576f
Show file tree
Hide file tree
Showing 76 changed files with 2,184 additions and 970 deletions.
39 changes: 30 additions & 9 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,24 @@ jobs: # Docs: <https://git.io/JvxXE>
- name: Run linter
uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action>
with:
version: v1.42 # without patch version
version: v1.44 # without patch version
only-new-issues: false # show only new issues if it's a pull request

validate-config-file:
name: Validate config file
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2

- uses: actions/setup-node@v2
with: {node-version: '16'}

- name: Install linter
run: npm install -g ajv-cli # Package page: <https://www.npmjs.com/package/ajv-cli>

- name: Run linter
run: ajv validate --all-errors --verbose -s ./schemas/config/1.0.schema.json -d ./error-pages.y*ml

go-test:
name: Unit tests
runs-on: ubuntu-20.04
Expand Down Expand Up @@ -68,7 +83,7 @@ jobs: # Docs: <https://git.io/JvxXE>
matrix:
os: [linux, darwin] # linux, freebsd, darwin, windows
arch: [amd64] # amd64, 386
needs: [golangci-lint, go-test]
needs: [golangci-lint, go-test, validate-config-file]
steps:
- uses: actions/setup-go@v2
with: {go-version: 1.17}
Expand Down Expand Up @@ -139,7 +154,7 @@ jobs: # Docs: <https://git.io/JvxXE>
docker-image:
name: Build docker image
runs-on: ubuntu-20.04
needs: [golangci-lint, go-test]
needs: [golangci-lint, go-test, validate-config-file]
steps:
- uses: actions/checkout@v2

Expand Down Expand Up @@ -187,6 +202,8 @@ jobs: # Docs: <https://git.io/JvxXE>
needs: [docker-image]
timeout-minutes: 2
steps:
- uses: actions/checkout@v2

- uses: actions/download-artifact@v2
with:
name: docker-image
Expand All @@ -195,17 +212,21 @@ jobs: # Docs: <https://git.io/JvxXE>
- working-directory: .artifact
run: docker load < docker-image.tar

- name: Download hurl
env:
VERSION: 1.5.0
run: curl -SL -o hurl.deb https://github.com/Orange-OpenSource/hurl/releases/download/${VERSION}/hurl_${VERSION}_amd64.deb

- name: Install hurl
run: sudo dpkg -i hurl.deb

- name: Run container with the app
run: docker run --rm -d -p "8080:8080/tcp" -e "DEFAULT_HTTP_CODE=401" --name app app:ci
run: docker run --rm -d -p "8080:8080/tcp" -e "SHOW_DETAILS=true" --name app app:ci

- name: Wait for container "healthy" state
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done

- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 401
- run: curl --fail http://127.0.0.1:8080/500.html
- run: curl --fail http://127.0.0.1:8080/400.html
- run: curl --fail http://127.0.0.1:8080/health/live
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/foobar) -eq 404
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 --summary ./test/hurl/*.hurl

- name: Stop the container
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/.vscode

## Binaries
error-pages
/error-pages

## Temp dirs & trash
/temp
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this package will be documented in this file.

The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].

## UNRELEASED

### Changed

- It is now possible to use [golang-tags of templates](https://pkg.go.dev/text/template) in error page templates and formatted (`json`, `xml`) responses
- Health-check route become `/healthz` (instead `/health/live`, previous route marked ad deprecated)

### Added

- The templates contain details block now (can be enabled using `--show-details` flag for the `serve` command or environment variable `SHOW_DETAILS=true`)
- Formatted response templates (`json`, `xml`) - the server responds with a formatted response depending on the `Content-Type` (and `X-Format`) request header value
- HTTP header `X-Robots-Tag: noindex` for the error pages
- Possibility to pass the needed error page code using `X-Code` HTTP header
- Possibility to integrate with [ingress-nginx](https://kubernetes.github.io/ingress-nginx/)

### Fixed

- Potential race condition (in the `pick.StringsSlice` struct)

## v2.3.0

### Added
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ RUN set -x \
&& echo 'appuser:x:10001:' > ./etc/group \
&& mv /src/error-pages ./bin/error-pages \
&& mv /src/templates ./opt/templates \
&& rm ./opt/templates/*.md \
&& mv /src/error-pages.yml ./opt/error-pages.yml

WORKDIR /tmp/rootfs/opt
Expand Down Expand Up @@ -66,12 +67,11 @@ WORKDIR /opt
ENV LISTEN_PORT="8080" \
TEMPLATE_NAME="ghost" \
DEFAULT_ERROR_PAGE="404" \
DEFAULT_HTTP_CODE="404"
DEFAULT_HTTP_CODE="404" \
SHOW_DETAILS="false"

# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
HEALTHCHECK --interval=7s --timeout=2s CMD [ \
"/bin/error-pages", "healthcheck", "--log-json" \
]
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "healthcheck", "--log-json"]

ENTRYPOINT ["/bin/error-pages"]

Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DC_RUN_ARGS = --rm --user "$(shell id -u):$(shell id -g)"
APP_NAME = $(notdir $(CURDIR))

.PHONY : help \
image dive build fmt lint gotest test shell \
image dive build fmt lint gotest int-test test shell \
up down restart \
clean
.DEFAULT_GOAL : help
Expand Down Expand Up @@ -42,7 +42,10 @@ lint: ## Run app linters
gotest: ## Run app tests
docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./...

test: lint gotest ## Run app tests and linters
int-test: ## Run integration tests (docs: https://hurl.dev/docs/man-page.html#options)
docker-compose run --rm hurl --color --test --fail-at-end --variable host=web --variable port=8080 --summary ./test/hurl/*.hurl

test: lint gotest int-test ## Run app tests and linters

shell: ## Start shell into container with golang
docker-compose run $(DC_RUN_ARGS) app bash
Expand Down
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ One day you may want to replace the standard error pages of your HTTP server wit
- Simple error pages generator, written on Go
- Single-page error page templates with different designs (located in the [templates](templates) directory)
- Fast and lightweight HTTP server (written on Go also, with the [FastHTTP][fasthttp] under the hood)
- HTTP server respects the `Content-Type` HTTP header (and `X-Format`) value and responds with the corresponding format
- Already generated error pages (sources can be [found here][preview-sources], the **demonstration** is always accessible [here][preview-demo])
- Lightweight docker image _(~3.5Mb compressed size)_ with all the things described above
- Lightweight docker image _(~3.7Mb compressed size)_ with all the things described above
- Ready to integrate with the Traefik and Ingress-nginx

Also, this project can be used for the [**Traefik** error pages customization](https://doc.traefik.io/traefik/middlewares/http/errorpages/).

Expand All @@ -30,10 +32,10 @@ Also, this project can be used for the [**Traefik** error pages customization](h

Download the latest binary file for your os/arch from the [releases page][link_releases] or use our docker image:

Registry | Image
-------------------------------------- | -----
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
| Registry | Image |
|--------------------------------------------|-----------------------------------|
| [Docker Hub][link_docker_hub] | `tarampampam/error-pages` |
| [GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages` |

> Using the `latest` tag for the docker image is highly discouraged because of possible backward-incompatible changes during **major** upgrades. Please, use tags in `X.Y.Z` format
Expand All @@ -56,21 +58,21 @@ $ docker run --rm -it \

## Templates

Name | Preview
:---------------: | :-----:
`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
`cats` | [![cats](https://hsto.org/webt/_g/y-/ke/_gy-keqinz-3867jbw36v37-iwe.jpeg)](https://tarampampam.github.io/error-pages/cats/404.html)
| Name | Preview |
|:-----------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------:|
| `ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html) |
| `l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html) |
| `l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html) |
| `shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html) |
| `noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html) |
| `hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html) |
| `cats` | [![cats](https://hsto.org/webt/_g/y-/ke/_gy-keqinz-3867jbw36v37-iwe.jpeg)](https://tarampampam.github.io/error-pages/cats/404.html) |

> Note: `noise` template highly uses the CPU, be careful
## Usage

All of the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.
All the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.

<details>
<summary><strong>HTTP server</strong></summary>
Expand All @@ -86,7 +88,11 @@ $ docker run --rm \
tarampampam/error-pages
```

And open [`http://127.0.0.1:8080/404.html`](http://127.0.0.1:8080/404.html) in your favorite browser. Error pages are accessible by the following URLs: `http://127.0.0.1:8080/{page_code}.html`.
And open [`http://127.0.0.1:8080/404.html`](http://127.0.0.1:8080/404.html) in your favorite browser. Error pages are accessible by the following URLs: `http://127.0.0.1:8080/{page_code}.html`. Another way is to pass the needed error page code (and HTTP response code) using the HTTP header `X-Code` when requesting an index page.

Additionally, the server respects the `Content-Type` HTTP header (and `X-Format`) value and responds with the corresponding format instead of the HTML-formatted response only. The `xml` and `json` content types (formats) are allowed at this moment, and its format can be fully customized using a configuration file!

For the integration with [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) you can start the server with the flag `--show-details` (environment variable `SHOW_DETAILS=true`) - in this case, the error pages (`json`, `xml` responses too) will contain additional information that passed from the upstream reverse proxy!

Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).

Expand Down
41 changes: 41 additions & 0 deletions cmd/error-pages/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

import (
"os"
"testing"

"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
)

func Test_Main(t *testing.T) {
os.Args = []string{"", "--help"}
exitFn = func(code int) { assert.Equal(t, 0, code) }

output := capturer.CaptureStdout(main)

assert.Contains(t, output, "Usage:")
assert.Contains(t, output, "Available Commands:")
assert.Contains(t, output, "Flags:")
}

func Test_MainWithoutCommands(t *testing.T) {
os.Args = []string{""}
exitFn = func(code int) { assert.Equal(t, 0, code) }

output := capturer.CaptureStdout(main)

assert.Contains(t, output, "Usage:")
assert.Contains(t, output, "Available Commands:")
assert.Contains(t, output, "Flags:")
}

func Test_MainUnknownSubcommand(t *testing.T) {
os.Args = []string{"", "foobar"}
exitFn = func(code int) { assert.Equal(t, 1, code) }

output := capturer.CaptureStderr(main)

assert.Contains(t, output, "unknown command")
assert.Contains(t, output, "foobar")
}
14 changes: 12 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,27 @@ services:
- serve
- --verbose
- --port=8080
- --show-details
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/health/live']
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz']
interval: 5s
timeout: 2s

golint:
image: golangci/golangci-lint:v1.42-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
image: golangci/golangci-lint:v1.44-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
environment:
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
volumes:
- .:/src:ro
- golint-cache:/tmp/golint:rw
working_dir: /src
command: /bin/true

hurl:
image: orangeopensource/hurl:1.5.0
volumes:
- .:/src:ro
working_dir: /src
depends_on:
web:
condition: service_healthy
37 changes: 37 additions & 0 deletions error-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,43 @@ templates:
- path: ./templates/hacker-terminal.html
- path: ./templates/cats.html

formats:
json:
content: |
{
"error": true,
"code": {{ code | json }},
"message": {{ message | json }},
"description": {{ description | json }}{{ if show_details }},
"details": {
"original_uri": {{ original_uri | json }},
"namespace": {{ namespace | json }},
"ingress_name": {{ ingress_name | json }},
"service_name": {{ service_name | json }},
"service_port": {{ service_port | json }},
"request_id": {{ request_id | json }},
"timestamp": {{ now.Unix }}
}{{ end }}
}
xml:
content: |
<?xml version="1.0" encoding="utf-8"?>
<error>
<code>{{ code }}</code>
<message>{{ message }}</message>
<description>{{ description }}</description>{{ if show_details }}
<details>
<originalURI>{{ original_uri }}</originalURI>
<namespace>{{ namespace }}</namespace>
<ingressName>{{ ingress_name }}</ingressName>
<serviceName>{{ service_name }}</serviceName>
<servicePort>{{ service_port }}</servicePort>
<requestID>{{ request_id }}</requestID>
<timestamp>{{ now.Unix }}</timestamp>
</details>{{ end }}
</error>
pages:
400:
message: Bad Request
Expand Down
4 changes: 4 additions & 0 deletions internal/breaker/os_signal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
)

func TestNewOSSignals(t *testing.T) {
t.Parallel()

oss := breaker.NewOSSignals(context.Background())

gotSignal := make(chan os.Signal, 1)
Expand All @@ -33,6 +35,8 @@ func TestNewOSSignals(t *testing.T) {
}

func TestNewOSSignalCtxCancel(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(context.Background())

oss := breaker.NewOSSignals(ctx)
Expand Down
2 changes: 1 addition & 1 deletion internal/checkers/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func NewHealthChecker(ctx context.Context, client ...httpClient) *HealthChecker

// Check application using liveness probe.
func (c *HealthChecker) Check(port uint16) error {
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/health/live", port), nil) //nolint:lll
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/healthz", port), nil) //nolint:lll
if err != nil {
return err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/checkers/health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ type httpClientFunc func(*http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }

func TestHealthChecker_CheckSuccess(t *testing.T) {
t.Parallel()

var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "http://127.0.0.1:123/health/live", req.URL.String())
assert.Equal(t, "http://127.0.0.1:123/healthz", req.URL.String())
assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent"))

return &http.Response{
Expand All @@ -33,6 +35,8 @@ func TestHealthChecker_CheckSuccess(t *testing.T) {
}

func TestHealthChecker_CheckFail(t *testing.T) {
t.Parallel()

var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
Expand Down
Loading

0 comments on commit bed576f

Please sign in to comment.