Compare commits

..

43 Commits

Author SHA1 Message Date
d672112cc2 Changelog updated 2022-01-31 13:53:06 +05:00
32b92611a7 small fixes 2022-01-31 13:45:22 +05:00
cc6cbc7d47 Template rendering performance issue has been fixed (#60) 2022-01-31 13:43:40 +05:00
690a405994 fix the template 2022-01-31 10:53:51 +05:00
f72c2b85fd Changes after merging 2022-01-31 10:46:51 +05:00
42523ae9d9 Adds "Host" and "X-Forwarded-For" header options (#61) 2022-01-31 10:40:58 +05:00
da2dc5c63a Bump github.com/fasthttp/router from 1.4.4 to 1.4.5 (#59) 2022-01-29 07:50:34 +00:00
a0a1d3caca Bump go.uber.org/zap from 1.19.1 to 1.20.0 (#58) 2022-01-29 07:50:31 +00:00
915e810088 Bump golang from 1.17.5-alpine to 1.17.6-alpine (#57) 2022-01-29 07:50:17 +00:00
00c139b525 Bump github.com/valyala/fasthttp from 1.31.0 to 1.33.0 (#56) 2022-01-29 07:46:47 +00:00
eca99eb569 Readme file updated 2022-01-29 01:14:06 +05:00
dfaeea7483 Readme file updated 2022-01-29 01:12:36 +05:00
f71b07f647 fix typos 2022-01-29 01:11:44 +05:00
be0a3c4820 Update CHANGELOG.md 2022-01-29 00:40:01 +05:00
04bf2231bc Readme file updated 2022-01-28 23:58:04 +05:00
ba98272530 Readme file updated 2022-01-28 23:23:25 +05:00
fab38255eb chore: add ingress-nginx to docs (#53) 2022-01-28 20:51:52 +05:00
88278d37a7 Prometheus metrics implemented (#54) 2022-01-28 20:42:08 +05:00
32daf80b76 Issue templates added (#55) 2022-01-28 20:41:54 +05:00
13e7a72790 Fix for the X-Format header (#51) 2022-01-28 12:53:35 +05:00
0efbccbb18 Content-type added into the logs 2022-01-27 19:28:11 +05:00
bed576f26c Go templates support, XML, JSON, Ingress (#49) 2022-01-27 17:29:49 +05:00
f75bf15552 Readme file updated 2022-01-03 22:43:47 +05:00
9915e321f4 CI updated 2022-01-03 22:07:48 +05:00
83720999d8 Changelog updated 2022-01-03 21:52:47 +05:00
79bbf3d71e Flag --default-http-code for the serve subcommand added (#44) 2022-01-03 21:51:30 +05:00
1dec69d726 Bump golang from 1.17.3-alpine to 1.17.5-alpine (#42) 2022-01-03 15:40:05 +00:00
ef2db68430 Bump github.com/spf13/cobra from 1.2.1 to 1.3.0 (#43) 2022-01-02 07:25:37 +00:00
e6f3250286 Bump golang from 1.17.2-alpine to 1.17.3-alpine (#38) 2021-12-02 11:08:52 +00:00
ca56f1dd07 Bump github.com/a8m/envsubst from 1.2.0 to 1.3.0 (#39) 2021-12-02 04:51:26 +00:00
6bd973a803 Bump golang from 1.17.1-alpine to 1.17.2-alpine (#34) 2021-11-02 06:33:04 +00:00
49dd703e12 Bump github.com/fasthttp/router from 1.4.3 to 1.4.4 (#36) 2021-11-02 06:32:13 +00:00
0f27441225 Updated: images to the latest version (#32) 2021-10-20 18:32:56 +05:00
97d76ddca8 Update README.md 2021-10-15 14:48:52 +05:00
891d491cdb Index page codes now sorted 2021-10-15 11:06:10 +05:00
2a1fb0eddf Changelog updated 2021-10-15 10:36:48 +05:00
5c25fbe2c4 Cats template updated 2021-10-15 10:32:31 +05:00
e3e618d3cf Add cat template (#31) 2021-10-15 09:55:28 +05:00
e2489a2487 Changelog updated 2021-10-06 22:38:26 +05:00
bb17027cc9 Allow to set default error page (#30) 2021-10-06 22:38:00 +05:00
6b17d3eb7d Bump github.com/fatih/color from 1.7.0 to 1.13.0 (#28) 2021-10-01 12:10:23 +00:00
c5f11eff8b Bump github.com/pkg/errors from 0.8.1 to 0.9.1 (#27) 2021-10-01 11:58:48 +00:00
b36bc5e47d Dependabot config added 2021-10-01 16:46:35 +05:00
90 changed files with 3626 additions and 1358 deletions

3
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,3 @@
# @link <https://help.github.com/en/articles/about-code-owners>
* @tarampampam

51
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# Docs: <https://git.io/JR5E4>
name: 🐞 Bug report
description: File a bug/issue
labels: ['type:bug']
assignees: [tarampampam]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Start the container using command ...
2. Send an HTTP request using this curl command ...
3. See error
- type: textarea
id: configs
attributes:
label: Configuration files
description: Please copy and paste any relevant configuration files. This will be automatically formatted into code (yaml), so no need for backticks.
render: yaml
placeholder: Traefik, docker-compose, helm, etc.
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code (shell), so no need for backticks.
render: shell
- type: textarea
attributes:
label: Anything else?
description: Links? References? Anything that will give us more context about the issue you are encountering!
placeholder: You can attach images or log files by clicking this area to highlight it and then dragging files in

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
# Docs: <https://git.io/JP3tm>
blank_issues_enabled: false
contact_links:
- name: 🗣 Ask a Question, Discuss
url: https://github.com/tarampampam/error-pages/discussions
about: Feel free to ask anything

View File

@ -0,0 +1,33 @@
# Docs: <https://git.io/JR5E4>
name: 💡 Feature request
description: Suggest an idea for this project
labels: ['type:feature_request']
assignees: [tarampampam]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Describe the problem to be solved
description: Please present a concise description of the problem to be addressed by this feature request
validations:
required: true
- type: textarea
attributes:
label: Suggest a solution
description: A concise description of your preferred solution
placeholder: If there are multiple solutions, please present each one separately
- type: textarea
attributes:
label: Additional context
description: Add any other context about the feature request
placeholder: You can attach images or log files by clicking this area to highlight it and then dragging files in

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]
- package-ecosystem: github-actions
directory: /
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]
- package-ecosystem: docker
directory: /
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]

19
.github/workflows/documentation.yml vendored Normal file
View File

@ -0,0 +1,19 @@
name: documentation
on:
push:
branches: [master, main]
paths: ['README.md']
jobs:
docker-hub-description:
name: Docker Hub Description
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: peter-evans/dockerhub-description@v2 # Action page: <https://github.com/peter-evans/dockerhub-description>
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_USER_PASSWORD }}
repository: tarampampam/error-pages

View File

@ -15,7 +15,7 @@ jobs:
arch: [amd64] # amd64, 386
steps:
- uses: actions/setup-go@v2
with: {go-version: 1.17.1}
with: {go-version: 1.17.6}
- uses: actions/checkout@v2

View File

@ -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
@ -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}
@ -134,11 +149,12 @@ jobs: # Docs: <https://git.io/JvxXE>
test -f ./out/shuffle/404.html
test -f ./out/noise/404.html
test -f ./out/hacker-terminal/404.html
test -f ./out/cats/404.html
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
@ -166,6 +182,8 @@ jobs: # Docs: <https://git.io/JvxXE>
runs-on: ubuntu-20.04
needs: [docker-image]
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: docker-image
@ -186,6 +204,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
@ -194,16 +214,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" --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: 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/) -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()

2
.gitignore vendored
View File

@ -3,7 +3,7 @@
/.vscode
## Binaries
error-pages
/error-pages
## Temp dirs & trash
/temp

View File

@ -5,6 +5,8 @@ run:
skip-dirs:
- .github
- .git
- tmp
- temp
modules-download-mode: readonly
allow-parallel-runners: true
@ -40,7 +42,9 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
disable-all: true
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- bodyclose # Checks whether HTTP response body is closed successfully
- contextcheck # check the function whether use a non-inherited context
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())

3
.grype.yaml Normal file
View File

@ -0,0 +1,3 @@
ignore:
# temporary ignore this CVE as false positive on the Go package
- vulnerability: CVE-2015-5237

View File

@ -4,6 +4,78 @@ 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].
## v2.5.0
### Changed
- Go updated from `1.17.5` up to `1.17.6`
### Added
- `Host` and `X-Forwarded-For` Header to error pages [#61]
### Fixed
- Performance issue, that affects template rendering. Now templates are cached in memory (for 2 seconds), and it has improved performance by more than 200% [#60]
[#60]:https://github.com/tarampampam/error-pages/pull/60
[#61]:https://github.com/tarampampam/error-pages/pull/61
## v2.4.0
### 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 [#49]
- Health-check route become `/healthz` (instead `/health/live`, previous route marked as deprecated) [#49]
### Added
- The templates contain details block now (can be enabled using `--show-details` flag for the `serve` command or environment variable `SHOW_DETAILS=true`) [#49]
- Formatted response templates (`json`, `xml`) - the server responds with a formatted response depending on the `Content-Type` (and `X-Format`) request header value [#49]
- HTTP header `X-Robots-Tag: noindex` for the error pages [#49]
- Possibility to pass the needed error page code using `X-Code` HTTP header [#49]
- Possibility to integrate with [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) [#49]
- Metrics HTTP endpoint `/metrics` in prometheus format [#54]
### Fixed
- Potential race condition (in the `pick.StringsSlice` struct) [#49]
[#54]:https://github.com/tarampampam/error-pages/pull/54
[#49]:https://github.com/tarampampam/error-pages/pull/49
## v2.3.0
### Added
- Flag `--default-http-code` for the `serve` subcommand (`404` is used by default instead of `200`, environment name `DEFAULT_HTTP_CODE`) [#41]
### Changed
- Go updated from `1.17.1` up to `1.17.5`
[#41]:https://github.com/tarampampam/error-pages/issues/41
## v2.2.0
### Added
- Template `cats` [#31]
[#31]:https://github.com/tarampampam/error-pages/pull/31
## v2.1.0
### Added
- `referer` field in access log records
- Flag `--default-error-page` for the `serve` subcommand (`404` is used by default, environment name `DEFAULT_ERROR_PAGE`)
### Changed
- The source code has been refactored
- The index page (`/`) now returns the error page with a code, declared using `--default-error-page` flag (HTTP code 200, when a page code exists)
## v2.0.0
### Changed

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.2
# Image page: <https://hub.docker.com/_/golang>
FROM golang:1.17.1-alpine as builder
FROM golang:1.17.6-alpine as builder
# can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3@GITHASH" .`
ARG APP_VERSION="undefined@docker"
@ -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
@ -64,12 +65,13 @@ USER appuser:appuser
WORKDIR /opt
ENV LISTEN_PORT="8080" \
TEMPLATE_NAME="ghost"
TEMPLATE_NAME="ghost" \
DEFAULT_ERROR_PAGE="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"]

View File

@ -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
@ -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

448
README.md
View File

@ -4,355 +4,185 @@
# HTTP's error pages
[![Release version][badge_release_version]][link_releases]
![Project language][badge_language]
[![Build Status][badge_build]][link_build]
[![Release Status][badge_release]][link_build]
[![Coverage][badge_coverage]][link_coverage]
[![Image size][badge_size_latest]][link_docker_hub]
[![License][badge_license]][link_license]
[![Release version][badge-release]][releases]
![Project language][badge-lang]
[![Build Status][badge-ci-build]][actions-page]
[![Release Status][badge-ci-release]][actions-page]
[![Coverage][badge-coverage]][coverage]
[![Image size][badge-image-size]][docker-hub]
[![License][badge-license]][license]
One day you may want to replace the standard error pages of your HTTP server with something more original and pretty. That's what this repository was created for :) It contains:
- 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)
- Single-page error page templates with different designs (located in the [templates](https://github.com/tarampampam/error-pages/tree/master/templates) directory)
- Fast and lightweight HTTP server
- 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
Also, this project can be used for the [**Traefik** error pages customization](https://doc.traefik.io/traefik/middlewares/http/errorpages/).
## 🔥 Features list
<p align="center">
<img src="https://hsto.org/webt/bc/bt/9i/bcbt9i3jyvozequr1e4maz7i2q8.png" alt="" />
</p>
- HTTP server written on Go, with the extremely fast [FastHTTP][fasthttp] under the hood
- Respects the `Content-Type` HTTP header (and `X-Format`) value and responds with the corresponding format (supported formats is `json` and `xml`)
- Writes logs in `json` format
- Contains healthcheck endpoint (`/healthz`)
- Contains metrics endpoint (`/metrics`) in Prometheus format
- Lightweight docker image _(~4.3Mb compressed size)_, distroless and uses the unleveled user by default
- [Go-template](https://pkg.go.dev/text/template) tags are allowed in the templates
- Ready for integration with [Traefik][traefik] ([error pages customization](https://doc.traefik.io/traefik/middlewares/http/errorpages/)) and [Ingress-nginx][ingress-nginx]
- Fully configurable (take a look at the [configuration file](https://github.com/tarampampam/error-pages/blob/master/error-pages.yml) and [project Wiki][wiki])
- Distributed using docker image and compiled binary files
## Installing
## 🧩 Install
Download the latest binary file for your os/arch from the [releases page][link_releases] or use our docker image:
Download the latest binary file for your os/arch from the [releases page][releases] or use our docker image:
[![image stats](https://dockeri.co/image/tarampampam/error-pages)][link_docker_hub]
[![image stats](https://dockeri.co/image/tarampampam/error-pages)][docker-hub-tags]
Registry | Image
-------------------------------------- | -----
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
| Registry | Image |
|-----------------------------------|-----------------------------------|
| [Docker Hub][docker-hub] | `tarampampam/error-pages` |
| [GitHub Container Registry][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
To watch the docker image content you can use the [dive](https://github.com/wagoodman/dive):
## 🛠 Usage
```bash
$ docker run --rm -it \
-v "/var/run/docker.sock:/var/run/docker.sock:ro" \
wagoodman/dive:latest \
tarampampam/error-pages:latest
Please, take a look at [our Wiki][wiki] for the common usage stories:
- [HTTP server][wiki-http-server] (routes, formats, flags and environment variables)
- [Pages generator][wiki-generator] (build your own error page set)
- [Static error pages][wiki-static-error-pages] (extract generated static error pages from the docker image)
- [Usage with nginx][wiki-usage-with-nginx] (include our error pages into an image with nginx)
- [Usage with Traefik and local Docker Compose][wiki-traefik-docker-compose] (it's a good starting point for the tests)
- [Usage with Traefik and Docker Swarm][wiki-traefik-swarm]
- [Kubernetes & ingress nginx][wiki-k8s-ingress-nginx]
[wiki]:https://github.com/tarampampam/error-pages/wiki
[wiki-http-server]:https://github.com/tarampampam/error-pages/wiki/HTTP-server
[wiki-generator]:https://github.com/tarampampam/error-pages/wiki/Generator
[wiki-static-error-pages]:https://github.com/tarampampam/error-pages/wiki/Static-error-pages
[wiki-usage-with-nginx]:https://github.com/tarampampam/error-pages/wiki/Usage-with-nginx
[wiki-traefik-swarm]:https://github.com/tarampampam/error-pages/wiki/Traefik-(docker-swarm)
[wiki-traefik-docker-compose]:https://github.com/tarampampam/error-pages/wiki/Traefik-(docker-compose)
[wiki-k8s-ingress-nginx]:https://github.com/tarampampam/error-pages/wiki/Kubernetes-&-ingress-nginx
## 🦾 Performance
Used hardware:
- Intel® Core™ i7-10510U CPU @ 1.80GHz × 8
- 16 GiB RAM
```shell
$ ulimit -aH | grep file
-f: file size (blocks) unlimited
-c: core file size (blocks) unlimited
-n: file descriptors 1048576
-x: file locks unlimited
$ docker run --rm -p "8080:8080/tcp" -e "SHOW_DETAILS=true" error-pages:local # in separate terminal
$ wrk --timeout 1s -t12 -c400 -d30s -s ./test/wrk/request.lua http://127.0.0.1:8080/
Running 30s test @ http://127.0.0.1:8080/
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 10.84ms 7.89ms 135.91ms 79.36%
Req/Sec 3.23k 785.11 6.30k 70.04%
1160567 requests in 30.10s, 4.12GB read
Requests/sec: 38552.04
Transfer/sec: 140.23MB
```
<details>
<summary>Dive screenshot</summary>
<summary>FS & memory usage stats during the test</summary>
<p align="center">
<img src="https://hsto.org/webt/mi/ak/uf/miakufsh2ibxtsa1nomfhyqombi.png" alt="" />
</p>
<p align="center">
<img src="https://hsto.org/webt/ts/w-/lz/tsw-lznvru0ngjneiimkwq7ysyc.png" alt="" />
</p>
</details>
## 🪂 Templates
## 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**.
### HTTP server
As mentioned above - our application can be run as an HTTP server. It only needs to specify the path to the configuration file (it does not need statically generated error pages). The server uses [FastHTTP][fasthttp] and stores all necessary data in memory - so it does not use the file system and very fast. Oh yes, the image with the app also contains a configured **healthcheck** and **logs in JSON** format :)
For the HTTP server running execute in your terminal:
```bash
$ docker run --rm \
-p "8080:8080/tcp" \
-e "TEMPLATE_NAME=random" \
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`.
Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).
> **Cheat**: you can use `random` (to use the randomized theme on server start) or `i-said-random` (to use the randomized template on **each request**)
To see the help run the following command:
```bash
$ docker run --rm tarampampam/error-pages serve --help
```
### Generator
Create a config file (`error-pages.yml`) with the following content:
```yaml
templates:
- path: ./foo.html # Template name "foo" (same as file name),
# content located in the file "foo.html"
- name: bar # Template name "bar", its content is described below:
content: "Error {{ code }}: {{ message }} ({{ description }})"
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password
```
Template file `foo.html`:
```html
<html>
<title>{{ code }}</title>
<body>
<h1>{{ message }}: {{ description }}</h1>
</body>
</html>
```
And run the generator:
```bash
$ docker run --rm \
-v "$(pwd):/opt:rw" \
-u "$(id -u):$(id -g)" \
tarampampam/error-pages build --config-file ./error-pages.yml ./out
$ tree
.
├── error-pages.yml
├── foo.html
└── out
├── bar
│ ├── 400.html
│ └── 401.html
└── foo
├── 400.html
└── 401.html
3 directories, 6 files
$ cat ./out/foo/400.html
<html>
<title>400</title>
<body>
<h1>Bad Request: The server did not understand the request</h1>
</body>
</html>
$ cat ./out/bar/400.html
Error 400: Bad Request (The server did not understand the request)
```
To see the usage help run the following command:
```bash
$ docker run --rm tarampampam/error-pages build --help
```
### Static error pages
You may want to use the generated error pages somewhere else, and you can simply extract them from the docker image to your local directory for this purpose:
```bash
$ docker create --name error-pages tarampampam/error-pages:2.0.0-rc2
$ docker cp error-pages:/opt/html ./out
$ docker rm -f error-pages
$ ls ./out
ghost hacker-terminal index.html l7-dark l7-light noise shuffle
$ tree
.
└── out
├── ghost
│ ├── 400.html
│ ├── ...
│ └── 505.html
├── hacker-terminal
│ ├── 400.html
│ ├── ...
│ └── 505.html
├── index.html
├── l7-dark
│ ├── 400.html
│ ├── ...
...
```
### Custom error pages for your image with [nginx][link_nginx]
You can build your own docker image with `nginx` and our error pages:
```nginx
# File `nginx.conf`
server {
listen 80;
server_name localhost;
error_page 401 /_error-pages/401.html;
error_page 403 /_error-pages/403.html;
error_page 404 /_error-pages/404.html;
error_page 500 /_error-pages/500.html;
error_page 502 /_error-pages/502.html;
error_page 503 /_error-pages/503.html;
location ^~ /_error-pages/ {
internal;
root /usr/share/nginx/errorpages;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
```
```dockerfile
# File `Dockerfile`
FROM nginx:1.21-alpine
COPY --chown=nginx \
./nginx.conf /etc/nginx/conf.d/default.conf
COPY --chown=nginx \
--from=tarampampam/error-pages:2.0.0 \
/opt/html/ghost /usr/share/nginx/errorpages/_error-pages
```
```shell
$ docker build --tag your-nginx:local -f ./Dockerfile .
```
> More info about `error_page` directive can be [found here](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page).
## 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)
| Name | Preview |
|:-----------------:|:------------------------------------------------------------------:|
| `ghost` | [![ghost][ghost-screen]][ghost-link] |
| `l7-light` | [![l7-light][l7-light-screen]][l7-light-link] |
| `l7-dark` | [![l7-dark][l7-dark-screen]][l7-dark-link] |
| `shuffle` | [![shuffle][shuffle-screen]][shuffle-link] |
| `noise` | [![noise][noise-screen]][noise-link] |
| `hacker-terminal` | [![hacker-terminal][hacker-terminal-screen]][hacker-terminal-link] |
| `cats` | [![cats][cats-screen]][cats-link] |
> Note: `noise` template highly uses the CPU, be careful
## Custom error pages for [Traefik][link_traefik]
[ghost-screen]:https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif
[ghost-link]:https://tarampampam.github.io/error-pages/ghost/404.html
[l7-light-screen]:https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png
[l7-light-link]:https://tarampampam.github.io/error-pages/l7-light/404.html
[l7-dark-screen]:https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png
[l7-dark-link]:https://tarampampam.github.io/error-pages/l7-dark/404.html
[shuffle-screen]:https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif
[shuffle-link]:https://tarampampam.github.io/error-pages/shuffle/404.html
[noise-screen]:https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif
[noise-link]:https://tarampampam.github.io/error-pages/noise/404.html
[hacker-terminal-screen]:https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif
[hacker-terminal-link]:https://tarampampam.github.io/error-pages/hacker-terminal/404.html
[cats-screen]:https://hsto.org/webt/_g/y-/ke/_gy-keqinz-3867jbw36v37-iwe.jpeg
[cats-link]:https://tarampampam.github.io/error-pages/cats/404.html
Simple traefik (tested on `v2.5.3`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
## 🦾 Contributors
```yaml
version: '3.8'
I want to say a big thank you to everyone who contributed to this project:
services:
error-pages:
image: tarampampam/error-pages:2.0.0
environment:
TEMPLATE_NAME: l7-dark
networks:
- traefik-public
deploy:
placement:
constraints:
- node.role == worker
labels:
traefik.enable: 'true'
traefik.docker.network: traefik-public
# use as "fallback" for any non-registered services (with priority below normal)
traefik.http.routers.error-pages-router.rule: HostRegexp(`{host:.+}`)
traefik.http.routers.error-pages-router.priority: 10
# should say that all of your services work on https
traefik.http.routers.error-pages-router.tls: 'true'
traefik.http.routers.error-pages-router.entrypoints: https
traefik.http.routers.error-pages-router.middlewares: error-pages-middleware@docker
traefik.http.services.error-pages-service.loadbalancer.server.port: 8080
# "errors" middleware settings
traefik.http.middlewares.error-pages-middleware.errors.status: 400-599
traefik.http.middlewares.error-pages-middleware.errors.service: error-pages-service@docker
traefik.http.middlewares.error-pages-middleware.errors.query: /{status}.html
[![contributors](https://contrib.rocks/image?repo=tarampampam/error-pages)][contributors]
any-another-http-service:
image: nginx:alpine
networks:
- traefik-public
deploy:
placement:
constraints:
- node.role == worker
labels:
traefik.enable: 'true'
traefik.docker.network: traefik-public
traefik.http.routers.another-service.rule: Host(`subdomain.example.com`)
traefik.http.routers.another-service.tls: 'true'
traefik.http.routers.another-service.entrypoints: https
# next line is important
traefik.http.routers.another-service.middlewares: error-pages-middleware@docker
traefik.http.services.another-service.loadbalancer.server.port: 80
[contributors]:https://github.com/tarampampam/error-pages/graphs/contributors
networks:
traefik-public:
external: true
```
## 📰 Changes log
## Changes log
[![Release date][badge-release-date]][releases]
[![Commits since latest release][badge-commits]][commits]
[![Release date][badge_release_date]][link_releases]
[![Commits since latest release][badge_commits_since_release]][link_commits]
Changes log can be [found here][changelog].
Changes log can be [found here][link_changes_log].
## 👾 Support
## Support
[![Issues][badge-issues]][issues]
[![Issues][badge-prs]][prs]
[![Issues][badge_issues]][link_issues]
[![Issues][badge_pulls]][link_pulls]
If you find any bugs in the project, please [create an issue][new-issue] in the current repository.
If you will find any package errors, please, [make an issue][link_create_issue] in current repository.
## 📖 License
## License
This is open-sourced software licensed under the [MIT License][license].
This is open-sourced software licensed under the [MIT License][link_license].
[badge-ci-build]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github
[badge-ci-release]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github
[badge-coverage]:https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30
[badge-release]:https://img.shields.io/github/release/tarampampam/error-pages.svg?maxAge=30
[badge-image-size]:https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30
[badge-lang]:https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true
[badge-license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
[badge-release-date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?maxAge=180
[badge-commits]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?maxAge=45
[badge-issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?maxAge=45
[badge-prs]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?maxAge=45
[badge_build]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github
[badge_release]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github
[badge_coverage]:https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30
[badge_release_version]:https://img.shields.io/github/release/tarampampam/error-pages.svg?maxAge=30
[badge_size_latest]:https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30
[badge_language]:https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true
[badge_license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
[badge_release_date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?maxAge=180
[badge_commits_since_release]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?maxAge=45
[badge_issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?maxAge=45
[badge_pulls]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?maxAge=45
[link_coverage]:https://codecov.io/gh/tarampampam/error-pages
[link_build]:https://github.com/tarampampam/error-pages/actions
[link_docker_hub]:https://hub.docker.com/r/tarampampam/error-pages
[link_docker_tags]:https://hub.docker.com/r/tarampampam/error-pages/tags
[link_license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
[link_releases]:https://github.com/tarampampam/error-pages/releases
[link_commits]:https://github.com/tarampampam/error-pages/commits
[link_changes_log]:https://github.com/tarampampam/error-pages/blob/master/CHANGELOG.md
[link_issues]:https://github.com/tarampampam/error-pages/issues
[link_create_issue]:https://github.com/tarampampam/error-pages/issues/new/choose
[link_pulls]:https://github.com/tarampampam/error-pages/pulls
[link_ghcr]:https://github.com/users/tarampampam/packages/container/package/error-pages
[coverage]:https://codecov.io/gh/tarampampam/error-pages
[actions-page]:https://github.com/tarampampam/error-pages/actions
[docker-hub]:https://hub.docker.com/r/tarampampam/error-pages
[docker-hub-tags]:https://hub.docker.com/r/tarampampam/error-pages/tags
[license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
[releases]:https://github.com/tarampampam/error-pages/releases
[commits]:https://github.com/tarampampam/error-pages/commits
[changelog]:https://github.com/tarampampam/error-pages/blob/master/CHANGELOG.md
[issues]:https://github.com/tarampampam/error-pages/issues
[new-issue]:https://github.com/tarampampam/error-pages/issues/new/choose
[prs]:https://github.com/tarampampam/error-pages/pulls
[ghcr]:https://github.com/users/tarampampam/packages/container/package/error-pages
[fasthttp]:https://github.com/valyala/fasthttp
[preview-sources]:https://github.com/tarampampam/error-pages/tree/gh-pages
[preview-demo]:https://tarampampam.github.io/error-pages/
[link_nginx]:http://nginx.org/
[link_traefik]:https://docs.traefik.io/
[link_swarm]:https://docs.docker.com/engine/swarm/
[link_gh_pages]:https://tarampampam.github.io/error-pages/
[traefik]:https://github.com/traefik/traefik
[ingress-nginx]:https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx

View File

@ -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")
}

View File

@ -1,6 +1,6 @@
# Docker-compose file is used only for local development. This is not production-ready example.
version: '3.4'
version: '3.8'
volumes:
tmp-data: {}
@ -8,7 +8,7 @@ volumes:
services:
app: &app-service
image: golang:1.17.1-buster # Image page: <https://hub.docker.com/_/golang>
image: golang:1.17.6-buster # Image page: <https://hub.docker.com/_/golang>
working_dir: /src
environment:
HOME: /tmp
@ -30,13 +30,14 @@ 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:
@ -44,3 +45,12 @@ services:
- 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

View File

@ -10,6 +10,48 @@ templates:
- path: ./templates/shuffle.html
- path: ./templates/noise.html
- 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": {
"host": {{ host | json }},
"original_uri": {{ original_uri | json }},
"forwarded_for": {{ forwarded_for | 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>
<host>{{ host }}</host>
<originalURI>{{ original_uri }}</originalURI>
<forwardedFor>{{ forwarded_for }}</forwardedFor>
<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:

35
go.mod
View File

@ -3,30 +3,39 @@ module github.com/tarampampam/error-pages
go 1.17
require (
github.com/a8m/envsubst v1.2.0
github.com/fasthttp/router v1.4.3
github.com/fatih/color v1.7.0
github.com/a8m/envsubst v1.3.0
github.com/fasthttp/router v1.4.5
github.com/fatih/color v1.13.0
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v1.2.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.0
github.com/prometheus/client_model v0.2.0
github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/valyala/fasthttp v1.30.0
go.uber.org/zap v1.19.1
github.com/valyala/fasthttp v1.33.0
go.uber.org/zap v1.20.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
require (
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/klauspost/compress v1.14.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
google.golang.org/protobuf v1.27.1 // indirect
)

352
go.sum
View File

@ -18,6 +18,15 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -26,7 +35,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -39,30 +48,57 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/a8m/envsubst v1.2.0 h1:yvzAhJD2QKdo35Ut03wIfXQmg+ta3wC/1bskfZynz+Q=
github.com/a8m/envsubst v1.2.0/go.mod h1:PpvLvNWa+Rvu/10qXmFbFiGICIU5hZvFJNPCCkUaObg=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/a8m/envsubst v1.3.0 h1:GmXKmVssap0YtlU3E230W98RWtWCyIZzjtf1apWWyAg=
github.com/a8m/envsubst v1.3.0/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -72,22 +108,37 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.3 h1:spS+LUnRryQ/+hbmYzs/xWGJlQCkeQI3hxGZdlVYhLU=
github.com/fasthttp/router v1.4.3/go.mod h1:9ytWCfZ5LcCcbD3S7pEXyBX9vZnOZmN918WiiaYUzr8=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fasthttp/router v1.4.5 h1:YZonsKCssEwEi3veDMhL6okIx550qegAiuXAK8NnM3Y=
github.com/fasthttp/router v1.4.5/go.mod h1:UYExWhCy7pUmavRZ0XfjEgHwzxyKwyS8uzXhaTRDG9Y=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -96,6 +147,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -112,6 +164,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@ -127,10 +180,13 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@ -142,100 +198,172 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.14.1 h1:hLQYb23E8/fO+1u53d02A97a8UnsddcvYzq4ERRU4ds=
github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg=
github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899 h1:Orn7s+r1raRTBKLSc9DmbktTT04sL+vkzsbRD2Q8rOI=
github.com/savsgio/gotils v0.0.0-20211223103454-d0aaa54c5899/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -244,19 +372,21 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fasthttp v1.32.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fasthttp v1.33.0 h1:mHBKd98J5NcXuBddgjvim1i3kWzlng1SzLhrnBOU9g8=
github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -264,23 +394,28 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc=
go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -316,10 +451,11 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -327,9 +463,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -352,7 +490,13 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -364,7 +508,12 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -378,21 +527,31 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -403,6 +562,8 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -410,7 +571,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -418,8 +581,23 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -429,6 +607,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -438,7 +617,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -446,9 +624,9 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@ -482,11 +660,15 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -509,7 +691,17 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -557,7 +749,29 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -577,7 +791,15 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -590,14 +812,22 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -8,7 +8,7 @@ import (
"syscall"
)
// OSSignals allows to subscribe for system signals.
// OSSignals allows subscribing for system signals.
type OSSignals struct {
ctx context.Context
ch chan os.Signal
@ -22,7 +22,7 @@ func NewOSSignals(ctx context.Context) OSSignals {
}
}
// Subscribe for some of system signals (call Stop for stopping).
// Subscribe for some system signals (call Stop for stopping).
func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) {
if len(signals) == 0 {
signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals

View File

@ -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
}

View File

@ -18,7 +18,7 @@ func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f
func TestHealthChecker_CheckSuccess(t *testing.T) {
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{

View File

@ -1,11 +1,8 @@
package build
import (
"bytes"
"os"
"path"
"text/template"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -14,12 +11,8 @@ import (
"go.uber.org/zap"
)
type historyItem struct {
Code, Message, Path string
}
// NewCommand creates `build` command.
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command {
var (
generateIndex bool
cfg *config.Config
@ -30,94 +23,23 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
Aliases: []string{"b"},
Short: "Build the error pages",
Args: cobra.ExactArgs(1),
PreRunE: func(*cobra.Command, []string) error {
PreRunE: func(*cobra.Command, []string) (err error) {
if configFile == nil {
return errors.New("path to the config file is required for this command")
}
if c, err := config.FromYamlFile(*configFile); err != nil {
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
} else {
if err = c.Validate(); err != nil {
return err
}
cfg = c
}
return nil
return
},
RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("wrong arguments count")
}
log.Info("loading templates")
templates, err := cfg.LoadTemplates()
if err != nil {
return err
} else if len(templates) == 0 {
return errors.New("no loaded templates")
}
log.Debug("the output directory preparing", zap.String("Path", args[0]))
if err = createDirectory(args[0]); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
codes := make(map[string]tpl.Annotator)
for code, desc := range cfg.Pages {
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
}
history := make(map[string][]historyItem, len(templates))
log.Info("saving the error pages")
startedAt := time.Now()
if err = tpl.NewErrors(templates, codes).VisitAll(func(template, code string, content []byte) error {
if e := createDirectory(path.Join(args[0], template)); e != nil {
return e
}
fileName := code + ".html"
if e := os.WriteFile(path.Join(args[0], template, fileName), content, 0664); e != nil { //nolint:gosec,gomnd
return e
}
if _, ok := history[template]; !ok {
history[template] = make([]historyItem, 0, len(codes))
}
history[template] = append(history[template], historyItem{
Code: code,
Message: codes[code].Message,
Path: path.Join(template, fileName),
})
return nil
}); err != nil {
return nil
}
log.Debug("saved", zap.Duration("duration", time.Since(startedAt)))
if generateIndex {
log.Info("index file generation")
startedAt = time.Now()
if err = writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
return err
}
log.Debug("index file generated", zap.Duration("duration", time.Since(startedAt)))
}
return nil
return run(log, cfg, args[0], generateIndex)
},
}
@ -131,11 +53,87 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
return cmd
}
func createDirectory(path string) error {
const (
outHTMLFileExt = ".html"
outIndexFileName = "index"
outFilePerm = os.FileMode(0664)
outDirPerm = os.FileMode(0775)
)
func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex bool) error { //nolint:funlen
if len(cfg.Templates) == 0 {
return errors.New("no loaded templates")
}
log.Info("output directory preparing", zap.String("path", outDirectoryPath))
if err := createDirectory(outDirectoryPath, outDirPerm); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
history, renderer := newBuildingHistory(), tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
for _, template := range cfg.Templates {
log.Debug("template processing", zap.String("name", template.Name()))
for _, page := range cfg.Pages {
if err := createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
return err
}
var (
fileName = page.Code() + outHTMLFileExt
filePath = path.Join(outDirectoryPath, template.Name(), fileName)
)
content, renderingErr := renderer.Render(template.Content(), tpl.Properties{
Code: page.Code(),
Message: page.Message(),
Description: page.Description(),
ShowRequestDetails: false,
})
if renderingErr != nil {
return renderingErr
}
if err := os.WriteFile(filePath, content, outFilePerm); err != nil {
return err
}
log.Debug("page rendered", zap.String("path", filePath))
if generateIndex {
history.Append(
template.Name(),
page.Code(),
page.Message(),
path.Join(template.Name(), fileName),
)
}
}
}
if generateIndex {
var filepath = path.Join(outDirectoryPath, outIndexFileName+outHTMLFileExt)
log.Info("index file generation", zap.String("path", filepath))
if err := history.WriteIndexFile(filepath, outFilePerm); err != nil {
return err
}
}
log.Info("job is done")
return nil
}
func createDirectory(path string, perm os.FileMode) error {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(path, 0775) //nolint:gomnd
return os.MkdirAll(path, perm)
}
return err
@ -147,52 +145,3 @@ func createDirectory(path string) error {
return nil
}
func writeIndexFile(path string, history map[string][]historyItem) error {
t, err := template.New("index").Parse(`<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Error pages list</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/css/bootstrap.min.css"
integrity="sha512-6KY5s6UI5J7SVYuZB4S/CZMyPylqyyNZco376NM2Z8Sb8OxEdp02e1jkKk/wZxIEmjQ6DRCEBhni+gpr9c4tvA=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-light">
<div class="container">
<main>
<div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
alt="" width="94">
<h2>Error pages index</h2>
</div>
{{- range $template, $item := . -}}
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
<ul class="mb-5">
{{ range $item -}}
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
{{ end -}}
</ul>
{{ end }}
</main>
</div>
<footer class="footer">
<div class="container text-center text-muted mt-3 mb-3">
For online documentation and support please refer to the
<a href="https://github.com/tarampampam/error-pages">project repository</a>.
</div>
</footer>
</body>
</html>`)
if err != nil {
return err
}
var buf bytes.Buffer
if err = t.Execute(&buf, history); err != nil {
return err
}
return os.WriteFile(path, buf.Bytes(), 0664) //nolint:gosec,gomnd
}

View File

@ -0,0 +1,59 @@
package build
import (
"bytes"
_ "embed"
"os"
"sort"
"text/template"
)
type (
buildingHistory struct {
items map[string][]historyItem
}
historyItem struct {
Code, Message, Path string
}
)
func newBuildingHistory() buildingHistory {
return buildingHistory{items: make(map[string][]historyItem)}
}
func (bh *buildingHistory) Append(templateName, pageCode, message, path string) {
if _, ok := bh.items[templateName]; !ok {
bh.items[templateName] = make([]historyItem, 0)
}
bh.items[templateName] = append(bh.items[templateName], historyItem{
Code: pageCode,
Message: message,
Path: path,
})
sort.Slice(bh.items[templateName], func(i, j int) bool { // keep history items sorted
return bh.items[templateName][i].Code < bh.items[templateName][j].Code
})
}
//go:embed index.tpl.html
var indexPageTemplate string
func (bh *buildingHistory) WriteIndexFile(path string, perm os.FileMode) error {
t, err := template.New("index").Parse(indexPageTemplate)
if err != nil {
return err
}
var buf bytes.Buffer
if err = t.Execute(&buf, bh.items); err != nil {
return err
}
defer buf.Reset() // optimization (is needed here?)
return os.WriteFile(path, buf.Bytes(), perm)
}

View File

@ -0,0 +1,35 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Error pages list</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css"
integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-light">
<div class="container">
<main>
<div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
alt="" width="94">
<h2>Error pages index</h2>
</div>
{{- range $template, $item := . -}}
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
<ul class="mb-5">
{{ range $item -}}
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
{{ end -}}
</ul>
{{ end }}
</main>
</div>
<footer class="footer">
<div class="container text-center text-muted mt-3 mb-3">
For online documentation and support please refer to the
<a href="https://github.com/tarampampam/error-pages">project repository</a>.
</div>
</footer>
</body>
</html>

View File

@ -6,14 +6,12 @@ import (
"os"
"time"
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
"github.com/tarampampam/error-pages/internal/tpl"
"go.uber.org/zap"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
"github.com/tarampampam/error-pages/internal/pick"
"go.uber.org/zap"
)
// NewCommand creates `serve` command.
@ -27,23 +25,17 @@ func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra
Use: "serve",
Aliases: []string{"s", "server"},
Short: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) error {
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
if configFile == nil {
return errors.New("path to the config file is required for this command")
}
if err := f.overrideUsingEnv(cmd.Flags()); err != nil {
if err = f.overrideUsingEnv(cmd.Flags()); err != nil {
return err
}
if c, err := config.FromYamlFile(*configFile); err != nil {
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
} else {
if err = c.Validate(); err != nil {
return err
}
cfg = c
}
return f.validate()
@ -56,8 +48,6 @@ func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra
return cmd
}
const serverShutdownTimeout = 15 * time.Second
// run current command.
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
var (
@ -77,35 +67,45 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
oss.Stop() // stop system signals listening
}()
// load templates content
templates, loadingErr := cfg.LoadTemplates()
if loadingErr != nil {
return loadingErr
} else if len(templates) == 0 {
return errors.New("no loaded templates")
}
var (
templateNames = cfg.TemplateNames()
picker *pick.StringsSlice
)
if f.template.name != "" && f.template.name != errorpage.UseRandom && f.template.name != errorpage.UseRandomOnEachRequest { //nolint:lll
if _, found := templates[f.template.name]; !found {
return errors.New("requested nonexistent template: " + f.template.name) // requested unknown template
switch f.template.name {
case useRandomTemplate:
log.Info("A random template will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
case useRandomTemplateOnEachRequest:
log.Info("A random template on EACH request will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
case "":
log.Info("The first template (ordered by name) will be used")
picker = pick.NewStringsSlice(templateNames, pick.First)
default:
if t, found := cfg.Template(f.template.name); found {
log.Info("We will use the requested template", zap.String("name", t.Name()))
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
} else {
return errors.New("requested nonexistent template: " + f.template.name)
}
}
// burn the error codes map
codes := make(map[string]tpl.Annotator)
for code, desc := range cfg.Pages {
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
}
// create HTTP server
server := appHttp.NewServer(log)
// register server routes, middlewares, etc.
if err := server.Register(f.template.name, templates, codes); err != nil {
if err := server.Register(cfg, picker, f.defaultErrorPage, f.defaultHTTPCode, f.showDetails); err != nil {
return err
}
startingErrCh := make(chan error, 1) // channel for server starting error
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
@ -114,7 +114,9 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
log.Info("Server starting",
zap.String("addr", f.listen.ip),
zap.Uint16("port", f.listen.port),
zap.String("template name", f.template.name),
zap.String("default error page", f.defaultErrorPage),
zap.Uint16("default HTTP response code", f.defaultHTTPCode),
zap.Bool("show request details", f.showDetails),
)
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
@ -128,20 +130,12 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
return err
case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping")
stoppedAt := time.Now()
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
// stop the server using created context above
if err := server.Stop(serverShutdownTimeout); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Error("Server stopping timeout exceeded", zap.Duration("timeout", serverShutdownTimeout))
}
if err := server.Stop(); err != nil {
return err
}
log.Debug("Server stopped", zap.Duration("stopping duration", time.Since(stoppedAt)))
}
return nil

View File

@ -6,8 +6,6 @@ import (
"strconv"
"strings"
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
)
@ -20,12 +18,23 @@ type flags struct {
template struct {
name string
}
defaultErrorPage string
defaultHTTPCode uint16
showDetails bool
}
const (
listenFlagName = "listen"
portFlagName = "port"
templateNameFlagName = "template-name"
listenFlagName = "listen"
portFlagName = "port"
templateNameFlagName = "template-name"
defaultErrorPageFlagName = "default-error-page"
defaultHTTPCodeFlagName = "default-http-code"
showDetailsFlagName = "show-details"
)
const (
useRandomTemplate = "random"
useRandomTemplateOnEachRequest = "i-said-random"
)
func (f *flags) init(flagSet *pflag.FlagSet) {
@ -46,13 +55,31 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
templateNameFlagName, "t",
"",
fmt.Sprintf(
"template name (set \"%s\" to use the randomized or \"%s\" to use the randomized template on each request) [$%s]", //nolint:lll
errorpage.UseRandom, errorpage.UseRandomOnEachRequest, env.TemplateName,
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll
useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName,
),
)
flagSet.StringVarP(
&f.defaultErrorPage,
defaultErrorPageFlagName, "",
"404",
fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage),
)
flagSet.Uint16VarP(
&f.defaultHTTPCode,
defaultHTTPCodeFlagName, "",
404, //nolint:gomnd
fmt.Sprintf("default HTTP response code [$%s]", env.DefaultHTTPCode),
)
flagSet.BoolVarP(
&f.showDetails,
showDetailsFlagName, "",
false,
fmt.Sprintf("show request details in response [$%s]", env.ShowDetails),
)
}
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo
flagSet.VisitAll(func(flag *pflag.Flag) {
// flag was NOT defined using CLI (flags should have maximal priority)
if !flag.Changed { //nolint:nestif
@ -75,6 +102,27 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
if envVar, exists := env.TemplateName.Lookup(); exists {
f.template.name = strings.TrimSpace(envVar)
}
case defaultErrorPageFlagName:
if envVar, exists := env.DefaultErrorPage.Lookup(); exists {
f.defaultErrorPage = strings.TrimSpace(envVar)
}
case defaultHTTPCodeFlagName:
if envVar, exists := env.DefaultHTTPCode.Lookup(); exists {
if code, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd
f.defaultHTTPCode = uint16(code)
} else {
lastErr = fmt.Errorf("wrong default HTTP response code environment variable [%s] value", envVar)
}
}
case showDetailsFlagName:
if envVar, exists := env.ShowDetails.Lookup(); exists {
if b, err := strconv.ParseBool(envVar); err == nil {
f.showDetails = b
}
}
}
}
})
@ -87,5 +135,9 @@ func (f *flags) validate() error {
return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip)
}
if f.defaultHTTPCode > 599 { //nolint:gomnd
return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode)
}
return nil
}

View File

@ -2,6 +2,8 @@ package config
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
@ -11,20 +13,116 @@ import (
"gopkg.in/yaml.v3"
)
// Config is a main (exportable) config struct.
type Config struct {
Templates []Template
Pages map[string]Page // map key is a page code
Formats map[string]Format // map key is a format name
}
// Template returns a Template with the passes name.
func (c *Config) Template(name string) (*Template, bool) {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].name == name {
return &c.Templates[i], true
}
}
return &Template{}, false
}
func (c *Config) JSONFormat() (*Format, bool) { return c.format("json") }
func (c *Config) XMLFormat() (*Format, bool) { return c.format("xml") }
func (c *Config) format(name string) (*Format, bool) {
if f, ok := c.Formats[name]; ok {
if len(f.content) > 0 {
return &f, true
}
}
return &Format{}, false
}
// TemplateNames returns all template names.
func (c *Config) TemplateNames() []string {
n := make([]string, len(c.Templates))
for i, t := range c.Templates {
n[i] = t.name
}
return n
}
// Template describes HTTP error page template.
type Template struct {
name string
content []byte
}
// Name returns the name of the template.
func (t Template) Name() string { return t.name }
// Content returns the template content.
func (t Template) Content() []byte { return t.content }
func (t *Template) loadContentFromFile(filePath string) (err error) {
if t.content, err = ioutil.ReadFile(filePath); err != nil {
return errors.Wrap(err, "cannot load content for the template "+t.Name()+" from file "+filePath)
}
return
}
// Page describes error page.
type Page struct {
code string
message string
description string
}
// Code returns the code of the Page.
func (p Page) Code() string { return p.code }
// Message returns the message of the Page.
func (p Page) Message() string { return p.message }
// Description returns the description of the Page.
func (p Page) Description() string { return p.description }
// Format describes different response formats.
type Format struct {
name string
content []byte
}
// Name returns the name of the format.
func (f Format) Name() string { return f.name }
// Content returns the format content.
func (f Format) Content() []byte { return f.content }
// config is internal struct for marshaling/unmarshaling configuration file content.
type config struct {
Templates []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"templates"`
Formats map[string]struct {
Content string `yaml:"content"`
} `yaml:"formats"`
Pages map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
} `yaml:"pages"`
}
// Validate the config and return an error if something is wrong.
func (c Config) Validate() error {
// Validate the config struct and return an error if something is wrong.
func (c config) Validate() error {
if len(c.Templates) == 0 {
return errors.New("empty templates list")
} else {
@ -53,64 +151,106 @@ func (c Config) Validate() error {
}
}
if len(c.Formats) > 0 {
for name := range c.Formats {
if name == "" {
return errors.New("empty format name")
}
if strings.ContainsRune(name, ' ') {
return errors.New("format should not contain whitespaces")
}
}
}
return nil
}
// LoadTemplates loading templates content from the local files and return it.
func (c Config) LoadTemplates() (map[string][]byte, error) {
var templates = make(map[string][]byte)
// Export the config struct into Config.
func (c *config) Export() (*Config, error) {
cfg := &Config{}
cfg.Templates = make([]Template, 0, len(c.Templates))
for i := 0; i < len(c.Templates); i++ {
var name string
if c.Templates[i].Name == "" {
basename := filepath.Base(c.Templates[i].Path)
name = strings.TrimSuffix(basename, filepath.Ext(basename))
} else {
name = c.Templates[i].Name
}
var content []byte
tpl := Template{name: c.Templates[i].Name}
if c.Templates[i].Content == "" {
b, err := ioutil.ReadFile(c.Templates[i].Path)
if err != nil {
return nil, errors.Wrap(err, "cannot load content for the template "+name)
if c.Templates[i].Path == "" {
return nil, errors.New("path to the template " + c.Templates[i].Name + " not provided")
}
content = b
if err := tpl.loadContentFromFile(c.Templates[i].Path); err != nil {
return nil, err
}
} else {
content = []byte(c.Templates[i].Content)
tpl.content = []byte(c.Templates[i].Content)
}
templates[name] = content
cfg.Templates = append(cfg.Templates, tpl)
}
return templates, nil
cfg.Pages = make(map[string]Page, len(c.Pages))
for code, p := range c.Pages {
cfg.Pages[code] = Page{code: code, message: p.Message, description: p.Description}
}
cfg.Formats = make(map[string]Format, len(c.Formats))
for name, f := range c.Formats {
cfg.Formats[name] = Format{name: name, content: []byte(strings.TrimSpace(f.Content))}
}
return cfg, nil
}
// FromYaml creates new config instance using YAML-structured content.
func FromYaml(in []byte) (cfg *Config, err error) {
cfg = &Config{}
// FromYaml creates new Config instance using YAML-structured content.
func FromYaml(in []byte) (_ *Config, err error) {
in, err = envsubst.Bytes(in)
if err != nil {
return nil, err
}
if err = yaml.Unmarshal(in, cfg); err != nil {
c := &config{}
if err = yaml.Unmarshal(in, c); err != nil {
return nil, errors.Wrap(err, "cannot parse configuration file")
}
return
var basename string
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" { // set the template name from file path
basename = filepath.Base(c.Templates[i].Path)
c.Templates[i].Name = strings.TrimSuffix(basename, filepath.Ext(basename))
}
}
if err = c.Validate(); err != nil {
return nil, err
}
return c.Export()
}
// FromYamlFile creates new config instance using YAML file.
// FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) {
bytes, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file")
}
// the following code makes it possible to use the relative links in the config file (`.` means "directory with
// the config file")
cwd, err := os.Getwd()
if err == nil {
if err = os.Chdir(path.Dir(filepath)); err != nil {
return nil, err
}
defer func() { _ = os.Chdir(cwd) }()
}
return FromYaml(bytes)
}

View File

@ -1,7 +1,6 @@
package config_test
import (
"errors"
"os"
"testing"
@ -9,196 +8,35 @@ import (
"github.com/tarampampam/error-pages/internal/config"
)
func TestConfig_Validate(t *testing.T) {
for name, tt := range map[string]struct {
giveConfig func() config.Config
wantError error
}{
"valid": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
"400": {"Bad Request", "The server did not understand the request"},
}
return c
},
wantError: nil,
},
"empty templates list": {
giveConfig: func() config.Config {
return config.Config{}
},
wantError: errors.New("empty templates list"),
},
"empty path and name": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{
Path: "foo",
Name: "bar",
Content: "baz",
},
{
Path: "",
Name: "",
Content: "blah",
},
}
return c
},
wantError: errors.New("empty path and name with index 1"),
},
"empty path and template content": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{
Path: "foo",
Name: "bar",
Content: "baz",
},
{
Path: "",
Name: "blah",
Content: "",
},
}
return c
},
wantError: errors.New("empty path and template content with index 1"),
},
"empty pages list": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{}
return c
},
wantError: errors.New("empty pages list"),
},
"empty page code": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
"": {"foo", "bar"},
}
return c
},
wantError: errors.New("empty page code"),
},
"code should not contain whitespaces": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
" 123": {"foo", "bar"},
}
return c
},
wantError: errors.New("code should not contain whitespaces"),
},
} {
tt := tt
t.Run(name, func(t *testing.T) {
err := tt.giveConfig().Validate()
if tt.wantError != nil {
assert.EqualError(t, err, tt.wantError.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestFromYaml(t *testing.T) {
var cases = []struct { //nolint:maligned
name string
var cases = map[string]struct { //nolint:maligned
giveYaml []byte
giveEnv map[string]string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
{
name: "with all possible values",
"with all possible values": {
giveEnv: map[string]string{
"__GHOST_PATH": "./templates/ghost.html",
"__GHOST_NAME": "ghost",
"__FOO_TPL_PATH": "./testdata/foo-tpl.html",
"__FOO_TPL_NAME": "Foo Template",
},
giveYaml: []byte(`
templates:
- path: ${__GHOST_PATH}
name: ${__GHOST_NAME:-default_value} # name is optional
- path: ./templates/l7-light.html
- name: Foo
- path: ${__FOO_TPL_PATH}
name: ${__FOO_TPL_NAME:-default_value} # name is optional
- path: ./testdata/bar-tpl.html
- name: Baz
content: |
Some content
Some content {{ code }}
New line
formats:
json:
content: |
{"code": "{{code}}"}
Avada_Kedavra:
content: "{{ message }}"
pages:
400:
message: Bad Request
@ -211,33 +49,66 @@ pages:
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 3)
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
assert.Equal(t, "ghost", cfg.Templates[0].Name)
assert.Equal(t, "", cfg.Templates[0].Content)
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
assert.Equal(t, "", cfg.Templates[1].Name)
assert.Equal(t, "", cfg.Templates[1].Content)
assert.Equal(t, "", cfg.Templates[2].Path)
assert.Equal(t, "Foo", cfg.Templates[2].Name)
assert.Equal(t, "Some content\nNew line\n", cfg.Templates[2].Content)
tpl, found := cfg.Template("Foo Template")
assert.True(t, found)
assert.Equal(t, "Foo Template", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("Baz")
assert.True(t, found)
assert.Equal(t, "Baz", tpl.Name())
assert.Equal(t, "Some content {{ code }}\nNew line\n", string(tpl.Content()))
tpl, found = cfg.Template("NonExists")
assert.False(t, found)
assert.Equal(t, "", tpl.Name())
assert.Equal(t, "", string(tpl.Content()))
assert.Len(t, cfg.Formats, 2)
format, found := cfg.Formats["json"]
assert.True(t, found)
assert.Equal(t, `{"code": "{{code}}"}`, string(format.Content()))
format, found = cfg.Formats["Avada_Kedavra"]
assert.True(t, found)
assert.Equal(t, "{{ message }}", string(format.Content()))
assert.Len(t, cfg.Pages, 2)
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
errPage, found = cfg.Pages["666"]
assert.False(t, found)
assert.Equal(t, "", errPage.Message())
assert.Equal(t, "", errPage.Code())
assert.Equal(t, "", errPage.Description())
},
},
{
name: "broken yaml",
"broken yaml": {
giveYaml: []byte(`foo bar`),
wantErr: true,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
if tt.giveEnv != nil {
for key, value := range tt.giveEnv {
assert.NoError(t, os.Setenv(key, value))
@ -263,45 +134,54 @@ pages:
}
func TestFromYamlFile(t *testing.T) {
var cases = []struct { //nolint:maligned
name string
var cases = map[string]struct { //nolint:maligned
giveYamlFilePath string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
{
name: "with all possible values",
"with all possible values": {
giveYamlFilePath: "./testdata/simple.yml",
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 2)
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
assert.Equal(t, "ghost", cfg.Templates[0].Name)
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
assert.Equal(t, "", cfg.Templates[1].Name)
tpl, found := cfg.Template("ghost")
assert.True(t, found)
assert.Equal(t, "ghost", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
assert.Len(t, cfg.Pages, 2)
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
},
},
{
name: "broken yaml",
"broken yaml": {
giveYamlFilePath: "./testdata/broken.yml",
wantErr: true,
},
{
name: "wrong file path",
"wrong file path": {
giveYamlFilePath: "foo bar",
wantErr: true,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
conf, err := config.FromYamlFile(tt.giveYamlFilePath)
if tt.wantErr {

1
internal/config/testdata/bar-tpl.html vendored Normal file
View File

@ -0,0 +1 @@
<html><body>bar {{ code }}</body></html>

1
internal/config/testdata/foo-tpl.html vendored Normal file
View File

@ -0,0 +1 @@
<html><body>foo {{ code }}</body></html>

View File

@ -1,7 +1,7 @@
templates:
- path: ./templates/ghost.html
- path: ./foo-tpl.html
name: ghost # name is optional
- path: ./templates/l7-light.html
- path: ./bar-tpl.html
pages:
400:

11
internal/env/env.go vendored
View File

@ -6,10 +6,13 @@ import "os"
type envVariable string
const (
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
ListenPort envVariable = "LISTEN_PORT" // port number for listening
TemplateName envVariable = "TEMPLATE_NAME" // template name
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
ListenPort envVariable = "LISTEN_PORT" // port number for listening
TemplateName envVariable = "TEMPLATE_NAME" // template name
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code)
DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code
ShowDetails envVariable = "SHOW_DETAILS" // show request details in response
)
// String returns environment variable name in the string representation.

View File

@ -12,6 +12,9 @@ func TestConstants(t *testing.T) {
assert.Equal(t, "LISTEN_PORT", string(ListenPort))
assert.Equal(t, "TEMPLATE_NAME", string(TemplateName))
assert.Equal(t, "CONFIG_FILE", string(ConfigFilePath))
assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage))
assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode))
assert.Equal(t, "SHOW_DETAILS", string(ShowDetails))
}
func TestEnvVariable_Lookup(t *testing.T) {
@ -22,6 +25,9 @@ func TestEnvVariable_Lookup(t *testing.T) {
{giveEnv: ListenPort},
{giveEnv: TemplateName},
{giveEnv: ConfigFilePath},
{giveEnv: DefaultErrorPage},
{giveEnv: DefaultHTTPCode},
{giveEnv: ShowDetails},
}
for _, tt := range cases {

View File

@ -1,35 +0,0 @@
package common
import (
"strings"
"github.com/valyala/fasthttp"
)
const internalErrorPattern = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<title>Internal error occurred</title>
<style>
html,body {background-color: #0e0e0e;color:#fff;font-family:'Nunito',sans-serif;height:100%;margin:0}
.message {height:100%;align-items:center;display:flex;justify-content:center;position:relative;font-size:1.4em}
img {padding-right: .4em}
</style>
</head>
<body>
<div class="message">
<img src="https://hsto.org/webt/fs/sx/gt/fssxgtssfg689qxboqvjil5yz8g.png" alt="logo" height="32">
<p>{{ message }}</p>
</div>
</body>
</html>`
func HandleInternalHTTPError(ctx *fasthttp.RequestCtx, statusCode int, message string) {
ctx.SetStatusCode(statusCode)
ctx.SetContentType("text/html; charset=UTF-8")
_, _ = ctx.WriteString(strings.ReplaceAll(internalErrorPattern, "{{ message }}", message))
}

View File

@ -25,9 +25,27 @@ func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHand
zap.String("useragent", ua),
zap.String("method", string(ctx.Method())),
zap.String("url", string(ctx.RequestURI())),
zap.String("referer", string(ctx.Referer())),
zap.Int("status_code", ctx.Response.StatusCode()),
zap.String("content_type", string(ctx.Response.Header.ContentType())),
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)),
)
}
}
type metrics interface {
IncrementTotalRequests()
ObserveRequestDuration(t time.Duration)
}
func DurationMetrics(h fasthttp.RequestHandler, m metrics) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var startedAt = time.Now()
h(ctx)
m.IncrementTotalRequests()
m.ObserveRequestDuration(time.Since(startedAt))
}
}

View File

@ -0,0 +1,114 @@
package core
import (
"strconv"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
type templatePicker interface {
// Pick the template name for responding.
Pick() string
}
type renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
func RespondWithErrorPage( //nolint:funlen
ctx *fasthttp.RequestCtx,
cfg *config.Config,
p templatePicker,
rdr renderer,
pageCode string,
httpCode int,
showRequestDetails bool,
) {
ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing
var (
clientWant = ClientWantFormat(ctx)
json, canJSON = cfg.JSONFormat()
xml, canXML = cfg.XMLFormat()
props = tpl.Properties{Code: pageCode, ShowRequestDetails: showRequestDetails}
)
if showRequestDetails {
props.OriginalURI = string(ctx.Request.Header.Peek(OriginalURI))
props.Namespace = string(ctx.Request.Header.Peek(Namespace))
props.IngressName = string(ctx.Request.Header.Peek(IngressName))
props.ServiceName = string(ctx.Request.Header.Peek(ServiceName))
props.ServicePort = string(ctx.Request.Header.Peek(ServicePort))
props.RequestID = string(ctx.Request.Header.Peek(RequestID))
props.ForwardedFor = string(ctx.Request.Header.Peek(ForwardedFor))
props.Host = string(ctx.Request.Header.Peek(Host))
}
if page, exists := cfg.Pages[pageCode]; exists {
props.Message = page.Message()
props.Description = page.Description()
} else if c, err := strconv.Atoi(pageCode); err == nil {
if s := fasthttp.StatusMessage(c); s != "Unknown Status Code" { // as a fallback
props.Message = s
}
}
SetClientFormat(ctx, PlainTextContentType) // set default content type
if props.Message == "" {
ctx.SetStatusCode(fasthttp.StatusNotFound)
_, _ = ctx.WriteString("requested pageCode (" + pageCode + ") not available")
return
}
switch {
case clientWant == JSONContentType && canJSON: // JSON
{
SetClientFormat(ctx, JSONContentType)
if content, err := rdr.Render(json.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render JSON template: " + err.Error())
}
}
case clientWant == XMLContentType && canXML: // XML
{
SetClientFormat(ctx, XMLContentType)
if content, err := rdr.Render(xml.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render XML template: " + err.Error())
}
}
default: // HTML
{
SetClientFormat(ctx, HTMLContentType)
var templateName = p.Pick()
if template, exists := cfg.Template(templateName); exists {
if content, err := rdr.Render(template.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render HTML template: " + err.Error())
}
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("template " + templateName + " not exists")
}
}
}
}

View File

@ -0,0 +1,102 @@
package core
import (
"bytes"
"sort"
"strconv"
"github.com/valyala/fasthttp"
)
type ContentType = byte
const (
UnknownContentType ContentType = iota // should be first
JSONContentType
XMLContentType
HTMLContentType
PlainTextContentType
)
func ClientWantFormat(ctx *fasthttp.RequestCtx) ContentType {
// parse "Content-Type" header (e.g.: `application/json;charset=UTF-8`)
if ct := bytes.ToLower(ctx.Request.Header.ContentType()); len(ct) > 4 { //nolint:gomnd
return mimeTypeToContentType(ct)
}
// parse `X-Format` header (aka `Accept`) for the Ingress support
// e.g.: `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8`
if h := bytes.ToLower(bytes.TrimSpace(ctx.Request.Header.Peek(FormatHeader))); len(h) > 2 { //nolint:gomnd,nestif
type format struct {
mimeType []byte
weight float32
}
var formats = make([]format, 0, 8) //nolint:gomnd
for _, b := range bytes.FieldsFunc(h, func(r rune) bool { return r == ',' }) {
if idx := bytes.Index(b, []byte(";q=")); idx > 0 && idx < len(b) {
f := format{b[0:idx], 0}
if len(b) > idx+3 {
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil { //nolint:gomnd
f.weight = float32(weight)
}
}
formats = append(formats, f)
} else {
formats = append(formats, format{b, 1})
}
}
switch l := len(formats); {
case l == 0:
return UnknownContentType
case l == 1:
return mimeTypeToContentType(formats[0].mimeType)
default:
sort.SliceStable(formats, func(i, j int) bool { return formats[i].weight > formats[j].weight })
return mimeTypeToContentType(formats[0].mimeType)
}
}
return UnknownContentType
}
func mimeTypeToContentType(mimeType []byte) ContentType {
switch {
case bytes.Contains(mimeType, []byte("application/json")), bytes.Contains(mimeType, []byte("text/json")):
return JSONContentType
case bytes.Contains(mimeType, []byte("application/xml")), bytes.Contains(mimeType, []byte("text/xml")):
return XMLContentType
case bytes.Contains(mimeType, []byte("text/html")):
return HTMLContentType
case bytes.Contains(mimeType, []byte("text/plain")):
return PlainTextContentType
}
return UnknownContentType
}
func SetClientFormat(ctx *fasthttp.RequestCtx, t ContentType) {
switch t {
case JSONContentType:
ctx.SetContentType("application/json; charset=utf-8")
case XMLContentType:
ctx.SetContentType("application/xml; charset=utf-8")
case HTMLContentType:
ctx.SetContentType("text/html; charset=utf-8")
case PlainTextContentType:
ctx.SetContentType("text/plain; charset=utf-8")
}
}

View File

@ -0,0 +1,117 @@
package core_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/valyala/fasthttp"
)
func TestClientWantFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentTypeHeader string
giveFormatHeader string
giveReqCtx func() *fasthttp.RequestCtx
wantFormat core.ContentType
}{
"priority": {
giveFormatHeader: "application/xml",
giveContentTypeHeader: "text/plain",
wantFormat: core.PlainTextContentType,
},
"format respects weight": {
giveFormatHeader: "text/html;q=0.5,application/xhtml+xml;q=0.9,application/xml;q=1,*/*;q=0.8",
wantFormat: core.XMLContentType,
},
"wrong format value": {
giveFormatHeader: ";q=foobar,bar/baz;;;;;application/xml",
wantFormat: core.UnknownContentType,
},
"content type - application/json": {
giveContentTypeHeader: "application/jsoN; charset=utf-8", wantFormat: core.JSONContentType,
},
"content type - text/json": {
giveContentTypeHeader: "text/Json; charset=utf-8", wantFormat: core.JSONContentType,
},
"format - json": {
giveFormatHeader: "application/jsoN,*/*;q=0.8", wantFormat: core.JSONContentType,
},
"content type - application/xml": {
giveContentTypeHeader: "application/xmL; charset=utf-8", wantFormat: core.XMLContentType,
},
"content type - text/xml": {
giveContentTypeHeader: "text/Xml; charset=utf-8", wantFormat: core.XMLContentType,
},
"format - xml": {
giveFormatHeader: "text/Xml", wantFormat: core.XMLContentType,
},
"content type - text/html": {
giveContentTypeHeader: "text/htMl; charset=utf-8", wantFormat: core.HTMLContentType,
},
"format - html": {
giveFormatHeader: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
wantFormat: core.HTMLContentType,
},
"content type - text/plain": {
giveContentTypeHeader: "text/plaiN; charset=utf-8", wantFormat: core.PlainTextContentType,
},
"format - plain": {
giveFormatHeader: "text/plaiN,text/html,application/xml;q=0.9,,,*/*;q=0.8", wantFormat: core.PlainTextContentType,
},
"unknown on empty": {
wantFormat: core.UnknownContentType,
},
"unknown on foo/bar": {
giveContentTypeHeader: "foo/bar; charset=utf-8",
giveFormatHeader: "foo/bar; charset=utf-8",
wantFormat: core.UnknownContentType,
},
} {
t.Run(name, func(t *testing.T) {
h := &fasthttp.RequestHeader{}
h.Set(fasthttp.HeaderContentType, tt.giveContentTypeHeader)
h.Set(core.FormatHeader, tt.giveFormatHeader)
ctx := &fasthttp.RequestCtx{
Request: fasthttp.Request{
Header: *h, //nolint:govet
},
}
assert.Equal(t, tt.wantFormat, core.ClientWantFormat(ctx))
})
}
}
func TestSetClientFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentType core.ContentType
wantHeaderValue string
}{
"plain on unknown": {giveContentType: core.UnknownContentType, wantHeaderValue: "text/plain; charset=utf-8"},
"json": {giveContentType: core.JSONContentType, wantHeaderValue: "application/json; charset=utf-8"},
"xml": {giveContentType: core.XMLContentType, wantHeaderValue: "application/xml; charset=utf-8"},
"html": {giveContentType: core.HTMLContentType, wantHeaderValue: "text/html; charset=utf-8"},
"plain": {giveContentType: core.PlainTextContentType, wantHeaderValue: "text/plain; charset=utf-8"},
} {
t.Run(name, func(t *testing.T) {
ctx := &fasthttp.RequestCtx{
Response: fasthttp.Response{
Header: fasthttp.ResponseHeader{},
},
}
assert.Empty(t, "", ctx.Response.Header.Peek(fasthttp.HeaderContentType))
core.SetClientFormat(ctx, tt.giveContentType)
assert.Equal(t, tt.wantHeaderValue, string(ctx.Response.Header.Peek(fasthttp.HeaderContentType)))
})
}
}

View File

@ -0,0 +1,33 @@
package core
const (
// FormatHeader name of the header used to extract the format
FormatHeader = "X-Format"
// CodeHeader name of the header used as source of the HTTP status code to return
CodeHeader = "X-Code"
// OriginalURI name of the header with the original URL from NGINX
OriginalURI = "X-Original-URI"
// Namespace name of the header that contains information about the Ingress namespace
Namespace = "X-Namespace"
// IngressName name of the header that contains the matched Ingress
IngressName = "X-Ingress-Name"
// ServiceName name of the header that contains the matched Service in the Ingress
ServiceName = "X-Service-Name"
// ServicePort name of the header that contains the matched Service port in the Ingress
ServicePort = "X-Service-Port"
// RequestID is a unique ID that identifies the request - same as for backend service
RequestID = "X-Request-ID"
// ForwardedFor identifies the user of this session
ForwardedFor = "X-Forwarded-For"
// Host identifies the hosts origin
Host = "Host"
)

View File

@ -1,85 +1,33 @@
package errorpage
import (
"math/rand"
"sort"
"time"
"github.com/pkg/errors"
"github.com/tarampampam/error-pages/internal/http/common"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
const (
UseRandom = "random"
UseRandomOnEachRequest = "i-said-random"
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for error pages serving.
func NewHandler(
templateName string,
templates map[string][]byte,
codes map[string]tpl.Annotator,
) (fasthttp.RequestHandler, error) {
if len(templates) == 0 {
return nil, errors.New("empty templates map")
}
var (
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
templateNames = templateTames(templates)
)
if templateName == "" { // on empty template name
templateName = templateNames[0] // pick the first
} else if templateName == UseRandom { // on "random" template name
templateName = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
}
if _, found := templates[templateName]; !found && templateName != UseRandomOnEachRequest {
return nil, errors.New("wrong template name passed")
}
var pages = tpl.NewErrors(templates, codes)
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, showRequestDetails bool) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var useTemplate = templateName // default
if templateName == UseRandomOnEachRequest {
useTemplate = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
}
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
if code, ok := ctx.UserValue("code").(string); ok {
if content, err := pages.Get(useTemplate, code); err == nil {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetContentType("text/html; charset=utf-8")
_, _ = ctx.Write(content)
} else {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"requested code not available: "+err.Error(),
)
}
} else { // will never happen
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusInternalServerError,
"cannot extract requested code from the request",
)
core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, showRequestDetails)
} else { // will never occur
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot extract requested code from the request")
}
}, nil
}
func templateTames(templates map[string][]byte) []string {
var templateNames = make([]string, 0, len(templates))
for name := range templates {
templateNames = append(templateNames, name)
}
sort.Strings(templateNames)
return templateNames
}

View File

@ -19,5 +19,6 @@ func NewHandler(checker checker) fasthttp.RequestHandler {
}
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.WriteString("OK")
}
}

View File

@ -0,0 +1,55 @@
package index
import (
"strconv"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for the index page serving.
func NewHandler(
cfg *config.Config,
p templatePicker,
rdr renderer,
defaultPageCode string,
defaultHTTPCode uint16,
showRequestDetails bool,
) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
pageCode, httpCode := defaultPageCode, int(defaultHTTPCode)
if returnCode, ok := extractCodeToReturn(ctx); ok {
pageCode, httpCode = strconv.Itoa(returnCode), returnCode
}
core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, showRequestDetails)
}
}
func extractCodeToReturn(ctx *fasthttp.RequestCtx) (int, bool) { // for the Ingress support
var ch = ctx.Request.Header.Peek(core.CodeHeader)
if len(ch) > 0 && len(ch) <= 3 {
if code, err := strconv.Atoi(string(ch)); err == nil {
if code > 0 && code <= 599 {
return code, true
}
}
}
return 0, false
}

View File

@ -1,4 +1,4 @@
package common_test
package index_test
import "testing"

View File

@ -0,0 +1,16 @@
// Package metrics contains HTTP handler for application metrics (prometheus format) generation.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
// NewHandler creates metrics handler.
func NewHandler(registry prometheus.Gatherer) fasthttp.RequestHandler {
return fasthttpadaptor.NewFastHTTPHandler(
promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}),
)
}

View File

@ -0,0 +1,7 @@
package metrics_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -0,0 +1,14 @@
package notfound
import (
"github.com/valyala/fasthttp"
)
// NewHandler creates handler missing requests handling.
func NewHandler() fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.SetContentType("text/plain; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
_, _ = ctx.WriteString("Wrong request URL. Error pages are available at the following URLs: /{code}.html")
}
}

View File

@ -0,0 +1,7 @@
package notfound_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,16 +1,20 @@
package http
import (
"context"
"strconv"
"time"
"github.com/fasthttp/router"
"github.com/tarampampam/error-pages/internal/checkers"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/http/common"
errorpageHandler "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
healthzHandler "github.com/tarampampam/error-pages/internal/http/handlers/healthz"
indexHandler "github.com/tarampampam/error-pages/internal/http/handlers/index"
metricsHandler "github.com/tarampampam/error-pages/internal/http/handlers/metrics"
notfoundHandler "github.com/tarampampam/error-pages/internal/http/handlers/notfound"
versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version"
"github.com/tarampampam/error-pages/internal/metrics"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/tarampampam/error-pages/internal/version"
"github.com/valyala/fasthttp"
@ -18,18 +22,20 @@ import (
)
type Server struct {
log *zap.Logger
fast *fasthttp.Server
router *router.Router
rdr *tpl.TemplateRenderer
}
const (
defaultWriteTimeout = time.Second * 7
defaultReadTimeout = time.Second * 7
defaultIdleTimeout = time.Second * 15
defaultWriteTimeout = time.Second * 4
defaultReadTimeout = time.Second * 4
defaultIdleTimeout = time.Second * 6
)
func NewServer(log *zap.Logger) Server {
r := router.New()
rdr := tpl.NewTemplateRenderer()
return Server{
// fasthttp docs: <https://github.com/valyala/fasthttp>
@ -37,13 +43,14 @@ func NewServer(log *zap.Logger) Server {
WriteTimeout: defaultWriteTimeout,
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
Handler: common.LogRequest(r.Handler, log),
NoDefaultServerHeader: true,
ReduceMemoryUsage: true,
CloseOnShutdown: true,
Logger: zap.NewStdLog(log),
},
router: r,
router: router.New(),
log: log,
rdr: rdr,
}
}
@ -52,55 +59,50 @@ func (s *Server) Start(ip string, port uint16) error {
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
}
type templatePicker interface {
// Pick the template name for responding.
Pick() string
}
// Register server routes, middlewares, etc.
// Router docs: <https://github.com/fasthttp/router>
func (s *Server) Register(
templateName string,
templates map[string][]byte,
codes map[string]tpl.Annotator,
cfg *config.Config,
templatePicker templatePicker,
defaultPageCode string,
defaultHTTPCode uint16,
showDetails bool,
) error {
s.router.GET("/", func(ctx *fasthttp.RequestCtx) {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"Hi there! Error pages are available at the following URLs: /{code}.html",
)
})
reg, m := metrics.NewRegistry(), metrics.NewMetrics()
s.router.NotFound = func(ctx *fasthttp.RequestCtx) {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"Wrong request URL. Error pages are available at the following URLs: /{code}.html",
)
}
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
if h, err := errorpageHandler.NewHandler(templateName, templates, codes); err != nil {
if err := m.Register(reg); err != nil {
return err
} else {
s.router.GET("/{code}.html", h)
}
s.fast.Handler = common.DurationMetrics(common.LogRequest(s.router.Handler, s.log), &m)
s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, defaultPageCode, defaultHTTPCode, showDetails)) //nolint:lll
s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails))
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker())
s.router.ANY("/healthz", liveHandler)
s.router.ANY("/health/live", liveHandler) // deprecated
s.router.GET("/metrics", metricsHandler.NewHandler(reg))
s.router.NotFound = notfoundHandler.NewHandler()
return nil
}
// Stop server.
func (s *Server) Stop(timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout) // TODO replace with simple time.After
defer cancel()
func (s *Server) Stop() error {
if err := s.rdr.Close(); err != nil {
defer func() { _ = s.fast.Shutdown() }()
ch := make(chan error, 1) // channel for server stopping error
go func() { defer close(ch); ch <- s.fast.Shutdown() }()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
return s.fast.Shutdown()
}

View File

@ -0,0 +1,52 @@
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
type Metrics struct {
total prometheus.Counter
duration prometheus.Histogram
}
// NewMetrics creates new Metrics collector.
func NewMetrics() Metrics {
const namespace, subsystem = "http", "requests"
return Metrics{
total: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "total_count",
Help: "Counter of HTTP requests made.",
}),
duration: prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "duration_milliseconds",
Help: "Histogram of the time (in milliseconds) each request took.",
Buckets: append([]float64{.001, .003}, prometheus.DefBuckets...),
}),
}
}
// IncrementTotalRequests increments total requests counter.
func (w *Metrics) IncrementTotalRequests() { w.total.Inc() }
// ObserveRequestDuration observer requests duration histogram.
func (w *Metrics) ObserveRequestDuration(t time.Duration) { w.duration.Observe(t.Seconds()) }
// Register metrics with registerer.
func (w *Metrics) Register(reg prometheus.Registerer) error {
if err := reg.Register(w.total); err != nil {
return err
}
if err := reg.Register(w.duration); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,70 @@
package metrics_test
import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/metrics"
)
func TestMetrics_Register(t *testing.T) {
var (
registry = prometheus.NewRegistry()
m = metrics.NewMetrics()
)
assert.NoError(t, m.Register(registry))
count, err := testutil.GatherAndCount(registry,
"http_requests_total_count",
"http_requests_duration_milliseconds",
)
assert.NoError(t, err)
assert.Equal(t, 2, count)
}
func TestMetrics_IncrementTotalRequests(t *testing.T) {
p := metrics.NewMetrics()
p.IncrementTotalRequests()
metric := getMetric(t, &p, "http_requests_total_count")
assert.Equal(t, float64(1), metric.Counter.GetValue())
}
func TestMetrics_ObserveRequestDuration(t *testing.T) {
p := metrics.NewMetrics()
p.ObserveRequestDuration(time.Second)
metric := getMetric(t, &p, "http_requests_duration_milliseconds")
assert.Equal(t, float64(1), metric.Histogram.GetSampleSum())
}
type registerer interface {
Register(prometheus.Registerer) error
}
func getMetric(t *testing.T, reg registerer, name string) *dto.Metric {
t.Helper()
registry := prometheus.NewRegistry()
_ = reg.Register(registry)
families, _ := registry.Gather()
for _, family := range families {
if family.GetName() == name {
return family.Metric[0]
}
}
assert.FailNowf(t, "cannot resolve metric for: %s", name)
return nil
}

View File

@ -0,0 +1,20 @@
// Package metrics contains custom prometheus metrics and registry factories.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
// NewRegistry creates new prometheus registry with pre-registered common collectors.
func NewRegistry() *prometheus.Registry {
registry := prometheus.NewRegistry()
// register common metric collectors
registry.MustRegister(
// collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
return registry
}

View File

@ -0,0 +1,18 @@
package metrics_test
import (
"testing"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/metrics"
)
func TestNewRegistry(t *testing.T) {
registry := metrics.NewRegistry()
count, err := testutil.GatherAndCount(registry)
assert.NoError(t, err)
assert.True(t, count >= 6, "not enough common metrics")
}

83
internal/pick/picker.go Normal file
View File

@ -0,0 +1,83 @@
package pick
import (
"math/rand"
"sync"
"time"
)
type pickMode = byte
const (
First pickMode = 1 + iota // Always pick the first element (index = 0)
RandomOnce // Pick random element once (any future Pick calls will return the same element)
RandomEveryTime // Always Pick the random element
)
type picker struct {
mode pickMode
rand *rand.Rand // will be nil for the First pick mode
maxIdx uint32
mu sync.Mutex
lastIdx uint32
}
const unsetIdx uint32 = 4294967295
func NewPicker(maxIdx uint32, mode pickMode) *picker {
var p = &picker{
maxIdx: maxIdx,
mode: mode,
lastIdx: unsetIdx,
}
if mode != First {
p.rand = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
}
return p
}
// NextIndex returns an index for the next element (based on pickMode).
func (p *picker) NextIndex() uint32 {
if p.maxIdx == 0 {
return 0
}
switch p.mode {
case First:
return 0
case RandomOnce:
if p.lastIdx == unsetIdx {
p.mu.Lock()
defer p.mu.Unlock()
p.lastIdx = uint32(p.rand.Intn(int(p.maxIdx)))
}
return p.lastIdx
case RandomEveryTime:
var idx = uint32(p.rand.Intn(int(p.maxIdx + 1)))
p.mu.Lock()
defer p.mu.Unlock()
if idx == p.lastIdx {
p.lastIdx++
} else {
p.lastIdx = idx
}
if p.lastIdx > p.maxIdx { // overflow?
p.lastIdx = 0
}
return p.lastIdx
default:
panic("picker.NextIndex(): unsupported mode")
}
}

View File

@ -0,0 +1,57 @@
package pick_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/pick"
)
func TestPicker_NextIndex_First(t *testing.T) {
for i := uint32(0); i < 100; i++ {
p := pick.NewPicker(i, pick.First)
for j := uint8(0); j < 100; j++ {
assert.Equal(t, uint32(0), p.NextIndex())
}
}
}
func TestPicker_NextIndex_RandomOnce(t *testing.T) {
for i := uint8(0); i < 10; i++ {
assert.Equal(t, uint32(0), pick.NewPicker(0, pick.RandomOnce).NextIndex())
}
for i := uint8(10); i < 100; i++ {
p := pick.NewPicker(uint32(i), pick.RandomOnce)
next := p.NextIndex()
assert.LessOrEqual(t, next, uint32(i))
for j := uint8(0); j < 100; j++ {
assert.Equal(t, next, p.NextIndex())
}
}
}
func TestPicker_NextIndex_RandomEveryTime(t *testing.T) {
for i := uint8(0); i < 10; i++ {
assert.Equal(t, uint32(0), pick.NewPicker(0, pick.RandomEveryTime).NextIndex())
}
for i := uint8(1); i < 100; i++ {
p := pick.NewPicker(uint32(i), pick.RandomEveryTime)
for j := uint8(0); j < 100; j++ {
one, two := p.NextIndex(), p.NextIndex()
assert.LessOrEqual(t, one, uint32(i))
assert.LessOrEqual(t, two, uint32(i))
assert.NotEqual(t, one, two)
}
}
}
func TestPicker_NextIndex_Unsupported(t *testing.T) {
assert.Panics(t, func() { pick.NewPicker(1, 255).NextIndex() })
}

View File

@ -0,0 +1,20 @@
package pick
type StringsSlice struct {
s []string
p *picker
}
// NewStringsSlice creates new StringsSlice.
func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
return &StringsSlice{s: items, p: NewPicker(uint32(len(items)-1), mode)}
}
// Pick an element from the strings slice.
func (s *StringsSlice) Pick() string {
if len(s.s) == 0 {
return ""
}
return s.s[s.p.NextIndex()]
}

View File

@ -0,0 +1,49 @@
package pick_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/pick"
)
func TestStringsSlice_Pick(t *testing.T) {
t.Run("first", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.First).Pick())
}
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.First)
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "foo", p.Pick())
}
})
t.Run("random once", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomOnce).Pick())
}
var (
p = pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomOnce)
picked = p.Pick()
)
for i := uint8(0); i < 100; i++ {
assert.Equal(t, picked, p.Pick())
}
})
t.Run("random every time", func(t *testing.T) {
for i := uint8(0); i < 100; i++ {
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomEveryTime).Pick())
}
for i := uint8(0); i < 100; i++ {
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomEveryTime)
assert.NotEqual(t, p.Pick(), p.Pick())
}
})
}

