mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
d672112cc2 | |||
32b92611a7 | |||
cc6cbc7d47 | |||
690a405994 | |||
f72c2b85fd | |||
42523ae9d9 | |||
da2dc5c63a | |||
a0a1d3caca | |||
915e810088 | |||
00c139b525 | |||
eca99eb569 | |||
dfaeea7483 | |||
f71b07f647 | |||
be0a3c4820 | |||
04bf2231bc | |||
ba98272530 | |||
fab38255eb | |||
88278d37a7 | |||
32daf80b76 | |||
13e7a72790 | |||
0efbccbb18 | |||
bed576f26c | |||
f75bf15552 | |||
9915e321f4 | |||
83720999d8 | |||
79bbf3d71e | |||
1dec69d726 | |||
ef2db68430 | |||
e6f3250286 | |||
ca56f1dd07 | |||
6bd973a803 | |||
49dd703e12 | |||
0f27441225 | |||
97d76ddca8 | |||
891d491cdb | |||
2a1fb0eddf | |||
5c25fbe2c4 | |||
e3e618d3cf |
3
.github/CODEOWNERS
vendored
Normal file
3
.github/CODEOWNERS
vendored
Normal 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
51
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal 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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
|
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
33
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
|
19
.github/workflows/documentation.yml
vendored
Normal file
19
.github/workflows/documentation.yml
vendored
Normal 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
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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
|
||||
|
||||
|
42
.github/workflows/tests.yml
vendored
42
.github/workflows/tests.yml
vendored
@ -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,17 +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/
|
||||
- run: curl --fail http://127.0.0.1:8080/500.html
|
||||
- run: curl --fail http://127.0.0.1:8080/400.html
|
||||
- run: curl --fail http://127.0.0.1:8080/health/live
|
||||
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/foobar) -eq 404
|
||||
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 --summary ./test/hurl/*.hurl
|
||||
|
||||
- name: Stop the container
|
||||
if: always()
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
/.vscode
|
||||
|
||||
## Binaries
|
||||
error-pages
|
||||
/error-pages
|
||||
|
||||
## Temp dirs & trash
|
||||
/temp
|
||||
|
@ -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
3
.grype.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
ignore:
|
||||
# temporary ignore this CVE as false positive on the Go package
|
||||
- vulnerability: CVE-2015-5237
|
60
CHANGELOG.md
60
CHANGELOG.md
@ -4,6 +4,66 @@ 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
|
||||
|
11
Dockerfile
11
Dockerfile
@ -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
|
||||
@ -65,12 +66,12 @@ WORKDIR /opt
|
||||
|
||||
ENV LISTEN_PORT="8080" \
|
||||
TEMPLATE_NAME="ghost" \
|
||||
DEFAULT_ERROR_PAGE="404"
|
||||
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"]
|
||||
|
||||
|
7
Makefile
7
Makefile
@ -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
|
||||
|
480
README.md
480
README.md
@ -4,385 +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:
|
||||
|
||||
Registry | Image
|
||||
-------------------------------------- | -----
|
||||
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
|
||||
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
|
||||
[][docker-hub-tags]
|
||||
|
||||
| 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
|
||||
## 🪂 Templates
|
||||
|
||||
Name | Preview
|
||||
:---------------: | :-----:
|
||||
`ghost` | [](https://tarampampam.github.io/error-pages/ghost/404.html)
|
||||
`l7-light` | [](https://tarampampam.github.io/error-pages/l7-light/404.html)
|
||||
`l7-dark` | [](https://tarampampam.github.io/error-pages/l7-dark/404.html)
|
||||
`shuffle` | [](https://tarampampam.github.io/error-pages/shuffle/404.html)
|
||||
`noise` | [](https://tarampampam.github.io/error-pages/noise/404.html)
|
||||
`hacker-terminal` | [](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
|
||||
|
||||
## Usage
|
||||
[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
|
||||
|
||||
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**.
|
||||
## 🦾 Contributors
|
||||
|
||||
<details>
|
||||
<summary><strong>HTTP server</strong></summary>
|
||||
I want to say a big thank you to everyone who contributed to this project:
|
||||
|
||||
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 :)
|
||||
[][contributors]
|
||||
|
||||
For the HTTP server running execute in your terminal:
|
||||
[contributors]:https://github.com/tarampampam/error-pages/graphs/contributors
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-p "8080:8080/tcp" \
|
||||
-e "TEMPLATE_NAME=random" \
|
||||
tarampampam/error-pages
|
||||
```
|
||||
## 📰 Changes log
|
||||
|
||||
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`.
|
||||
[![Release date][badge-release-date]][releases]
|
||||
[![Commits since latest release][badge-commits]][commits]
|
||||
|
||||
Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).
|
||||
Changes log can be [found here][changelog].
|
||||
|
||||
> **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**)
|
||||
## 👾 Support
|
||||
|
||||
To see the help run the following command:
|
||||
[![Issues][badge-issues]][issues]
|
||||
[![Issues][badge-prs]][prs]
|
||||
|
||||
```bash
|
||||
$ docker run --rm tarampampam/error-pages serve --help
|
||||
```
|
||||
</details>
|
||||
If you find any bugs in the project, please [create an issue][new-issue] in the current repository.
|
||||
|
||||
<details>
|
||||
<summary><strong>Generator</strong></summary>
|
||||
## 📖 License
|
||||
|
||||
Create a config file (`error-pages.yml`) with the following content:
|
||||
This is open-sourced software licensed under the [MIT License][license].
|
||||
|
||||
```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 }})"
|
||||
[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
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Static error pages</strong></summary>
|
||||
|
||||
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
|
||||
$ 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
|
||||
│ ├── ...
|
||||
...
|
||||
```
|
||||
|
||||
Or inside another docker image:
|
||||
|
||||
```dockerfile
|
||||
FROM alpine:latest
|
||||
|
||||
COPY --from=tarampampam/error-pages /opt/html /error-pages
|
||||
|
||||
RUN ls -l /error-pages
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker build --rm .
|
||||
|
||||
...
|
||||
Step 3/3 : RUN ls -l /error-pages
|
||||
---> Running in 30095dc344a9
|
||||
total 12
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 ghost
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 hacker-terminal
|
||||
-rw-r--r-- 1 root root 11241 Sep 29 15:44 index.html
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 l7-dark
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 l7-light
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 noise
|
||||
drwxr-xr-x 2 root root 326 Sep 29 15:44 shuffle
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Custom error pages for your image with nginx</strong></summary>
|
||||
|
||||
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).
|
||||
</details>
|
||||
|
||||
## Custom error pages for [Traefik][link_traefik]
|
||||
|
||||
Simple traefik (tested on `v2.5.3`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Changes log
|
||||
|
||||
[![Release date][badge_release_date]][link_releases]
|
||||
[![Commits since latest release][badge_commits_since_release]][link_commits]
|
||||
|
||||
Changes log can be [found here][link_changes_log].
|
||||
|
||||
## Support
|
||||
|
||||
[![Issues][badge_issues]][link_issues]
|
||||
[![Issues][badge_pulls]][link_pulls]
|
||||
|
||||
If you will find any package errors, please, [make an issue][link_create_issue] in current repository.
|
||||
|
||||
## License
|
||||
|
||||
This is open-sourced software licensed under the [MIT License][link_license].
|
||||
|
||||
[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
|
||||
|
41
cmd/error-pages/main_test.go
Normal file
41
cmd/error-pages/main_test.go
Normal 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")
|
||||
}
|
@ -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
|
||||
|
@ -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:
|
||||
|
29
go.mod
29
go.mod
@ -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/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.9.1
|
||||
github.com/spf13/cobra v1.2.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.1.9 // 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-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
339
go.sum
339
go.sum
@ -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,23 +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/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/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.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
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=
|
||||
@ -97,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=
|
||||
@ -113,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=
|
||||
@ -128,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=
|
||||
@ -143,76 +198,116 @@ 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/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||
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/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=
|
||||
@ -220,27 +315,55 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
|
||||
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=
|
||||
@ -249,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=
|
||||
@ -269,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=
|
||||
@ -321,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=
|
||||
@ -332,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=
|
||||
@ -357,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=
|
||||
@ -369,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=
|
||||
@ -383,22 +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=
|
||||
@ -409,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=
|
||||
@ -416,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=
|
||||
@ -425,8 +582,22 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
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=
|
||||
@ -436,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=
|
||||
@ -445,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=
|
||||
@ -453,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=
|
||||
@ -489,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=
|
||||
@ -516,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=
|
||||
@ -564,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=
|
||||
@ -584,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=
|
||||
@ -597,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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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,gocyclo
|
||||
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command {
|
||||
var (
|
||||
generateIndex bool
|
||||
cfg *config.Config
|
||||
@ -30,95 +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")
|
||||
}
|
||||
|
||||
errorPages := tpl.NewErrorPages()
|
||||
|
||||
log.Info("loading templates")
|
||||
if templates, err := cfg.LoadTemplates(); err == nil {
|
||||
if len(templates) > 0 {
|
||||
for templateName, content := range templates {
|
||||
errorPages.AddTemplate(templateName, content)
|
||||
}
|
||||
|
||||
for code, desc := range cfg.Pages {
|
||||
errorPages.AddPage(code, desc.Message, desc.Description)
|
||||
}
|
||||
} else {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
history, startedAt := make(map[string][]historyItem), time.Now()
|
||||
|
||||
log.Info("saving the error pages")
|
||||
if err := errorPages.IteratePages(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(cfg.Pages))
|
||||
}
|
||||
|
||||
history[template] = append(history[template], historyItem{
|
||||
Code: code,
|
||||
Message: cfg.Pages[code].Message,
|
||||
Path: path.Join(template, fileName),
|
||||
})
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
@ -132,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
|
||||
@ -148,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
|
||||
}
|
||||
|
59
internal/cli/build/history.go
Normal file
59
internal/cli/build/history.go
Normal 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)
|
||||
}
|
35
internal/cli/build/index.tpl.html
Normal file
35
internal/cli/build/index.tpl.html
Normal 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>
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@ -12,7 +11,6 @@ import (
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
appHttp "github.com/tarampampam/error-pages/internal/http"
|
||||
"github.com/tarampampam/error-pages/internal/pick"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -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()
|
||||
@ -76,35 +68,10 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
}()
|
||||
|
||||
var (
|
||||
errorPages = tpl.NewErrorPages()
|
||||
templateNames = make([]string, 0) // slice with all possible template names
|
||||
templateNames = cfg.TemplateNames()
|
||||
picker *pick.StringsSlice
|
||||
)
|
||||
|
||||
log.Debug("Loading templates")
|
||||
|
||||
if templates, err := cfg.LoadTemplates(); err == nil {
|
||||
if len(templates) > 0 {
|
||||
for templateName, content := range templates {
|
||||
errorPages.AddTemplate(templateName, content)
|
||||
templateNames = append(templateNames, templateName)
|
||||
}
|
||||
|
||||
for code, desc := range cfg.Pages {
|
||||
errorPages.AddPage(code, desc.Message, desc.Description)
|
||||
}
|
||||
|
||||
log.Info("Templates loaded", zap.Int("templates", len(templates)), zap.Int("pages", len(cfg.Pages)))
|
||||
} else {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Strings(templateNames) // sorting is important for the first template picking
|
||||
|
||||
var picker *pick.StringsSlice
|
||||
|
||||
switch f.template.name {
|
||||
case useRandomTemplate:
|
||||
log.Info("A random template will be used")
|
||||
@ -122,29 +89,21 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
picker = pick.NewStringsSlice(templateNames, pick.First)
|
||||
|
||||
default:
|
||||
var found bool
|
||||
|
||||
for i := 0; i < len(templateNames); i++ {
|
||||
if templateNames[i] == f.template.name {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
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)
|
||||
}
|
||||
|
||||
log.Info("We will use the requested template", zap.String("name", f.template.name))
|
||||
picker = pick.NewStringsSlice([]string{f.template.name}, pick.First)
|
||||
}
|
||||
|
||||
// create HTTP server
|
||||
server := appHttp.NewServer(log)
|
||||
|
||||
// register server routes, middlewares, etc.
|
||||
server.Register(&errorPages, picker, f.defaultErrorPage)
|
||||
if err := server.Register(cfg, picker, f.defaultErrorPage, f.defaultHTTPCode, f.showDetails); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
|
||||
|
||||
@ -156,6 +115,8 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
zap.String("addr", f.listen.ip),
|
||||
zap.Uint16("port", f.listen.port),
|
||||
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 {
|
||||
|
@ -19,6 +19,8 @@ type flags struct {
|
||||
name string
|
||||
}
|
||||
defaultErrorPage string
|
||||
defaultHTTPCode uint16
|
||||
showDetails bool
|
||||
}
|
||||
|
||||
const (
|
||||
@ -26,6 +28,8 @@ const (
|
||||
portFlagName = "port"
|
||||
templateNameFlagName = "template-name"
|
||||
defaultErrorPageFlagName = "default-error-page"
|
||||
defaultHTTPCodeFlagName = "default-http-code"
|
||||
showDetailsFlagName = "show-details"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -61,9 +65,21 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
"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
|
||||
@ -91,6 +107,22 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -103,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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
1
internal/config/testdata/bar-tpl.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<html><body>bar {{ code }}</body></html>
|
1
internal/config/testdata/foo-tpl.html
vendored
Normal file
1
internal/config/testdata/foo-tpl.html
vendored
Normal file
@ -0,0 +1 @@
|
||||
<html><body>foo {{ code }}</body></html>
|
4
internal/config/testdata/simple.yml
vendored
4
internal/config/testdata/simple.yml
vendored
@ -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:
|
||||
|
2
internal/env/env.go
vendored
2
internal/env/env.go
vendored
@ -11,6 +11,8 @@ const (
|
||||
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.
|
||||
|
4
internal/env/env_test.go
vendored
4
internal/env/env_test.go
vendored
@ -13,6 +13,8 @@ func TestConstants(t *testing.T) {
|
||||
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) {
|
||||
@ -24,6 +26,8 @@ func TestEnvVariable_Lookup(t *testing.T) {
|
||||
{giveEnv: TemplateName},
|
||||
{giveEnv: ConfigFilePath},
|
||||
{giveEnv: DefaultErrorPage},
|
||||
{giveEnv: DefaultHTTPCode},
|
||||
{giveEnv: ShowDetails},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
|
@ -27,8 +27,25 @@ func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHand
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
114
internal/http/core/errorpage.go
Normal file
114
internal/http/core/errorpage.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
internal/http/core/formats.go
Normal file
102
internal/http/core/formats.go
Normal 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")
|
||||
}
|
||||
}
|
117
internal/http/core/formats_test.go
Normal file
117
internal/http/core/formats_test.go
Normal 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)))
|
||||
})
|
||||
}
|
||||
}
|
33
internal/http/core/headers.go
Normal file
33
internal/http/core/headers.go
Normal 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"
|
||||
)
|
@ -1,38 +1,33 @@
|
||||
package errorpage
|
||||
|
||||
import (
|
||||
"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 (
|
||||
errorsPager interface {
|
||||
// GetPage with passed template name and error code.
|
||||
GetPage(templateName, code string) ([]byte, error)
|
||||
}
|
||||
|
||||
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(e errorsPager, p templatePicker) fasthttp.RequestHandler {
|
||||
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, showRequestDetails bool) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetContentType("text/plain; charset=utf-8") // default content type
|
||||
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
|
||||
|
||||
if code, ok := ctx.UserValue("code").(string); ok {
|
||||
if content, err := e.GetPage(p.Pick(), code); err == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
_, _ = ctx.Write(content)
|
||||
} else {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
_, _ = ctx.WriteString("requested code not available: " + err.Error()) // TODO customize the output?
|
||||
}
|
||||
} else { // will never happen
|
||||
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") // TODO customize the output?
|
||||
_, _ = ctx.WriteString("cannot extract requested code from the request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ func NewHandler(checker checker) fasthttp.RequestHandler {
|
||||
}
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
_, _ = ctx.WriteString("OK")
|
||||
}
|
||||
}
|
||||
|
@ -1,36 +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 (
|
||||
errorsPager interface {
|
||||
// GetPage with passed template name and error code.
|
||||
GetPage(templateName, code string) ([]byte, error)
|
||||
}
|
||||
|
||||
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(e errorsPager, p templatePicker, defaultPageCode string) fasthttp.RequestHandler {
|
||||
func NewHandler(
|
||||
cfg *config.Config,
|
||||
p templatePicker,
|
||||
rdr renderer,
|
||||
defaultPageCode string,
|
||||
defaultHTTPCode uint16,
|
||||
showRequestDetails bool,
|
||||
) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
content, err := e.GetPage(p.Pick(), defaultPageCode)
|
||||
pageCode, httpCode := defaultPageCode, int(defaultHTTPCode)
|
||||
|
||||
if err == nil {
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
_, _ = ctx.Write(content)
|
||||
|
||||
return
|
||||
if returnCode, ok := extractCodeToReturn(ctx); ok {
|
||||
pageCode, httpCode = strconv.Itoa(returnCode), returnCode
|
||||
}
|
||||
|
||||
ctx.SetContentType("text/plain; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusNotAcceptable)
|
||||
_, _ = ctx.WriteString("default page code " + defaultPageCode + " is not available: " + err.Error())
|
||||
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
|
||||
}
|
||||
|
16
internal/http/handlers/metrics/handler.go
Normal file
16
internal/http/handlers/metrics/handler.go
Normal 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}),
|
||||
)
|
||||
}
|
7
internal/http/handlers/metrics/handler_test.go
Normal file
7
internal/http/handlers/metrics/handler_test.go
Normal 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")
|
||||
}
|
@ -6,20 +6,26 @@ import (
|
||||
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
log *zap.Logger
|
||||
fast *fasthttp.Server
|
||||
router *router.Router
|
||||
rdr *tpl.TemplateRenderer
|
||||
}
|
||||
|
||||
const (
|
||||
@ -29,7 +35,7 @@ const (
|
||||
)
|
||||
|
||||
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,28 +59,50 @@ func (s *Server) Start(ip string, port uint16) error {
|
||||
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
type (
|
||||
errorsPager interface {
|
||||
// GetPage with passed template name and error code.
|
||||
GetPage(templateName, code string) ([]byte, error)
|
||||
}
|
||||
|
||||
templatePicker interface {
|
||||
// Pick the template name for responding.
|
||||
Pick() string
|
||||
}
|
||||
)
|
||||
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(errorsPager errorsPager, templatePicker templatePicker, defaultPageCode string) {
|
||||
s.router.GET("/", indexHandler.NewHandler(errorsPager, templatePicker, defaultPageCode))
|
||||
func (s *Server) Register(
|
||||
cfg *config.Config,
|
||||
templatePicker templatePicker,
|
||||
defaultPageCode string,
|
||||
defaultHTTPCode uint16,
|
||||
showDetails bool,
|
||||
) error {
|
||||
reg, m := metrics.NewRegistry(), metrics.NewMetrics()
|
||||
|
||||
if err := m.Register(reg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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()))
|
||||
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
|
||||
s.router.GET("/{code}.html", errorpageHandler.NewHandler(errorsPager, templatePicker))
|
||||
|
||||
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() error { return s.fast.Shutdown() }
|
||||
func (s *Server) Stop() error {
|
||||
if err := s.rdr.Close(); err != nil {
|
||||
defer func() { _ = s.fast.Shutdown() }()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return s.fast.Shutdown()
|
||||
}
|
||||
|
52
internal/metrics/metrics.go
Normal file
52
internal/metrics/metrics.go
Normal 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
|
||||
}
|
70
internal/metrics/metrics_test.go
Normal file
70
internal/metrics/metrics_test.go
Normal 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
|
||||
}
|
20
internal/metrics/registry.go
Normal file
20
internal/metrics/registry.go
Normal 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
|
||||
}
|
18
internal/metrics/registry_test.go
Normal file
18
internal/metrics/registry_test.go
Normal 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
83
internal/pick/picker.go
Normal 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")
|
||||
}
|
||||
}
|
57
internal/pick/picker_test.go
Normal file
57
internal/pick/picker_test.go
Normal 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() })
|
||||
}
|
@ -1,64 +1,20 @@
|
||||
package pick
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pickMode byte
|
||||
|
||||
const (
|
||||
First pickMode = 1 + iota // Always pick the first element
|
||||
RandomOnce // Pick random element once (any future Pick calls will return the same element)
|
||||
RandomEveryTime // Always Pick the random element
|
||||
)
|
||||
|
||||
type StringsSlice struct {
|
||||
items []string
|
||||
mode pickMode
|
||||
lastUsedIdx int // -1 when unset, needed for RandomOnce mode
|
||||
rnd *rand.Rand // will be nil for the First mode
|
||||
s []string
|
||||
p *picker
|
||||
}
|
||||
|
||||
// NewStringsSlice creates new StringsSlice.
|
||||
func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
|
||||
var rnd *rand.Rand
|
||||
|
||||
if mode != First {
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
}
|
||||
|
||||
return &StringsSlice{
|
||||
items: items,
|
||||
mode: mode,
|
||||
lastUsedIdx: -1,
|
||||
rnd: rnd,
|
||||
}
|
||||
return &StringsSlice{s: items, p: NewPicker(uint32(len(items)-1), mode)}
|
||||
}
|
||||
|
||||
// Pick an element from the strings slice.
|
||||
func (s *StringsSlice) Pick() string {
|
||||
if l := len(s.items); l == 0 {
|
||||
if len(s.s) == 0 {
|
||||
return ""
|
||||
} else if l == 1 {
|
||||
return s.items[0]
|
||||
}
|
||||
|
||||
switch s.mode {
|
||||
case First:
|
||||
return s.items[0]
|
||||
|
||||
case RandomOnce:
|
||||
if s.lastUsedIdx == -1 {
|
||||
s.lastUsedIdx = s.rnd.Intn(len(s.items))
|
||||
}
|
||||
|
||||
return s.items[s.lastUsedIdx]
|
||||
|
||||
case RandomEveryTime:
|
||||
return s.items[s.rnd.Intn(len(s.items))]
|
||||
|
||||
default:
|
||||
panic("pick: unsupported mode")
|
||||
}
|
||||
return s.s[s.p.NextIndex()]
|
||||
}
|
||||
|
@ -1,102 +1,49 @@
|
||||
package pick_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/pick"
|
||||
)
|
||||
|
||||
func TestStringsSlice_Pick_First(t *testing.T) {
|
||||
for name, items := range map[string][]string{
|
||||
"0 item": {},
|
||||
"1 item": {"foo"},
|
||||
"3 items": {"foo", "bar", "baz"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := pick.NewStringsSlice(items, pick.First)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
if len(items) == 0 {
|
||||
assert.Equal(t, "", p.Pick())
|
||||
} else {
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringsSlice_Pick_RandomOnce(t *testing.T) {
|
||||
p := pick.NewStringsSlice([]string{}, pick.RandomOnce)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
|
||||
p = pick.NewStringsSlice([]string{"foo"}, pick.RandomOnce)
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
|
||||
dataSet := randomStringsSlice(t, 2048) // if this test will fail - Increase this value
|
||||
p = pick.NewStringsSlice(dataSet, pick.RandomOnce)
|
||||
picked := p.Pick()
|
||||
|
||||
assert.NotEqual(t, dataSet[0], p.Pick())
|
||||
|
||||
for i := 0; i < 32; i++ {
|
||||
assert.Equal(t, picked, p.Pick())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringsSlice_Pick_RandomEveryTime(t *testing.T) {
|
||||
p := pick.NewStringsSlice([]string{}, pick.RandomEveryTime)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
|
||||
p = pick.NewStringsSlice([]string{"foo"}, pick.RandomEveryTime)
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
|
||||
dataSet := randomStringsSlice(t, 2048) // if this test will fail - Increase this value
|
||||
p = pick.NewStringsSlice(dataSet, pick.RandomEveryTime)
|
||||
|
||||
lastPicked := p.Pick()
|
||||
|
||||
for i := 0; i < 32; i++ {
|
||||
picked := p.Pick()
|
||||
|
||||
assert.NotEqual(t, lastPicked, picked)
|
||||
lastPicked = picked
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringsSlice_Pick_UnsupportedMode(t *testing.T) {
|
||||
p := pick.NewStringsSlice([]string{}, 255)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
|
||||
p = pick.NewStringsSlice([]string{"foo"}, 255)
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
|
||||
p = pick.NewStringsSlice([]string{"foo", "bar"}, 255)
|
||||
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
func randomStringsSlice(t *testing.T, itemsCount int) []string {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
items = make([]string, itemsCount)
|
||||
letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=")
|
||||
)
|
||||
|
||||
for i := 0; i < len(items); i++ {
|
||||
b := make([]rune, 32)
|
||||
|
||||
for j := range b {
|
||||
b[j] = letters[rnd.Intn(len(letters))]
|
||||
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())
|
||||
}
|
||||
|
||||
items[i] = string(b)
|
||||
}
|
||||
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.First)
|
||||
|
||||
return items
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,136 +0,0 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
// ErrorPages is a error page templates generator.
|
||||
ErrorPages struct {
|
||||
mu sync.RWMutex
|
||||
templates map[string][]byte // map[template_name]raw_content
|
||||
pages map[string]*pageProperties // map[page_code]props
|
||||
state map[string]map[string][]byte // map[template_name]map[page_code]content
|
||||
}
|
||||
|
||||
pageProperties struct {
|
||||
message, description string
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownTemplate = errors.New("unknown template") // unknown template
|
||||
ErrUnknownPageCode = errors.New("unknown page code") // unknown page code
|
||||
)
|
||||
|
||||
// NewErrorPages creates ErrorPages templates generator.
|
||||
func NewErrorPages() ErrorPages {
|
||||
return ErrorPages{
|
||||
templates: make(map[string][]byte),
|
||||
pages: make(map[string]*pageProperties),
|
||||
state: make(map[string]map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
// AddTemplate to the generator. Template can contain the special placeholders for the error code, message and
|
||||
// description:
|
||||
// {{ code }} - for the code
|
||||
// {{ message }} - for the message
|
||||
// {{ description }} - for the description
|
||||
func (e *ErrorPages) AddTemplate(templateName string, content []byte) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.templates[templateName] = content
|
||||
e.state[templateName] = make(map[string][]byte)
|
||||
|
||||
for code, props := range e.pages { // update the state
|
||||
e.state[templateName][code] = e.makeReplaces(content, code, props.message, props.description)
|
||||
}
|
||||
}
|
||||
|
||||
// AddPage with the passed code, message and description. This page will ba available for the all templates.
|
||||
func (e *ErrorPages) AddPage(code, message, description string) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
e.pages[code] = &pageProperties{message, description}
|
||||
|
||||
for templateName, content := range e.templates { // update the state
|
||||
e.state[templateName][code] = e.makeReplaces(content, code, message, description)
|
||||
}
|
||||
}
|
||||
|
||||
// GetPage with passed template name and error code.
|
||||
func (e *ErrorPages) GetPage(templateName, code string) (content []byte, err error) {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
if pages, templateExists := e.state[templateName]; templateExists {
|
||||
if c, pageExists := pages[code]; pageExists {
|
||||
content = c
|
||||
} else {
|
||||
err = ErrUnknownPageCode
|
||||
}
|
||||
} else {
|
||||
err = ErrUnknownTemplate
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IteratePages will call the passed function for each page and template.
|
||||
func (e *ErrorPages) IteratePages(fn func(template, code string, content []byte) error) error {
|
||||
e.mu.RLock()
|
||||
defer e.mu.RUnlock()
|
||||
|
||||
for tplName, codes := range e.state {
|
||||
for code, content := range codes {
|
||||
if err := fn(tplName, code, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 }}")},
|
||||
}
|
||||
|
||||
func (e *ErrorPages) makeReplaces(where []byte, code, message, description string) []byte {
|
||||
for tkn, set := range tknSets {
|
||||
var replaceWith []byte
|
||||
|
||||
switch tkn {
|
||||
case tknCode:
|
||||
replaceWith = []byte(code)
|
||||
case tknMessage:
|
||||
replaceWith = []byte(message)
|
||||
case tknDescription:
|
||||
replaceWith = []byte(description)
|
||||
default:
|
||||
panic("tpl: unsupported token") // this is like a fuse, will never occur during normal usage
|
||||
}
|
||||
|
||||
if len(replaceWith) > 0 {
|
||||
for i := 0; i < len(set); i++ {
|
||||
where = bytes.ReplaceAll(where, set[i], replaceWith)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return where
|
||||
}
|
@ -1,132 +0,0 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorPages_GetPage(t *testing.T) {
|
||||
e := tpl.NewErrorPages()
|
||||
|
||||
e.AddTemplate("foo", []byte("{{code}}: {{ message }} {{description}}"))
|
||||
e.AddPage("200", "ok", "all is ok")
|
||||
e.AddTemplate("bar", []byte("{{ code }} _ {{message}} ({{ description }})"))
|
||||
e.AddPage("201", "lorem", "ipsum")
|
||||
|
||||
content, err := e.GetPage("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.GetPage("foo", "201")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "201: lorem ipsum", string(content))
|
||||
|
||||
content, err = e.GetPage("bar", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200 _ ok (all is ok)", string(content))
|
||||
|
||||
content, err = e.GetPage("bar", "201")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "201 _ lorem (ipsum)", string(content))
|
||||
|
||||
content, err = e.GetPage("foo", "666")
|
||||
assert.ErrorIs(t, err, tpl.ErrUnknownPageCode)
|
||||
assert.Nil(t, content)
|
||||
|
||||
content, err = e.GetPage("baz", "200")
|
||||
assert.ErrorIs(t, err, tpl.ErrUnknownTemplate)
|
||||
assert.Nil(t, content)
|
||||
}
|
||||
|
||||
func TestErrorPages_GetPage_Concurrent(t *testing.T) {
|
||||
e := tpl.NewErrorPages()
|
||||
|
||||
init := func() {
|
||||
e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
|
||||
e.AddPage("200", "ok", "all is ok")
|
||||
e.AddPage("201", "lorem", "ipsum")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
init()
|
||||
|
||||
for i := 0; i < 1234; i++ {
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
init() // make re-initialization
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
content, err := e.GetPage("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.GetPage("foo", "201")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "201: lorem ipsum", string(content))
|
||||
|
||||
content, err = e.GetPage("foo", "666")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, content)
|
||||
|
||||
content, err = e.GetPage("bar", "200")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, content)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestErrorPages_IteratePages(t *testing.T) {
|
||||
e := tpl.NewErrorPages()
|
||||
|
||||
e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
|
||||
e.AddTemplate("bar", []byte("{{ code }}: {{ message }} {{ description }}"))
|
||||
e.AddPage("200", "ok", "all is ok")
|
||||
e.AddPage("400", "Bad Request", "")
|
||||
|
||||
visited := make(map[string]map[string]bool) // map[template]codes
|
||||
|
||||
assert.NoError(t, e.IteratePages(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 TestErrorPages_IteratePages_WillReturnTheError(t *testing.T) {
|
||||
e := tpl.NewErrorPages()
|
||||
|
||||
e.AddTemplate("foo", []byte("{{ code }}: {{ message }} {{ description }}"))
|
||||
e.AddPage("200", "ok", "all is ok")
|
||||
|
||||
assert.EqualError(t, e.IteratePages(func(template, code string, content []byte) error {
|
||||
return errors.New("foo error")
|
||||
}), "foo error")
|
||||
}
|
25
internal/tpl/hasher.go
Normal file
25
internal/tpl/hasher.go
Normal 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
|
||||
}
|
35
internal/tpl/hasher_test.go
Normal file
35
internal/tpl/hasher_test.go
Normal 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)
|
||||
}
|
39
internal/tpl/properties.go
Normal file
39
internal/tpl/properties.go
Normal 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) }
|
66
internal/tpl/properties_test.go
Normal file
66
internal/tpl/properties_test.go
Normal 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
216
internal/tpl/render.go
Normal 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
119
internal/tpl/render_test.go
Normal 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"},
|
||||
)
|
||||
}
|
||||
}
|
108
schemas/config/1.0.schema.json
Normal file
108
schemas/config/1.0.schema.json
Normal 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
13
schemas/config/readme.md
Normal 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
15
schemas/readme.md
Normal 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
111
templates/cats.html
Normal 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>
|
@ -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>
|
||||
<!--
|
||||
|
@ -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>
|
||||
<!--
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
1
templates/readme.md
Normal file
@ -0,0 +1 @@
|
||||
# TODO: Write docs
|
@ -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
7
test/hurl/404.hurl
Normal 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"
|
10
test/hurl/code_502_default.hurl
Normal file
10
test/hurl/code_502_default.hurl
Normal 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"
|
43
test/hurl/code_502_json.hurl
Normal file
43
test/hurl/code_502_json.hurl
Normal 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"
|
42
test/hurl/code_502_xml.hurl
Normal file
42
test/hurl/code_502_xml.hurl
Normal 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
12
test/hurl/healthz.hurl
Normal 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
34
test/hurl/index.hurl
Normal 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
8
test/hurl/metrics.hurl
Normal 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
27
test/hurl/readme.md
Normal 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
8
test/hurl/version.hurl
Normal 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
37
test/hurl/x_code.hurl
Normal 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
9
test/wrk/request.lua
Normal 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
|
Reference in New Issue
Block a user