View File

@ -1,102 +0,0 @@
package tpl
import (
"errors"
"sync"
)
// Annotator allows to annotate error code.
type Annotator struct {
Message string
Description string
}
// Errors is a "cached storage" for the rendered error pages for the different templates and codes.
type Errors struct {
templates map[string][]byte
codes map[string]Annotator
cacheMu sync.RWMutex
cache map[string]map[string][]byte // map[template]map[code]content
}
// NewErrors creates new Errors.
func NewErrors(templates map[string][]byte, codes map[string]Annotator) *Errors {
return &Errors{
templates: templates,
codes: codes,
cache: make(map[string]map[string][]byte),
}
}
func (e *Errors) existsInCache(template, code string) ([]byte, bool) {
e.cacheMu.RLock()
defer e.cacheMu.RUnlock()
if codes, tplOk := e.cache[template]; tplOk {
if content, codeOk := codes[code]; codeOk {
return content, true
}
}
return nil, false
}
func (e *Errors) putInCache(template, code string) error {
if _, ok := e.templates[template]; !ok {
return errors.New("template \"" + template + "\" does not exists")
}
if _, ok := e.codes[code]; !ok {
return errors.New("code \"" + code + "\" does not exists")
}
e.cacheMu.Lock()
defer e.cacheMu.Unlock()
if _, ok := e.cache[template]; !ok {
e.cache[template] = make(map[string][]byte)
}
e.cache[template][code] = Replace(e.templates[template], Replaces{
Code: code,
Message: e.codes[code].Message,
Description: e.codes[code].Description,
})
return nil
}
// Get the rendered error page content.
func (e *Errors) Get(template, code string) ([]byte, error) {
if content, ok := e.existsInCache(template, code); ok {
return content, nil
}
if err := e.putInCache(template, code); err != nil {
return nil, err
}
e.cacheMu.RLock()
defer e.cacheMu.RUnlock()
return e.cache[template][code], nil
}
// VisitAll allows to iterate all possible error pages and templates.
func (e *Errors) VisitAll(fn func(template, code string, content []byte) error) error {
for tpl := range e.templates {
for code := range e.codes {
content, err := e.Get(tpl, code)
if err != nil {
return err // will never happen
}
if err = fn(tpl, code, content); err != nil {
return err
}
}
}
return nil
}

View File

@ -1,106 +0,0 @@
package tpl_test
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func TestErrors_Get(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
)
content, err := e.Get("foo", "200")
assert.NoError(t, err)
assert.Equal(t, "200: ok all is ok", string(content))
content, err = e.Get("foo", "666")
assert.EqualError(t, err, "code \"666\" does not exists")
assert.Nil(t, content)
content, err = e.Get("bar", "200")
assert.EqualError(t, err, "template \"bar\" does not exists")
assert.Nil(t, content)
}
func TestErrors_GetConcurrent(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
)
var wg sync.WaitGroup
for i := 0; i < 1234; i++ {
wg.Add(1)
go func() {
defer wg.Done()
content, err := e.Get("foo", "200")
assert.NoError(t, err)
assert.Equal(t, "200: ok all is ok", string(content))
content, err = e.Get("foo", "666")
assert.Error(t, err)
assert.Nil(t, content)
}()
}
wg.Wait()
}
func TestErrors_VisitAll(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
"bar": []byte("{{ code }}: {{ message }} {{ description }}"),
},
map[string]tpl.Annotator{
"200": {"ok", "all is ok"},
"400": {"Bad Request", "The server did not understand the request"},
},
)
visited := make(map[string]map[string]bool) // map[template]codes
assert.NoError(t, e.VisitAll(func(template, code string, content []byte) error {
if _, ok := visited[template]; !ok {
visited[template] = make(map[string]bool)
}
visited[template][code] = true
assert.NotNil(t, content)
return nil
}))
assert.Len(t, visited, 2)
assert.Len(t, visited["foo"], 2)
assert.True(t, visited["foo"]["200"])
assert.True(t, visited["foo"]["400"])
assert.Len(t, visited["bar"], 2)
assert.True(t, visited["bar"]["200"])
assert.True(t, visited["bar"]["400"])
}
func TestErrors_VisitAllWillReturnTheError(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
},
map[string]tpl.Annotator{
"200": {"ok", "all is ok"},
},
)
assert.EqualError(t, e.VisitAll(func(template, code string, content []byte) error {
return errors.New("foo error")
}), "foo error")
}

25
internal/tpl/hasher.go Normal file
View File

@ -0,0 +1,25 @@
package tpl
import (
"bytes"
"crypto/md5" //nolint:gosec
"encoding/gob"
)
const hashLength = 16 // md5 hash length
type Hash [hashLength]byte
func HashStruct(s interface{}) (Hash, error) {
var b bytes.Buffer
if err := gob.NewEncoder(&b).Encode(s); err != nil {
return Hash{}, err
}
return md5.Sum(b.Bytes()), nil //nolint:gosec
}
func HashBytes(b []byte) Hash {
return md5.Sum(b) //nolint:gosec
}

View File

@ -0,0 +1,35 @@
package tpl_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func TestHashBytes(t *testing.T) {
assert.NotEqual(t, tpl.HashBytes([]byte{1}), tpl.HashBytes([]byte{2}))
}
func TestHashStruct(t *testing.T) {
type s struct {
S string
I int
B bool
}
h1, err1 := tpl.HashStruct(s{S: "foo", I: 1, B: false})
assert.NoError(t, err1)
h2, err2 := tpl.HashStruct(s{S: "bar", I: 2, B: true})
assert.NoError(t, err2)
assert.NotEqual(t, h1, h2)
type p struct { // no exported fields
any string
}
_, err := tpl.HashStruct(p{any: "foo"})
assert.Error(t, err)
}

View File

@ -0,0 +1,39 @@
package tpl
import (
"reflect"
)
type Properties struct { // only string properties with a "token" tag, please
Code string `token:"code"`
Message string `token:"message"`
Description string `token:"description"`
OriginalURI string `token:"original_uri"`
Namespace string `token:"namespace"`
IngressName string `token:"ingress_name"`
ServiceName string `token:"service_name"`
ServicePort string `token:"service_port"`
RequestID string `token:"request_id"`
ForwardedFor string `token:"forwarded_for"`
Host string `token:"host"`
ShowRequestDetails bool
}
// Replaces return a map with strings for the replacing, where the map key is a token.
func (p *Properties) Replaces() map[string]string {
var replaces = make(map[string]string, reflect.ValueOf(*p).NumField())
for i, v := 0, reflect.ValueOf(*p); i < v.NumField(); i++ {
if keyword, tagExists := v.Type().Field(i).Tag.Lookup("token"); tagExists {
if sv, isString := v.Field(i).Interface().(string); isString && len(sv) > 0 {
replaces[keyword] = sv
} else {
replaces[keyword] = ""
}
}
}
return replaces
}
func (p *Properties) Hash() (Hash, error) { return HashStruct(p) }

View File

@ -0,0 +1,66 @@
package tpl_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func TestProperties_Replaces(t *testing.T) {
props := tpl.Properties{
Code: "foo",
Message: "bar",
Description: "baz",
OriginalURI: "aaa",
Namespace: "bbb",
IngressName: "ccc",
ServiceName: "ddd",
ServicePort: "eee",
RequestID: "fff",
ForwardedFor: "ggg",
Host: "hhh",
}
r := props.Replaces()
assert.Equal(t, "foo", r["code"])
assert.Equal(t, "bar", r["message"])
assert.Equal(t, "baz", r["description"])
assert.Equal(t, "aaa", r["original_uri"])
assert.Equal(t, "bbb", r["namespace"])
assert.Equal(t, "ccc", r["ingress_name"])
assert.Equal(t, "ddd", r["service_name"])
assert.Equal(t, "eee", r["service_port"])
assert.Equal(t, "fff", r["request_id"])
assert.Equal(t, "ggg", r["forwarded_for"])
assert.Equal(t, "hhh", r["host"])
props.Code, props.Message, props.Description = "", "", ""
r = props.Replaces()
assert.Equal(t, "", r["code"])
assert.Equal(t, "", r["message"])
assert.Equal(t, "", r["description"])
}
func TestProperties_Hash(t *testing.T) {
props1 := tpl.Properties{Code: "123"}
props2 := tpl.Properties{Code: "123"}
hash1, err := props1.Hash()
assert.NoError(t, err)
hash2, err := props2.Hash()
assert.NoError(t, err)
assert.Equal(t, hash1, hash2)
props2.Code = "321"
hash2, err = props2.Hash()
assert.NoError(t, err)
assert.NotEqual(t, hash1, hash2)
}

216
internal/tpl/render.go Normal file
View File

@ -0,0 +1,216 @@
package tpl
import (
"bytes"
"encoding/json"
"os"
"strconv"
"sync"
"text/template"
"time"
"github.com/pkg/errors"
"github.com/tarampampam/error-pages/internal/version"
)
// These functions are always allowed in the templates.
var tplFnMap = template.FuncMap{ //nolint:gochecknoglobals
"now": time.Now,
"hostname": os.Hostname,
"json": func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }, //nolint:nlreturn
"version": version.Version,
"int": func(v interface{}) int {
if s, ok := v.(string); ok {
if i, err := strconv.Atoi(s); err == nil {
return i
}
} else if i, ok := v.(int); ok {
return i
}
return 0
},
}
var ErrClosed = errors.New("closed")
type TemplateRenderer struct {
cacheMu sync.RWMutex
cache map[cacheEntryHash]cacheItem // map key is a unique hash
cacheCleanupInterval time.Duration
cacheItemLifetime time.Duration
close chan struct{}
closedMu sync.RWMutex
closed bool
}
type (
cacheEntryHash = [hashLength * 2]byte // two md5 hashes
cacheItem struct {
data []byte
expiresAtNano int64
}
)
const (
cacheCleanupInterval = time.Second
cacheItemLifetime = time.Second * 2
)
// NewTemplateRenderer returns new template renderer. Don't forget to call Close() function!
func NewTemplateRenderer() *TemplateRenderer {
tr := &TemplateRenderer{
cache: make(map[cacheEntryHash]cacheItem),
cacheCleanupInterval: cacheCleanupInterval,
cacheItemLifetime: cacheItemLifetime,
close: make(chan struct{}, 1),
}
go tr.cleanup()
return tr
}
func (tr *TemplateRenderer) cleanup() {
defer close(tr.close)
timer := time.NewTimer(tr.cacheCleanupInterval)
defer timer.Stop()
for {
select {
case <-tr.close:
tr.cacheMu.Lock()
for hash := range tr.cache {
delete(tr.cache, hash)
}
tr.cacheMu.Unlock()
return
case <-timer.C:
tr.cacheMu.Lock()
var now = time.Now().UnixNano()
for hash, item := range tr.cache {
if now > item.expiresAtNano {
delete(tr.cache, hash)
}
}
tr.cacheMu.Unlock()
timer.Reset(tr.cacheCleanupInterval)
}
}
}
func (tr *TemplateRenderer) Render(content []byte, props Properties) ([]byte, error) { //nolint:funlen
if tr.isClosed() {
return nil, ErrClosed
}
if len(content) == 0 {
return content, nil
}
var (
cacheKey cacheEntryHash
cacheKeyInit bool
)
if propsHash, err := props.Hash(); err == nil {
cacheKeyInit, cacheKey = true, tr.mixHashes(propsHash, HashBytes(content))
tr.cacheMu.RLock()
item, hit := tr.cache[cacheKey]
tr.cacheMu.RUnlock()
if hit {
// cache item has been expired?
if time.Now().UnixNano() > item.expiresAtNano {
tr.cacheMu.Lock()
delete(tr.cache, cacheKey)
tr.cacheMu.Unlock()
} else {
return item.data, nil
}
}
}
var funcMap = template.FuncMap{
"show_details": func() bool { return props.ShowRequestDetails },
"hide_details": func() bool { return !props.ShowRequestDetails },
}
// make a copy of template functions map
for s, i := range tplFnMap {
funcMap[s] = i
}
// and allow the direct calling of Properties tokens, e.g. `{{ code | json }}`
for what, with := range props.Replaces() {
var n, s = what, with
funcMap[n] = func() string { return s }
}
t, err := template.New("").Funcs(funcMap).Parse(string(content))
if err != nil {
return nil, err
}
var buf bytes.Buffer
if err = t.Execute(&buf, props); err != nil {
return nil, err
}
b := buf.Bytes()
if cacheKeyInit {
tr.cacheMu.Lock()
tr.cache[cacheKey] = cacheItem{
data: b,
expiresAtNano: time.Now().UnixNano() + tr.cacheItemLifetime.Nanoseconds(),
}
tr.cacheMu.Unlock()
}
return b, nil
}
func (tr *TemplateRenderer) isClosed() (closed bool) {
tr.closedMu.RLock()
closed = tr.closed
tr.closedMu.RUnlock()
return
}
func (tr *TemplateRenderer) Close() error {
if tr.isClosed() {
return ErrClosed
}
tr.closedMu.Lock()
tr.closed = true
tr.closedMu.Unlock()
tr.close <- struct{}{}
return nil
}
func (tr *TemplateRenderer) mixHashes(a, b Hash) (result cacheEntryHash) {
for i := 0; i < len(a); i++ {
result[i] = a[i]
}
for i := 0; i < len(b); i++ {
result[i+len(a)] = b[i]
}
return
}

119
internal/tpl/render_test.go Normal file
View File

@ -0,0 +1,119 @@
package tpl_test
import (
"math/rand"
"strconv"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func Test_Render(t *testing.T) {
renderer := tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
for name, tt := range map[string]struct {
giveContent string
giveProps tpl.Properties
wantContent string
wantError bool
}{
"common case": {
giveContent: "{{code}}: {{ message }} {{description}}",
giveProps: tpl.Properties{Code: "404", Message: "Not found", Description: "Blah"},
wantContent: "404: Not found Blah",
},
"html markup": {
giveContent: "<!-- comment --><html><body>{{code}}: {{ message }} {{description}}</body></html>",
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: "<!-- comment --><html><body>201: lorem ipsum </body></html>",
},
"with line breakers": {
giveContent: "\t {{code}}: {{ message }} {{description}}\n",
giveProps: tpl.Properties{},
wantContent: "\t : \n",
},
"golang template": {
giveContent: "\t {{code}} {{ .Code }}{{ if .Message }} Yeah {{end}}",
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: "\t 201 201 Yeah ",
},
"wrong golang template": {
giveContent: "{{ if foo() }} Test {{ end }}",
giveProps: tpl.Properties{},
wantError: true,
},
"json common case": {
giveContent: `{"code": {{code | json}}, "message": {"here":[ {{ message | json }} ]}, "desc": "{{description}}"}`,
giveProps: tpl.Properties{Code: `404'"{`, Message: "Not found\t\r\n"},
wantContent: `{"code": "404'\"{", "message": {"here":[ "Not found\t\r\n" ]}, "desc": ""}`,
},
"json golang template": {
giveContent: `{"code": "{{code}}", "message": {"here":[ "{{ if .Message }} Yeah {{end}}" ]}}`,
giveProps: tpl.Properties{Code: "201", Message: "lorem ipsum"},
wantContent: `{"code": "201", "message": {"here":[ " Yeah " ]}}`,
},
} {
t.Run(name, func(t *testing.T) {
content, err := renderer.Render([]byte(tt.giveContent), tt.giveProps)
if tt.wantError == true {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantContent, string(content))
}
})
}
}
func TestTemplateRenderer_Render_Concurrent(t *testing.T) {
renderer := tpl.NewTemplateRenderer()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
props := tpl.Properties{
Code: strconv.Itoa(rand.Intn(599-300+1) + 300), //nolint:gosec
Message: "Not found",
Description: "Blah",
}
content, err := renderer.Render([]byte("{{code}}: {{ message }} {{description}}"), props)
assert.NoError(t, err)
assert.NotEmpty(t, content)
}()
}
wg.Wait()
assert.NoError(t, renderer.Close())
assert.EqualError(t, renderer.Close(), tpl.ErrClosed.Error())
content, err := renderer.Render([]byte{}, tpl.Properties{})
assert.Nil(t, content)
assert.EqualError(t, err, tpl.ErrClosed.Error())
}
func BenchmarkRenderHTML(b *testing.B) {
b.ReportAllocs()
renderer := tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
for i := 0; i < b.N; i++ {
_, _ = renderer.Render(
[]byte("{{code}}: {{ message }} {{description}}"),
tpl.Properties{Code: "404", Message: "Not found", Description: "Blah"},
)
}
}

View File

@ -1,47 +0,0 @@
package tpl
import "bytes"
type Replaces struct {
Code string
Message string
Description string
}
const (
tknCode byte = iota + 1
tknMessage
tknDescription
)
var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
}
// Replace found tokens in the incoming slice with passed tokens.
func Replace(in []byte, re Replaces) []byte {
for tkn, set := range tknSets {
var replaceWith []byte
switch tkn {
case tknCode:
replaceWith = []byte(re.Code)
case tknMessage:
replaceWith = []byte(re.Message)
case tknDescription:
replaceWith = []byte(re.Description)
default:
panic("tpl: unsupported token")
}
if len(replaceWith) > 0 {
for i := 0; i < len(set); i++ {
in = bytes.ReplaceAll(in, set[i], replaceWith)
}
}
}
return in
}

View File

@ -1,66 +0,0 @@
package tpl_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func ExampleReplace() {
var in = []byte("{{ code }}: {{message}} ({{ description }})")
fmt.Println(string(tpl.Replace(in, tpl.Replaces{
Code: "400",
Message: "Bad Request",
Description: "The server did not understand the request",
})))
// Output:
// 400: Bad Request (The server did not understand the request)
}
func TestReplace(t *testing.T) {
for name, tt := range map[string]struct {
giveIn []byte
giveRe tpl.Replaces
wantResult []byte
}{
"common": {
giveIn: []byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "),
giveRe: tpl.Replaces{
Code: "123",
Message: "message",
Description: "desc",
},
wantResult: []byte("-- 123 123 __ message desc "),
},
"alpha and underline in the code": {
giveIn: []byte("\t{{ code }}\t"),
giveRe: tpl.Replaces{
Code: " qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ ",
},
wantResult: []byte("\t qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ \t"),
},
} {
tt := tt
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.wantResult, tpl.Replace(tt.giveIn, tt.giveRe))
})
}
}
func BenchmarkReplace(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.Replace([]byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "), tpl.Replaces{
Code: "123",
Message: "message",
Description: "desc",
})
}
}

View File

@ -0,0 +1,108 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Error-Pages config file schema",
"description": "Error-Pages config file schema.",
"type": "object",
"properties": {
"templates": {
"type": "array",
"description": "Templates list",
"items": {
"type": "object",
"description": "Template properties",
"properties": {
"path": {
"type": "string",
"description": "Path to the template file",
"examples": [
"./templates/ghost.html",
"/opt/tpl/ghost.htm"
]
},
"name": {
"type": "string",
"description": "Template name (optional, if path is defined)",
"examples": [
"ghost"
]
},
"content": {
"type": "string",
"description": "Template content, if path is not defined",
"examples": [
"<html><body>{{ code }}: {{ message }}</body></html>"
]
}
},
"additionalProperties": false
}
},
"formats": {
"type": "object",
"description": "Responses, based on requested content-type format",
"properties": {
"json": {
"type": "object",
"description": "JSON format",
"properties": {
"content": {
"type": "string",
"description": "JSON response body (template tags are allowed here)",
"examples": [
"{\"error\": true, \"code\": {{ code | json }}, \"message\": {{ message | json }}}"
]
}
},
"additionalProperties": false
},
"xml": {
"type": "object",
"description": "XML format",
"properties": {
"content": {
"type": "string",
"description": "XML response body (template tags are allowed here)",
"examples": [
"<?xml version=\"1.0\" encoding=\"utf-8\"?><error><code>{{ code }}</code><message>{{ message }}</message></error>"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"pages": {
"type": "object",
"description": "Error pages (codes)",
"patternProperties": {
"^[a-zA-Z0-9_-]+$": {
"type": "object",
"description": "Error page (code)",
"properties": {
"message": {
"type": "string",
"description": "Error page message (title)",
"examples": [
"Bad Request"
]
},
"description": {
"type": "string",
"description": "Error page description",
"examples": [
"The server did not understand the request"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"templates"
]
}

13
schemas/config/readme.md Normal file
View File

@ -0,0 +1,13 @@
# Config file schemas
These schemas describe Error Pages configuration file and used by:
- <https://github.com/SchemaStore/schemastore>
Schemas naming agreement: `<version_major>.<version_minor>.schema.json`.
## Contributing guide
If you want to modify the existing schema - your changes **MUST** be backward compatible. If your changes break backward compatibility - you **MUST** create a new schema file with a fresh version and "register" it in a [schemas catalog][schemas_catalog].
[schemas_catalog]:https://github.com/SchemaStore/schemastore/blob/master/src/api/json/catalog.json

15
schemas/readme.md Normal file
View File

@ -0,0 +1,15 @@
# Schemas
This directory contains public schemas for the most important parts of application.
**Do not rename or remove this directory or any file or directory inside.**
- You can validate existing config file using the following command:
```bash
$ docker run --rm -v "$(pwd):/src" -w "/src" node:16-alpine sh -c \
"npm install -g ajv-cli && \
ajv validate --all-errors --verbose \
-s ./schemas/config/1.0.schema.json \
-d ./error-pages.y*ml"
```

111
templates/cats.html Normal file
View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title>
<style>
html, body {
margin: 0;
height: 100vh;
background-color: #000;
color: #ddd;
font-family: 'Open Sans', sans-serif;
font-size: 0;
}
.centered {
height: 100vh;
align-items: center;
display: flex;
justify-content: center
}
.centered img {
max-width: 750px;
width: 100%;
}
/* {{ if show_details }} */
.details table {
width: 100%;
}
.details td {
font-size: 11px;
}
.details td.name {
text-align: right;
padding-right: .6em;
width: 50%;
}
.details td.value {
text-align: left;
padding-left: .6em;
font-family: 'Lucida Console', 'Courier New', monospace;
}
/* {{ end }} */
</style>
</head>
<body>
<div class="centered">
<!-- Pictures provider: <https://http.cat/> -->
<div>
<img src="https://http.cat/{{ code }}.jpg" alt="{{ message }}">
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name">Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name">Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name">Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name">Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name">Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name">Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name">Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name">Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name">Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
</body>
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
-->
</html>

View File

@ -11,38 +11,90 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet" />
<style>
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif}
.wrap {top:50%;left:50%;width:310px;height:260px;margin-left:-155px;margin-top:-110px;position:absolute;text-align:center}
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0}
.container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative}
.wrap {text-align:center}
.ghost {animation:float 3s ease-out infinite}
@keyframes float { 50% {transform:translate(0,20px)}}
.shadowFrame {width:130px;margin: 10px auto 0 auto}
.shadow {animation:shrink 3s ease-out infinite;transform-origin:center center}
@keyframes shrink {0%{width:90%;margin:0 5%} 50% {width:60%;margin:0 18%} 100% {width:90%;margin:0 5%}}
h3 {font-size:1.05em;text-transform: uppercase;margin:0.3em auto}
.description {font-size:0.8em;color:#aaa}
h3 {font-size:17px;text-transform: uppercase;margin:0.3em auto}
.description {font-size:13px;color:#aaa}
/* {{ if show_details }} */
.details {color:#999;width:100%}
.details table {width:100%}
.details td {white-space:nowrap;font-size:11px}
.details .name {text-align:right;padding-right:.6em;width:50%}
.details .value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="wrap">
<svg class="ghost" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="0 0 127.433 132.743" enable-background="new 0 0 127.433 132.743" xml:space="preserve">
<path fill="#FFF6F4" d="M116.223,125.064c1.032-1.183,1.323-2.73,1.391-3.747V54.76c0,0-4.625-34.875-36.125-44.375 s-66,6.625-72.125,44l-0.781,63.219c0.062,4.197,1.105,6.177,1.808,7.006c1.94,1.811,5.408,3.465,10.099-0.6 c7.5-6.5,8.375-10,12.75-6.875s5.875,9.75,13.625,9.25s12.75-9,13.75-9.625s4.375-1.875,7,1.25s5.375,8.25,12.875,7.875 s12.625-8.375,12.625-8.375s2.25-3.875,7.25,0.375s7.625,9.75,14.375,8.125C114.739,126.01,115.412,125.902,116.223,125.064z"></path>
<circle fill="#1a1a1a" cx="86.238" cy="57.885" r="6.667"></circle>
<circle fill="#1a1a1a" cx="40.072" cy="57.885" r="6.667"></circle>
<path fill="#1a1a1a" d="M71.916,62.782c0.05-1.108-0.809-2.046-1.917-2.095c-0.673-0.03-1.28,0.279-1.667,0.771 c-0.758,0.766-2.483,2.235-4.696,2.358c-1.696,0.094-3.438-0.625-5.191-2.137c-0.003-0.003-0.007-0.006-0.011-0.009l0.002,0.005 c-0.332-0.294-0.757-0.488-1.235-0.509c-1.108-0.049-2.046,0.809-2.095,1.917c-0.032,0.724,0.327,1.37,0.887,1.749 c-0.001,0-0.002-0.001-0.003-0.001c2.221,1.871,4.536,2.88,6.912,2.986c0.333,0.014,0.67,0.012,1.007-0.01 c3.163-0.191,5.572-1.942,6.888-3.166l0.452-0.453c0.021-0.019,0.04-0.041,0.06-0.061l0.034-0.034 c-0.007,0.007-0.015,0.014-0.021,0.02C71.666,63.771,71.892,63.307,71.916,62.782z"></path>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.614" cy="99.426" r="3.292"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="95.364" cy="28.676" r="3.291"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="24.739" cy="93.551" r="2.667"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="101.489" cy="33.051" r="2.666"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.738" cy="87.717" r="2.833"></circle>
<path fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" d="M116.279,55.814c-0.021-0.286-2.323-28.744-30.221-41.012 c-7.806-3.433-15.777-5.173-23.691-5.173c-16.889,0-30.283,7.783-37.187,15.067c-9.229,9.736-13.84,26.712-14.191,30.259 l-0.748,62.332c0.149,2.133,1.389,6.167,5.019,6.167c1.891,0,4.074-1.083,6.672-3.311c4.96-4.251,7.424-6.295,9.226-6.295 c1.339,0,2.712,1.213,5.102,3.762c4.121,4.396,7.461,6.355,10.833,6.355c2.713,0,5.311-1.296,7.942-3.962 c3.104-3.145,5.701-5.239,8.285-5.239c2.116,0,4.441,1.421,7.317,4.473c2.638,2.8,5.674,4.219,9.022,4.219 c4.835,0,8.991-2.959,11.27-5.728l0.086-0.104c1.809-2.2,3.237-3.938,5.312-3.938c2.208,0,5.271,1.942,9.359,5.936 c0.54,0.743,3.552,4.674,6.86,4.674c1.37,0,2.559-0.65,3.531-1.932l0.203-0.268L116.279,55.814z M114.281,121.405 c-0.526,0.599-1.096,0.891-1.734,0.891c-2.053,0-4.51-2.82-5.283-3.907l-0.116-0.136c-4.638-4.541-7.975-6.566-10.82-6.566 c-3.021,0-4.884,2.267-6.857,4.667l-0.086,0.104c-1.896,2.307-5.582,4.999-9.725,4.999c-2.775,0-5.322-1.208-7.567-3.59 c-3.325-3.528-6.03-5.102-8.772-5.102c-3.278,0-6.251,2.332-9.708,5.835c-2.236,2.265-4.368,3.366-6.518,3.366 c-2.772,0-5.664-1.765-9.374-5.723c-2.488-2.654-4.29-4.395-6.561-4.395c-2.515,0-5.045,2.077-10.527,6.777 c-2.727,2.337-4.426,2.828-5.37,2.828c-2.662,0-3.017-4.225-3.021-4.225l0.745-62.163c0.332-3.321,4.767-19.625,13.647-28.995 c3.893-4.106,10.387-8.632,18.602-11.504c-0.458,0.503-0.744,1.165-0.744,1.898c0,1.565,1.269,2.833,2.833,2.833 c1.564,0,2.833-1.269,2.833-2.833c0-1.355-0.954-2.485-2.226-2.764c4.419-1.285,9.269-2.074,14.437-2.074 c7.636,0,15.336,1.684,22.887,5.004c26.766,11.771,29.011,39.047,29.027,39.251V121.405z"></path>
</svg>
<p class="shadowFrame">
<svg version="1.1" class="shadow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="0 0 122.436 39.744" enable-background="new 0 0 122.436 39.744" xml:space="preserve">
<ellipse fill="#262626" cx="61.128" cy="19.872" rx="49.25" ry="8.916"></ellipse>
</svg>
</p>
<h3>Error {{ code }}</h3>
<p class="description">{{ description }}</p>
<div class="container">
<div class="wrap">
<svg class="ghost" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="0 0 127.433 132.743" enable-background="new 0 0 127.433 132.743" xml:space="preserve">
<path fill="#FFF6F4" d="M116.223,125.064c1.032-1.183,1.323-2.73,1.391-3.747V54.76c0,0-4.625-34.875-36.125-44.375 s-66,6.625-72.125,44l-0.781,63.219c0.062,4.197,1.105,6.177,1.808,7.006c1.94,1.811,5.408,3.465,10.099-0.6 c7.5-6.5,8.375-10,12.75-6.875s5.875,9.75,13.625,9.25s12.75-9,13.75-9.625s4.375-1.875,7,1.25s5.375,8.25,12.875,7.875 s12.625-8.375,12.625-8.375s2.25-3.875,7.25,0.375s7.625,9.75,14.375,8.125C114.739,126.01,115.412,125.902,116.223,125.064z"></path>
<circle fill="#1a1a1a" cx="86.238" cy="57.885" r="6.667"></circle>
<circle fill="#1a1a1a" cx="40.072" cy="57.885" r="6.667"></circle>
<path fill="#1a1a1a" d="M71.916,62.782c0.05-1.108-0.809-2.046-1.917-2.095c-0.673-0.03-1.28,0.279-1.667,0.771 c-0.758,0.766-2.483,2.235-4.696,2.358c-1.696,0.094-3.438-0.625-5.191-2.137c-0.003-0.003-0.007-0.006-0.011-0.009l0.002,0.005 c-0.332-0.294-0.757-0.488-1.235-0.509c-1.108-0.049-2.046,0.809-2.095,1.917c-0.032,0.724,0.327,1.37,0.887,1.749 c-0.001,0-0.002-0.001-0.003-0.001c2.221,1.871,4.536,2.88,6.912,2.986c0.333,0.014,0.67,0.012,1.007-0.01 c3.163-0.191,5.572-1.942,6.888-3.166l0.452-0.453c0.021-0.019,0.04-0.041,0.06-0.061l0.034-0.034 c-0.007,0.007-0.015,0.014-0.021,0.02C71.666,63.771,71.892,63.307,71.916,62.782z"></path>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.614" cy="99.426" r="3.292"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="95.364" cy="28.676" r="3.291"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="24.739" cy="93.551" r="2.667"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="101.489" cy="33.051" r="2.666"></circle>
<circle fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" cx="18.738" cy="87.717" r="2.833"></circle>
<path fill="#FCEFED" stroke="#FEEBE6" stroke-miterlimit="10" d="M116.279,55.814c-0.021-0.286-2.323-28.744-30.221-41.012 c-7.806-3.433-15.777-5.173-23.691-5.173c-16.889,0-30.283,7.783-37.187,15.067c-9.229,9.736-13.84,26.712-14.191,30.259 l-0.748,62.332c0.149,2.133,1.389,6.167,5.019,6.167c1.891,0,4.074-1.083,6.672-3.311c4.96-4.251,7.424-6.295,9.226-6.295 c1.339,0,2.712,1.213,5.102,3.762c4.121,4.396,7.461,6.355,10.833,6.355c2.713,0,5.311-1.296,7.942-3.962 c3.104-3.145,5.701-5.239,8.285-5.239c2.116,0,4.441,1.421,7.317,4.473c2.638,2.8,5.674,4.219,9.022,4.219 c4.835,0,8.991-2.959,11.27-5.728l0.086-0.104c1.809-2.2,3.237-3.938,5.312-3.938c2.208,0,5.271,1.942,9.359,5.936 c0.54,0.743,3.552,4.674,6.86,4.674c1.37,0,2.559-0.65,3.531-1.932l0.203-0.268L116.279,55.814z M114.281,121.405 c-0.526,0.599-1.096,0.891-1.734,0.891c-2.053,0-4.51-2.82-5.283-3.907l-0.116-0.136c-4.638-4.541-7.975-6.566-10.82-6.566 c-3.021,0-4.884,2.267-6.857,4.667l-0.086,0.104c-1.896,2.307-5.582,4.999-9.725,4.999c-2.775,0-5.322-1.208-7.567-3.59 c-3.325-3.528-6.03-5.102-8.772-5.102c-3.278,0-6.251,2.332-9.708,5.835c-2.236,2.265-4.368,3.366-6.518,3.366 c-2.772,0-5.664-1.765-9.374-5.723c-2.488-2.654-4.29-4.395-6.561-4.395c-2.515,0-5.045,2.077-10.527,6.777 c-2.727,2.337-4.426,2.828-5.37,2.828c-2.662,0-3.017-4.225-3.021-4.225l0.745-62.163c0.332-3.321,4.767-19.625,13.647-28.995 c3.893-4.106,10.387-8.632,18.602-11.504c-0.458,0.503-0.744,1.165-0.744,1.898c0,1.565,1.269,2.833,2.833,2.833 c1.564,0,2.833-1.269,2.833-2.833c0-1.355-0.954-2.485-2.226-2.764c4.419-1.285,9.269-2.074,14.437-2.074 c7.636,0,15.336,1.684,22.887,5.004c26.766,11.771,29.011,39.047,29.027,39.251V121.405z"></path>
</svg>
<p class="shadowFrame">
<svg version="1.1" class="shadow" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="0 0 122.436 39.744" enable-background="new 0 0 122.436 39.744" xml:space="preserve">
<ellipse fill="#262626" cx="61.128" cy="19.872" rx="49.25" ry="8.916"></ellipse>
</svg>
</p>
<h3>Error {{ code }}</h3>
<p class="description">{{ description }}</p>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name">Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name">Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name">Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name">Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name">Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name">Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name">Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name">Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name">Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
</body>
<!--

View File

@ -18,6 +18,7 @@
overflow: hidden;
margin: 0;
padding: 0;
font-size: 0;
}
body {
@ -27,10 +28,9 @@
background-repeat: no-repeat;
background-size: cover;
font-family: 'Inconsolata', Helvetica, sans-serif;
font-size: 1.5rem;
color: rgba(128, 255, 128, 0.8);
text-shadow:
0 0 1ex rgba(51, 255, 51, 1),
0 0 11px rgba(51, 255, 51, 1),
0 0 2px rgba(255, 255, 255, 0.8);
}
@ -82,10 +82,18 @@
height: 100%;
width: 1000px;
max-width: 100%;
padding: 4rem;
padding: 64px;
text-transform: uppercase;
}
h1 {
font-size: 48px;
}
p {
font-size: 24px;
}
.output {
color: rgba(128, 255, 128, 0.8);
text-shadow:
@ -113,6 +121,25 @@
.error_code {
color: white;
}
/* {{ if show_details }} */
.details p {
margin-top: .5em;
margin-bottom: .5em;
}
.details * {
font-size: 15px;
}
.details p::before {
content: "$ ";
}
.details code {
font-size: 0.9em;
}
/* {{ end }} */
</style>
</head>
<body>
@ -121,6 +148,19 @@
<h1>Error <span class="error_code">{{ code }}</span></h1>
<p class="output">{{ description }}.</p>
<p class="output">Good luck.</p>
{{ if show_details }}
<div class="details">
{{- if host }}<p class="output small">Host: <code>{{ host }}</code></p>{{ end -}}
{{- if original_uri }}<p class="output small">Original URI: <code>{{ original_uri }}</code></p>{{ end -}}
{{- if forwarded_for }}<p class="output small">Forwarded for: <code>{{ forwarded_for }}</code></p>{{ end -}}
{{- if namespace }}<p class="output small">Namespace: <code>{{ namespace }}</code></p>{{ end -}}
{{- if ingress_name }}<p class="output small">Ingress name: <code>{{ ingress_name }}</code></p>{{ end -}}
{{- if service_name }}<p class="output small">Service name: <code>{{ service_name }}</code></p>{{ end -}}
{{- if service_port }}<p class="output small">Service port: <code>{{ service_port }}</code></p>{{ end -}}
{{- if request_id }}<p class="output small">Request ID: <code>{{ request_id }}</code></p>{{ end -}}
<p class="output small">Timestamp: <code>{{ now.Unix }}</code></p>
</div>
{{ end }}
</div>
</body>
<!--

View File

@ -12,21 +12,73 @@
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<style>
html,body {background-color: #222526;color:#fff;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0}
html,body {background-color:#222526;color:#fff;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0}
.full-height {height:100vh}
.flex-center {align-items:center;display:flex;justify-content:center}
.position-ref {position:relative}
.code {border-right:2px solid;font-size:26px;padding:0 10px 0 15px;text-align:center}
.message {font-size:18px;text-align:center;padding:10px}
/* {{ if show_details }} */
.details table {width:100%;border-collapse:collapse;box-sizing:border-box;margin-top:20px}
.details td {font-size:11px;color:#999}
.details td.name {text-align:right;padding-right:.6em;width:50%;border-right:2px solid;border-color:#777}
.details td.value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="code">
{{ code }}
</div>
<div class="message">
{{ message }}
<div>
<div class="flex-center">
<div class="code">
{{ code }}
</div>
<div class="message">
{{ message }}
</div>
</div>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name">Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name">Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name">Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name">Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name">Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name">Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name">Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name">Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name">Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
</body>

View File

@ -12,21 +12,73 @@
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<style>
html,body {background-color:#fff;color:#636b6f;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0}
html,body {background-color:#fff;color:#636b6f;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0}
.full-height {height:100vh}
.flex-center {align-items:center;display:flex;justify-content:center}
.position-ref {position:relative}
.code {border-right:2px solid;font-size:26px;padding:0 10px 0 15px;text-align:center}
.message {font-size:18px;text-align:center;padding:10px}
/* {{ if show_details }} */
.details table {width:100%;border-collapse:collapse;box-sizing:border-box;margin-top:20px}
.details td {font-size:11px;color:#777}
.details td.name {text-align:right;padding-right:.6em;width:50%;border-right:2px solid;border-color:#aaa}
.details td.value {text-align:left;padding-left:.6em;font-family:'Lucida Console','Courier New',monospace}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
<div class="code">
{{ code }}
</div>
<div class="message">
{{ message }}
<div>
<div class="flex-center">
<div class="code">
{{ code }}
</div>
<div class="message">
{{ message }}
</div>
</div>
{{ if show_details }}
<div class="details">
<table>
{{- if host }}<tr>
<td class="name">Host</td>
<td class="value">{{ host }}</td>
</tr>{{ end -}}
{{- if original_uri }}<tr>
<td class="name">Original URI</td>
<td class="value">{{ original_uri }}</td>
</tr>{{ end -}}
{{- if forwarded_for }}<tr>
<td class="name">Forwarded for</td>
<td class="value">{{ forwarded_for }}</td>
</tr>{{ end -}}
{{- if namespace }}<tr>
<td class="name">Namespace</td>
<td class="value">{{ namespace }}</td>
</tr>{{ end -}}
{{- if ingress_name }}<tr>
<td class="name">Ingress name</td>
<td class="value">{{ ingress_name }}</td>
</tr>{{ end -}}
{{- if service_name }}<tr>
<td class="name">Service name</td>
<td class="value">{{ service_name }}</td>
</tr>{{ end -}}
{{- if service_port }}<tr>
<td class="name">Service port</td>
<td class="value">{{ service_port }}</td>
</tr>{{ end -}}
{{- if request_id }}<tr>
<td class="name">Request ID</td>
<td class="value">{{ request_id }}</td>
</tr>{{ end -}}
<tr>
<td class="name">Timestamp</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
</body>

View File

@ -2,6 +2,17 @@
<!--
Error {{ code }}: {{ message }}
Description: {{ description }}
{{ if show_details }}
{{ if host }}Host: {{ host }}{{ end }}
{{ if original_uri }}Original URI: {{ original_uri }}{{ end }}
{{ if forwarded_for }}Forwarded for: {{ forwarded_for }}{{ end }}
{{ if namespace }}Namespace: {{ namespace }}{{ end }}
{{ if ingress_name }}Ingress name: {{ ingress_name }}{{ end }}
{{ if service_name }}Service name: {{ service_name }}{{ end }}
{{ if service_port }}Service port: {{ service_port }}{{ end }}
{{ if request_id }}Request ID: {{ request_id }}{{ end }}
Timestamp: {{ now.Unix }}
{{ end }}
-->
<html lang="en">
<head>

1
templates/readme.md Normal file
View File

@ -0,0 +1 @@
# TODO: Write docs

View File

@ -15,27 +15,123 @@
margin: 0;
background-color: #222;
color: #aaa;
font-family: 'Hack', monospace
font-family: 'Hack', monospace;
font-size: 0;
}
.full-height {
height: 100vh
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center
justify-content: center;
}
#error_text {
font-size: 2em
font-size: 32px;
}
/* {{ if show_details }} */
#details table {
width: 100%;
border-collapse: collapse;
box-sizing: border-box;
margin-top: 20px;
}
#details.hidden td {
opacity: 0;
font-size: 0;
color: #222;
}
#details td {
font-size: 11px;
color: #999;
padding-top: .5em;
transition: opacity 1.4s, font-size .3s, color 1.2s;
opacity: 1;
}
#details td.name {
text-align: right;
padding-right: .3em;
width: 50%;
}
#details td.value {
text-align: left;
padding-left: .3em;
font-family: 'Lucida Console', 'Courier New', monospace;
}
/* {{ end }} */
</style>
</head>
<body>
<div class="flex-center full-height">
<span id="error_text">{{ code }}: {{ message }}</span>
<div>
<span id="error_text">{{ code }}: {{ message }}</span>
{{ if show_details }}
<div class="hidden" id="details">
<table>
{{- if host }}
<tr>
<td class="name">Host:</td>
<td class="value">{{ host }}</td>
</tr>
{{ end -}}
{{- if original_uri }}
<tr>
<td class="name">Original URI:</td>
<td class="value">{{ original_uri }}</td>
</tr>
{{ end -}}
{{- if forwarded_for }}
<tr>
<td class="name">Forwarded for:</td>
<td class="value">{{ forwarded_for }}</td>
</tr>
{{ end -}}
{{- if namespace }}
<tr>
<td class="name">Namespace:</td>
<td class="value">{{ namespace }}</td>
</tr>
{{ end -}}
{{- if ingress_name }}
<tr>
<td class="name">Ingress name:</td>
<td class="value">{{ ingress_name }}</td>
</tr>
{{ end -}}
{{- if service_name }}
<tr>
<td class="name">Service name:</td>
<td class="value">{{ service_name }}</td>
</tr>
{{ end -}}
{{- if service_port }}
<tr>
<td class="name">Service port:</td>
<td class="value">{{ service_port }}</td>
</tr>
{{ end -}}
{{- if request_id }}
<tr>
<td class="name">Request ID:</td>
<td class="value">{{ request_id }}</td>
</tr>
{{ end -}}
<tr>
<td class="name">Timestamp:</td>
<td class="value">{{ now.Unix }}</td>
</tr>
</table>
</div>
{{ end }}
</div>
</div>
<script>
@ -58,7 +154,13 @@
}
$errorText.innerText = newText;
}, 800 / 60);
}, 450 / 60);
// {{ if show_details }}
window.setTimeout(function () {
document.getElementById('details').classList.remove('hidden');
}, 550);
// {{ end }}
window.setTimeout(function () {
let revealInterval = window.setInterval(function () {

7
test/hurl/404.hurl Normal file
View File

@ -0,0 +1,7 @@
GET http://{{ host }}:{{ port }}/not-found
HTTP/* 404
[Asserts]
header "Content-Type" contains "text/plain"
body contains "Wrong request URL"

View File

@ -0,0 +1,10 @@
GET http://{{ host }}:{{ port }}/502.html
HTTP/* 200
[Asserts]
header "Content-Type" contains "text/html"
header "X-Robots-Tag" == "noindex"
body contains "502"
body contains "Bad Gateway"
body contains "The server received an invalid response from the upstream server"

View File

@ -0,0 +1,43 @@
# The common request
GET http://{{ host }}:{{ port }}/502.html
Content-Type: application/json;charset=UTF-8
X-Original-URI: foo
X-Namespace: bar
X-Ingress-Name: baz
X-Service-Name: aaa
X-Service-Port: bbb
X-Request-ID: ccc
X-Forwarded-For: ddd
Host: eee
HTTP/* 200
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "502"
jsonpath "$.message" == "Bad Gateway"
jsonpath "$.description" == "The server received an invalid response from the upstream server"
jsonpath "$.details.original_uri" == "foo"
jsonpath "$.details.namespace" == "bar"
jsonpath "$.details.ingress_name" == "baz"
jsonpath "$.details.service_name" == "aaa"
jsonpath "$.details.service_port" == "bbb"
jsonpath "$.details.request_id" == "ccc"
jsonpath "$.details.forwarded_for" == "ddd"
jsonpath "$.details.host" == "eee"
jsonpath "$.details.timestamp" isInteger
# X-Format in the action
GET http://{{ host }}:{{ port }}/502.html
X-Format: text/json
HTTP/* 200
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "502"
jsonpath "$.message" == "Bad Gateway"

View File

@ -0,0 +1,42 @@
# The common request
GET http://{{ host }}:{{ port }}/502.html
Content-Type: application/xml;charset=UTF-8
X-Original-URI: foo
X-Namespace: bar
X-Ingress-Name: baz
X-Service-Name: aaa
X-Service-Port: bbb
X-Request-ID: ccc
X-Forwarded-For: ddd
Host: eee
HTTP/* 200
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "502"
xpath "string(//error/message)" == "Bad Gateway"
xpath "string(//error/description)" == "The server received an invalid response from the upstream server"
xpath "string(//error/details/originalURI)" == "foo"
xpath "string(//error/details/namespace)" == "bar"
xpath "string(//error/details/ingressName)" == "baz"
xpath "string(//error/details/serviceName)" == "aaa"
xpath "string(//error/details/servicePort)" == "bbb"
xpath "string(//error/details/requestID)" == "ccc"
xpath "string(//error/details/forwardedFor)" == "ddd"
xpath "string(//error/details/host)" == "eee"
xpath "string(//error/details/timestamp)" exists
# X-Format in the action
GET http://{{ host }}:{{ port }}/502.html
X-Format: text/xml
HTTP/* 200
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "502"
xpath "string(//error/message)" == "Bad Gateway"
xpath "string(//error/description)" == "The server received an invalid response from the upstream server"

12
test/hurl/healthz.hurl Normal file
View File

@ -0,0 +1,12 @@
GET http://{{ host }}:{{ port }}/healthz
HTTP/* 200
```OK```
# Next endpoint marked as deprecated
GET http://{{ host }}:{{ port }}/health/live
HTTP/* 200
```OK```

34
test/hurl/index.hurl Normal file
View File

@ -0,0 +1,34 @@
# HTML content
GET http://{{ host }}:{{ port }}/
HTTP/* 404
[Asserts]
header "Content-Type" contains "text/html"
body contains "404"
body contains "Not Found"
# JSON content
GET http://{{ host }}:{{ port }}/
Content-Type: text/json
HTTP/* 404
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "404"
jsonpath "$.message" == "Not Found"
# XML content
GET http://{{ host }}:{{ port }}/
Content-Type: application/xml
HTTP/* 404
[Asserts]
header "Content-Type" contains "application/xml"
header "X-Robots-Tag" == "noindex"
xpath "string(//error/code)" == "404"
xpath "string(//error/message)" == "Not Found"

8
test/hurl/metrics.hurl Normal file
View File

@ -0,0 +1,8 @@
GET http://{{ host }}:{{ port }}/metrics
HTTP/* 200
[Asserts]
header "Content-Type" contains "text/plain"
body contains "http_requests_duration_millisecond"
body contains "http_requests_total_count"

27
test/hurl/readme.md Normal file
View File

@ -0,0 +1,27 @@
# Hurl
Hurl is a command line tool that runs **HTTP requests** defined in a simple **plain text format**.
## How to use
It can perform requests, capture values and evaluate queries on headers and body response. Hurl is very versatile: it can be used for both fetching data and testing HTTP sessions.
```hurl
# Get home:
GET https://example.net
HTTP/1.1 200
[Captures]
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
# Do login!
POST https://example.net/login?user=toto&password=1234
X-CSRF-TOKEN: {{csrf_token}}
HTTP/1.1 302
```
### Links:
- [Official website](https://hurl.dev/)
- [GitHub](https://github.com/Orange-OpenSource/hurl)

8
test/hurl/version.hurl Normal file
View File

@ -0,0 +1,8 @@
GET http://{{ host }}:{{ port }}/version
HTTP/* 200
[Asserts]
header "Content-Type" == "application/json"
jsonpath "$.version" exists
jsonpath "$.version" isString

37
test/hurl/x_code.hurl Normal file
View File

@ -0,0 +1,37 @@
# Common request to the index page
GET http://{{ host }}:{{ port }}/
X-Code: 410
HTTP/* 410
[Asserts]
header "Content-Type" contains "text/html"
header "X-Robots-Tag" == "noindex"
body contains "410"
body contains "Gone"
# X-Code with X-Format
GET http://{{ host }}:{{ port }}/
X-Code: 410
X-Format: text/html;q=0.9,application/xhtml+xml;q=0.9,application/json,image/avif,image/webp,*/*;q=0.8
HTTP/* 410
[Asserts]
header "Content-Type" contains "application/json"
header "X-Robots-Tag" == "noindex"
jsonpath "$.error" == true
jsonpath "$.code" == "410"
jsonpath "$.message" == "Gone"
# Error pages should ignore X-Code
GET http://{{ host }}:{{ port }}/502.html
X-Code: 410
HTTP/* 200
[Asserts]
header "Content-Type" contains "text/html"
header "X-Robots-Tag" == "noindex"
body contains "502"
body contains "Bad Gateway"

9
test/wrk/request.lua Normal file
View File

@ -0,0 +1,9 @@
local formats = { 'application/json', 'application/xml', 'text/html', 'text/plain' }
request = function()
wrk.headers["X-Namespace"] = "NAMESPACE_" .. tostring(math.random(0, 99999999))
wrk.headers["X-Request-ID"] = "REQ_ID_" .. tostring(math.random(0, 99999999))
wrk.headers["Content-Type"] = formats[ math.random( 0, #formats - 1 ) ]
return wrk.format("GET", "/500.html?rnd=" .. tostring(math.random(0, 99999999)), nil, nil)
end