mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Compare commits
119 Commits
Author | SHA1 | Date | |
---|---|---|---|
7329d7697c | |||
83f38cdd16 | |||
057006366d | |||
f08539962e | |||
2cc8549cef | |||
8f8e5abd3d | |||
1889a57c05 | |||
c42ff85dd6 | |||
00d3c10e5e | |||
7153c260d8 | |||
48bd1a44e6 | |||
bde35e2c79 | |||
d649e371a5 | |||
01c2a37055 | |||
a932f94ec0 | |||
3ac2c74249 | |||
18af96bada | |||
445aad8b41 | |||
b61cc7460f | |||
c9586fe79a | |||
405afec38a | |||
5e0be010b7 | |||
9bc00fa4ca | |||
6742381562 | |||
6d3ced480d | |||
e8fa8896c9 | |||
c9bd47618d | |||
3ffb952cdd | |||
b5892f44d9 | |||
769b0cebb6 | |||
8f49ff7204 | |||
f89bdfbd51 | |||
1b2e899201 | |||
6c0c85544e | |||
d3d1c62411 | |||
8019d07cab | |||
d21a6f2797 | |||
a3389aaafa | |||
c6a7e30609 | |||
01abc48a01 | |||
d6374d7edf | |||
7ebfac9dc2 | |||
64d4798156 | |||
4adad3df10 | |||
30a7b2793f | |||
2d9deb7370 | |||
873944f90f | |||
cd5abe458b | |||
481e11d527 | |||
fac7394ae2 | |||
4a918b1899 | |||
05be3841d7 | |||
02cadcd907 | |||
94dff2421c | |||
51f8824659 | |||
e82c02c768 | |||
dc51e3192c | |||
45ca69432b | |||
f5f572a4d3 | |||
2d418ecffa | |||
c6b3342361 | |||
3614f0503f | |||
a2ee92acc4 | |||
93dddd75d9 | |||
c17587ca6b | |||
d7d5245d07 | |||
6c0885a5d3 | |||
4b83ce7d09 | |||
d6cebc27ab | |||
2bcbd4ba41 | |||
edc05ec6d2 | |||
94b6af6d53 | |||
8d24125eee | |||
97fc3b8693 | |||
ac1c19df28 | |||
b7f82e4635 | |||
62493411b4 | |||
0e20e39cd2 | |||
4bdbb882b5 | |||
4b2a792148 | |||
1d41cf190b | |||
e857c0309b | |||
06aff4ecb3 | |||
3145bdfa00 | |||
178e6b2d9b | |||
7a3dc917a2 | |||
8a14836bd1 | |||
ae2bf27463 | |||
c53a87b816 | |||
8463ecf00d | |||
1d7596b3df | |||
251e0a01cf | |||
22d3e3485e | |||
375272b561 | |||
7e7f956fae | |||
d672112cc2 | |||
32b92611a7 | |||
cc6cbc7d47 | |||
690a405994 | |||
f72c2b85fd | |||
42523ae9d9 | |||
da2dc5c63a | |||
a0a1d3caca | |||
915e810088 | |||
00c139b525 | |||
eca99eb569 | |||
dfaeea7483 | |||
f71b07f647 | |||
be0a3c4820 | |||
04bf2231bc | |||
ba98272530 | |||
fab38255eb | |||
88278d37a7 | |||
32daf80b76 | |||
13e7a72790 | |||
0efbccbb18 | |||
bed576f26c | |||
f75bf15552 | |||
9915e321f4 |
@ -10,7 +10,7 @@ indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml, yaml, sh, conf}]
|
||||
[*.{yml, yaml, sh, conf, json}]
|
||||
indent_size = 2
|
||||
|
||||
[{Makefile, go.mod, *.go}]
|
||||
|
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# Text files have auto line endings
|
||||
* text=auto
|
||||
|
||||
# Go source files always have LF line endings
|
||||
*.go text eol=lf
|
||||
|
||||
# Disable next extensions in project "used languages" list
|
||||
*.lua linguist-detectable=false
|
||||
*.html linguist-detectable=false
|
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
|
53
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# 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
|
||||
- label: And it has nothing to do with Traefik
|
||||
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
|
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
12
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# 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
|
||||
|
||||
- name: 🌀 I have a question about Traefik..
|
||||
url: https://community.traefik.io/
|
||||
about: In this case - ask in the Traefik community
|
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
|
7
.github/renovate.json
vendored
Normal file
7
.github/renovate.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>tarampampam/.github//renovate/default",
|
||||
":rebaseStalePrs"
|
||||
]
|
||||
}
|
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@v3
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v3 # Action page: <https://github.com/peter-evans/dockerhub-description>
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_LOGIN }}
|
||||
password: ${{ secrets.DOCKER_USER_PASSWORD }}
|
||||
repository: tarampampam/error-pages
|
36
.github/workflows/release.yml
vendored
36
.github/workflows/release.yml
vendored
@ -5,6 +5,18 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
purge-cdn-cache:
|
||||
name: Purge jsDelivr CDN cache
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: gacts/purge-jsdelivr-cache@v1 # Action page: <https://github.com/gacts/purge-jsdelivr-cache>
|
||||
with:
|
||||
url: |
|
||||
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.js
|
||||
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js
|
||||
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@latest/l10n/l10n.js
|
||||
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@latest/l10n/l10n.min.js
|
||||
|
||||
build:
|
||||
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
|
||||
runs-on: ubuntu-20.04
|
||||
@ -14,10 +26,10 @@ jobs:
|
||||
os: [linux, darwin] # linux, freebsd, darwin, windows
|
||||
arch: [amd64] # amd64, 386
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17.5}
|
||||
- uses: actions/setup-go@v3
|
||||
with: {go-version: 1.18}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
@ -46,27 +58,27 @@ jobs:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
|
||||
- uses: docker/setup-qemu-action@v2 # Action page: <https://github.com/docker/setup-qemu-action>
|
||||
|
||||
- uses: docker/setup-buildx-action@v1 # Action page: <https://github.com/docker/setup-buildx-action>
|
||||
- uses: docker/setup-buildx-action@v2 # Action page: <https://github.com/docker/setup-buildx-action>
|
||||
|
||||
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
- uses: docker/login-action@v2 # Action page: <https://github.com/docker/login-action>
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_LOGIN }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
- uses: docker/login-action@v2 # Action page: <https://github.com/docker/login-action>
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
|
||||
- uses: docker/build-push-action@v3 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@ -75,8 +87,12 @@ jobs:
|
||||
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
|
||||
tags: |
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version }}
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
|
||||
tarampampam/error-pages:latest
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
|
||||
|
||||
demo:
|
||||
|
120
.github/workflows/tests.yml
vendored
120
.github/workflows/tests.yml
vendored
@ -8,40 +8,72 @@ on:
|
||||
pull_request:
|
||||
paths-ignore: ['**.md']
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs: # Docs: <https://git.io/JvxXE>
|
||||
gitleaks:
|
||||
name: Gitleaks
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with: {fetch-depth: 0}
|
||||
|
||||
- uses: zricethezav/gitleaks-action@v1 # Action page: <https://github.com/zricethezav/gitleaks-action>
|
||||
- name: Check for GitLeaks
|
||||
uses: gacts/gitleaks@v1 # Action page: <https://github.com/gacts/gitleaks>
|
||||
|
||||
golangci-lint:
|
||||
name: Golang-CI (lint)
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: golangci/golangci-lint-action@v3 # Action page: <https://github.com/golangci/golangci-lint-action>
|
||||
|
||||
validate-config-file:
|
||||
name: Validate config file
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
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
|
||||
uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action>
|
||||
with:
|
||||
version: v1.42 # without patch version
|
||||
only-new-issues: false # show only new issues if it's a pull request
|
||||
run: ajv validate --all-errors --verbose -s ./schemas/config/1.0.schema.json -d ./error-pages.y*ml
|
||||
|
||||
lint-l10n:
|
||||
name: Lint l10n file(s)
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with: {node-version: '16'}
|
||||
|
||||
- name: Install eslint
|
||||
run: npm install -g eslint@v8 # Package page: <https://www.npmjs.com/package/eslint>
|
||||
|
||||
- name: Run linter
|
||||
working-directory: l10n
|
||||
run: eslint ./*.js
|
||||
|
||||
go-test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17}
|
||||
- uses: actions/setup-go@v3
|
||||
with: {go-version: 1.18}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
|
||||
|
||||
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
id: go-cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
@ -54,7 +86,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
- name: Run Unit tests
|
||||
run: go test -race -covermode=atomic -coverprofile /tmp/coverage.txt ./...
|
||||
|
||||
- uses: codecov/codecov-action@v2 # https://github.com/codecov/codecov-action
|
||||
- uses: codecov/codecov-action@v3 # https://github.com/codecov/codecov-action
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: /tmp/coverage.txt
|
||||
@ -68,18 +100,18 @@ 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}
|
||||
- uses: actions/setup-go@v3
|
||||
with: {go-version: 1.18}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
@ -99,7 +131,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
if: matrix.os == 'linux'
|
||||
run: ./error-pages version && ./error-pages -h
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: error-pages-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: error-pages
|
||||
@ -111,9 +143,9 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: error-pages-linux-amd64
|
||||
path: .artifact
|
||||
@ -135,18 +167,22 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
test -f ./out/noise/404.html
|
||||
test -f ./out/hacker-terminal/404.html
|
||||
test -f ./out/cats/404.html
|
||||
test -f ./out/lost-in-space/404.html
|
||||
test -f ./out/app-down/404.html
|
||||
test -f ./out/connection/404.html
|
||||
test -f ./out/matrix/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
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
|
||||
- uses: docker/build-push-action@v3 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@ -156,7 +192,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
|
||||
- run: docker save app:ci > ./docker-image.tar
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: docker-image
|
||||
path: ./docker-image.tar
|
||||
@ -167,19 +203,25 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [docker-image]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3 # is needed for `upload-sarif` action
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-image
|
||||
path: .artifact
|
||||
|
||||
- working-directory: .artifact
|
||||
run: docker load < docker-image.tar
|
||||
|
||||
- uses: anchore/scan-action@v3 # action page: <https://github.com/anchore/scan-action>
|
||||
- uses: aquasecurity/trivy-action@0.5.1 # action page: <https://github.com/aquasecurity/trivy-action>
|
||||
with:
|
||||
image: app:ci
|
||||
fail-build: true
|
||||
severity-cutoff: low # negligible, low, medium, high or critical
|
||||
input: .artifact/docker-image.tar
|
||||
format: sarif
|
||||
severity: MEDIUM,HIGH,CRITICAL
|
||||
exit-code: 1
|
||||
output: trivy-results.sarif
|
||||
|
||||
- uses: github/codeql-action/upload-sarif@v2
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
with: {sarif_file: trivy-results.sarif}
|
||||
|
||||
poke-docker-image:
|
||||
name: Run the docker image
|
||||
@ -187,7 +229,9 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
needs: [docker-image]
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-image
|
||||
path: .artifact
|
||||
@ -195,17 +239,15 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
- working-directory: .artifact
|
||||
run: docker load < docker-image.tar
|
||||
|
||||
- uses: gacts/install-hurl@v1
|
||||
|
||||
- name: Run container with the app
|
||||
run: docker run --rm -d -p "8080:8080/tcp" -e "DEFAULT_HTTP_CODE=401" --name app app:ci
|
||||
run: docker run --rm -d -p "8080:8080/tcp" -e "SHOW_DETAILS=true" -e "PROXY_HTTP_HEADERS=X-Foo,Bar,Baz_blah" --name app app:ci
|
||||
|
||||
- name: Wait for container "healthy" state
|
||||
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
|
||||
|
||||
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 401
|
||||
- run: curl --fail http://127.0.0.1:8080/500.html
|
||||
- run: curl --fail http://127.0.0.1:8080/400.html
|
||||
- run: curl --fail http://127.0.0.1:8080/health/live
|
||||
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/foobar) -eq 404
|
||||
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 --summary ./test/hurl/*.hurl
|
||||
|
||||
- name: Stop the container
|
||||
if: always()
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
/.vscode
|
||||
|
||||
## Binaries
|
||||
error-pages
|
||||
/error-pages
|
||||
|
||||
## Temp dirs & trash
|
||||
/temp
|
||||
|
@ -42,7 +42,7 @@ 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
|
||||
- bodyclose # Checks whether HTTP response body is closed successfully
|
||||
- bidichk # Checks for dangerous unicode character sequences
|
||||
- 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())
|
||||
@ -71,17 +71,12 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
|
||||
- nakedret # Finds naked returns in functions greater than a specified function length
|
||||
- nestif # Reports deeply nested if statements
|
||||
- nlreturn # checks for a new line before return and branch statements to increase code clarity
|
||||
- noctx # finds sending http request without context.Context
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives
|
||||
- prealloc # Finds slice declarations that could potentially be preallocated
|
||||
- rowserrcheck # Checks whether Err of rows is checked successfully
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
|
||||
- structcheck # Finds unused struct fields
|
||||
- stylecheck # Stylecheck is a replacement for golint
|
||||
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
|
||||
- unconvert # Remove unnecessary type conversions
|
||||
- unparam # Reports unused function parameters
|
||||
- unused # Checks Go code for unused constants, variables, functions and types
|
||||
- varcheck # Finds unused global variables and constants
|
||||
- whitespace # Tool for detection of leading and trailing whitespace
|
||||
@ -95,4 +90,3 @@ issues:
|
||||
- funlen
|
||||
- scopelint
|
||||
- gocognit
|
||||
- noctx
|
||||
|
188
CHANGELOG.md
188
CHANGELOG.md
@ -4,6 +4,194 @@ 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.17.0
|
||||
|
||||
### Added
|
||||
|
||||
- Error pages now translated into Spanish 🇪🇸 [#124]
|
||||
|
||||
[#124]:https://github.com/tarampampam/error-pages/pull/124
|
||||
|
||||
## v2.16.0
|
||||
|
||||
### Added
|
||||
|
||||
- Error pages are now translated into German 🇩🇪 [#115]
|
||||
|
||||
[#115]:https://github.com/tarampampam/error-pages/pull/115
|
||||
|
||||
## v2.15.0
|
||||
|
||||
### Added
|
||||
|
||||
- Error pages now translated into Dutch 🇳🇱 [#104]
|
||||
|
||||
[#104]:https://github.com/tarampampam/error-pages/pull/104
|
||||
|
||||
## v2.14.0
|
||||
|
||||
### Added
|
||||
|
||||
- Error pages now translated into Portuguese 🇵🇹 [#103]
|
||||
|
||||
### Changed
|
||||
|
||||
- Go updated from `1.18.0` up to `1.18.1`
|
||||
|
||||
[#103]:https://github.com/tarampampam/error-pages/pull/103
|
||||
|
||||
## v2.13.0
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to disable error pages auto-localization (using `--disable-l10n` flag for the `serve` & `build` commands or environment variable `DISABLE_L10N`) [#91]
|
||||
|
||||
### Fixed
|
||||
|
||||
- User UID/GID changed to the numeric values in the dockerfile [#92]
|
||||
|
||||
[#92]:https://github.com/tarampampam/error-pages/issues/92
|
||||
[#91]:https://github.com/tarampampam/error-pages/issues/91
|
||||
|
||||
## v2.12.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix translation 🇫🇷 [#86]
|
||||
|
||||
[#85]:https://github.com/tarampampam/error-pages/pull/86
|
||||
|
||||
## v2.12.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Error pages now translated into 🇫🇷 [#82]
|
||||
|
||||
[#82]:https://github.com/tarampampam/error-pages/pull/82
|
||||
|
||||
## v2.11.0
|
||||
|
||||
### Added
|
||||
|
||||
- Template `matrix` [#81]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Localization mistakes [#81]
|
||||
|
||||
[#81]:https://github.com/tarampampam/error-pages/pull/81
|
||||
|
||||
## v2.10.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Template `shuffle`
|
||||
- Localization mistakes
|
||||
|
||||
## v2.10.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Error pages now translated into 🇺🇦 and 🇷🇺 languages [#80]
|
||||
|
||||
[#80]:https://github.com/tarampampam/error-pages/pull/80
|
||||
|
||||
## v2.9.0
|
||||
|
||||
### Added
|
||||
|
||||
- Template `connection` [#79]
|
||||
|
||||
[#79]:https://github.com/tarampampam/error-pages/pull/79
|
||||
|
||||
## v2.8.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Dark mode for `app-down` template
|
||||
|
||||
### Changed
|
||||
|
||||
- The index page for built error pages now supports a dark theme
|
||||
|
||||
## v2.8.0
|
||||
|
||||
### Added
|
||||
|
||||
- Template `app-down` [#74]
|
||||
|
||||
### Changed
|
||||
|
||||
- Go updated from `1.17.6` up to `1.18.0`
|
||||
|
||||
[#74]:https://github.com/tarampampam/error-pages/pull/74
|
||||
|
||||
## v2.7.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Logs includes request/response headers now [#67]
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to proxy HTTP headers from the requests to the responses (can be enabled using `--proxy-headers` flag for the `serve` command or environment variable `PROXY_HTTP_HEADERS`, headers list should be comma-separated) [#67]
|
||||
- Template `lost-in-space` [#68]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Template `l7-light` uses the dark colors in the browsers with the preferred dark theme
|
||||
|
||||
[#67]:https://github.com/tarampampam/error-pages/pull/67
|
||||
[#68]:https://github.com/tarampampam/error-pages/pull/68
|
||||
|
||||
## v2.6.0
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to change the template to the random once a day using "special" template name `random-daily` (or hourly, using `random-hourly`) [#48]
|
||||
|
||||
[#48]:https://github.com/tarampampam/error-pages/issues/48
|
||||
|
||||
## 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
|
||||
|
13
Dockerfile
13
Dockerfile
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1.2
|
||||
|
||||
# Image page: <https://hub.docker.com/_/golang>
|
||||
FROM golang:1.17.5-alpine as builder
|
||||
FROM golang:1.18-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
|
||||
@ -59,19 +60,19 @@ LABEL \
|
||||
COPY --from=builder /tmp/rootfs /
|
||||
|
||||
# Use an unprivileged user
|
||||
USER appuser:appuser
|
||||
USER 10001:10001
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
ENV LISTEN_PORT="8080" \
|
||||
TEMPLATE_NAME="ghost" \
|
||||
DEFAULT_ERROR_PAGE="404" \
|
||||
DEFAULT_HTTP_CODE="404"
|
||||
DEFAULT_HTTP_CODE="404" \
|
||||
SHOW_DETAILS="false" \
|
||||
DISABLE_L10N="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
|
||||
|
500
README.md
500
README.md
@ -1,389 +1,201 @@
|
||||
<p align="center">
|
||||
<img src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png" width="94" alt="" />
|
||||
<a href="https://github.com/tarampampam/error-pages#readme"><img src="https://socialify.git.ci/tarampampam/error-pages/image?description=1&font=Raleway&forks=1&issues=1&logo=https%3A%2F%2Fhsto.org%2Fwebt%2Frm%2F9y%2Fww%2Frm9ywwx3gjv9agwkcmllhsuyo7k.png&owner=1&pulls=1&pattern=Solid&stargazers=1&theme=Dark" alt="banner" width="100%" /></a>
|
||||
</p>
|
||||
|
||||
# HTTP's error pages
|
||||
<p align="center">
|
||||
<a href="#"><img src="https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true&label=&logo=go&logoColor=white&style=flat-square" alt="" /></a>
|
||||
<a href="https://codecov.io/gh/tarampampam/error-pages"><img src="https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30&label=&logo=codecov&logoColor=white&style=flat-square" alt="" /></a>
|
||||
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github&style=flat-square" alt="" /></a>
|
||||
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github&style=flat-square" alt="" /></a>
|
||||
<a href="https://hub.docker.com/r/tarampampam/error-pages"><img src="https://img.shields.io/docker/pulls/tarampampam/error-pages.svg?maxAge=30&label=pulls&logo=docker&logoColor=white&style=flat-square" alt="" /></a>
|
||||
<a href="https://hub.docker.com/r/tarampampam/error-pages"><img src="https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30&label=size&logo=docker&logoColor=white&style=flat-square" alt="" /></a>
|
||||
<a href="https://github.com/tarampampam/error-pages/blob/master/LICENSE"><img src="https://img.shields.io/github/license/tarampampam/error-pages.svg?maxAge=30&style=flat-square" alt="" /></a>
|
||||
</p>
|
||||
|
||||
[![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]
|
||||
<p align="center"><sup>22 feb. 2022 - ⚡ Our Docker image was downloaded <strong>one MILLION times</strong> from the docker hub! ⚡</sup></p>
|
||||
|
||||
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)
|
||||
- Simple error pages generator, written in Go
|
||||
- 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 in 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 are `json` and `xml`)
|
||||
- Writes logs in `json` format
|
||||
- Contains healthcheck endpoint (`/healthz`)
|
||||
- Contains metrics endpoint (`/metrics`) in Prometheus format
|
||||
- Lightweight docker image _(~4.6Mb 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]
|
||||
- Error pages can be [embedded into your own `nginx`][wiki-usage-with-nginx] docker image
|
||||
- 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
|
||||
- Localized (🇺🇸, 🇫🇷, 🇺🇦, 🇷🇺, 🇵🇹, 🇳🇱, 🇩🇪, 🇪🇸) HTML error pages (translation process [described here](https://github.com/tarampampam/error-pages/tree/master/l10n) - other translations are welcome!)
|
||||
|
||||
## 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):
|
||||
💣 **Or** you can download **already rendered** error pages pack as a [zip][pages-pack-zip] or [tar.gz][pages-pack-tar-gz] archive.
|
||||
|
||||
```bash
|
||||
$ docker run --rm -it \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock:ro" \
|
||||
wagoodman/dive:latest \
|
||||
tarampampam/error-pages:latest
|
||||
[pages-pack-zip]:https://github.com/tarampampam/error-pages/zipball/gh-pages/
|
||||
[pages-pack-tar-gz]:https://github.com/tarampampam/error-pages/tarball/gh-pages/
|
||||
|
||||
## 🛠 Usage
|
||||
|
||||
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)
|
||||
`cats` | [](https://tarampampam.github.io/error-pages/cats/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] |
|
||||
| `lost-in-space` | [![lost-in-space][lost-in-space-screen]][lost-in-space-link] |
|
||||
| `app-down` | [![app-down][app-down-screen]][app-down-link] |
|
||||
| `connection` | [![connection][connection-screen]][connection-link] |
|
||||
| `matrix` | [![matrix][matrix-screen]][matrix-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/hx/ca/mm/hxcammfm7qjmogtvsjxcidgf7c8.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
|
||||
[lost-in-space-screen]:https://hsto.org/webt/lf/ln/x8/lflnx8fuy4rofxju34ttskijdsu.gif
|
||||
[lost-in-space-link]:https://tarampampam.github.io/error-pages/lost-in-space/404.html
|
||||
[app-down-screen]:https://habrastorage.org/webt/j2/la/fj/j2lafjvu_xjflzrvhiixobxy_ca.png
|
||||
[app-down-link]:https://tarampampam.github.io/error-pages/app-down/404.html
|
||||
[connection-screen]:https://hsto.org/webt/x4/ah/jb/x4ahjboo4-arm3bxpaash_sflmw.png
|
||||
[connection-link]:https://tarampampam.github.io/error-pages/connection/404.html
|
||||
[matrix-screen]:https://hsto.org/webt/ng/tf/oi/ngtfoiolvmq6hf15kimcxmhprhk.gif
|
||||
[matrix-link]:https://tarampampam.github.io/error-pages/matrix/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-release]:https://img.shields.io/github/release/tarampampam/error-pages.svg?maxAge=30
|
||||
[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.23-alpine
|
||||
|
||||
COPY --chown=nginx \
|
||||
./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --chown=nginx \
|
||||
--from=tarampampam/error-pages:2.2.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.2.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
|
||||
[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")
|
||||
}
|
@ -8,7 +8,7 @@ volumes:
|
||||
|
||||
services:
|
||||
app: &app-service
|
||||
image: golang:1.17.5-buster # Image page: <https://hub.docker.com/_/golang>
|
||||
image: golang:1.18-buster # Image page: <https://hub.docker.com/_/golang>
|
||||
working_dir: /src
|
||||
environment:
|
||||
HOME: /tmp
|
||||
@ -23,20 +23,15 @@ services:
|
||||
<<: *app-service
|
||||
ports:
|
||||
- "8080:8080/tcp" # Open <http://127.0.0.1:8080>
|
||||
command:
|
||||
- go
|
||||
- run
|
||||
- ./cmd/error-pages
|
||||
- serve
|
||||
- --verbose
|
||||
- --port=8080
|
||||
command: sh -c "go build -o /tmp/app ./cmd/error-pages && /tmp/app serve"
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/health/live']
|
||||
interval: 5s
|
||||
timeout: 2s
|
||||
test: ['CMD', '/tmp/app', 'healthcheck', '--log-json']
|
||||
interval: 4s
|
||||
timeout: 1s
|
||||
start_period: 5s
|
||||
|
||||
golint:
|
||||
image: golangci/golangci-lint:v1.42-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
|
||||
image: golangci/golangci-lint:v1.47-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 +39,11 @@ services:
|
||||
- golint-cache:/tmp/golint:rw
|
||||
working_dir: /src
|
||||
command: /bin/true
|
||||
|
||||
hurl:
|
||||
image: orangeopensource/hurl:1.6.1
|
||||
volumes:
|
||||
- .:/src:ro
|
||||
working_dir: /src
|
||||
depends_on:
|
||||
web: {condition: service_healthy}
|
||||
|
@ -11,6 +11,51 @@ templates:
|
||||
- path: ./templates/noise.html
|
||||
- path: ./templates/hacker-terminal.html
|
||||
- path: ./templates/cats.html
|
||||
- path: ./templates/lost-in-space.html
|
||||
- path: ./templates/app-down.html
|
||||
- path: ./templates/connection.html
|
||||
- path: ./templates/matrix.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:
|
||||
|
31
go.mod
31
go.mod
@ -1,32 +1,41 @@
|
||||
module github.com/tarampampam/error-pages
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/a8m/envsubst v1.3.0
|
||||
github.com/fasthttp/router v1.4.4
|
||||
github.com/fasthttp/router v1.4.10
|
||||
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.3.0
|
||||
github.com/prometheus/client_golang v1.12.2
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/valyala/fasthttp v1.31.0
|
||||
go.uber.org/zap v1.19.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/valyala/fasthttp v1.38.0
|
||||
go.uber.org/zap v1.21.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
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/klauspost/compress v1.15.0 // 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-20220530130905-52f3993e8d6d // 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-20211205182925-97ca703d548d // indirect
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
396
go.sum
396
go.sum
@ -13,20 +13,6 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
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=
|
||||
@ -35,7 +21,6 @@ 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.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=
|
||||
@ -48,91 +33,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/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/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/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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
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/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/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.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/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=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
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/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
|
||||
github.com/fasthttp/router v1.4.4 h1:Z025tHFTjDp6T6QMBjloyGL6KV5wtakW365K/7KiE1c=
|
||||
github.com/fasthttp/router v1.4.4/go.mod h1:TiyF2kc+mogKcTxqkhUbiXpwklouv5dN58A0ZUo8J6s=
|
||||
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/fasthttp/router v1.4.10 h1:C8z6K1pTqhLjSv97/qCY9tZiiPT8JuFwDoO9E2HJFWQ=
|
||||
github.com/fasthttp/router v1.4.10/go.mod h1:FGSUOg9SQ/tU864SfD23kG/HwfD0akXqOqhTQ27gTFQ=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
@ -140,8 +91,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
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=
|
||||
@ -157,9 +106,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
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=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@ -169,16 +117,12 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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=
|
||||
@ -186,206 +130,131 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
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/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.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-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/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.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/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.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
|
||||
github.com/klauspost/compress v1.15.0/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/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
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.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/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/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.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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
|
||||
github.com/prometheus/client_golang v1.12.2/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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
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/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
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/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
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/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.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/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
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.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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
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=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
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.31.0 h1:lrauRLII19afgCs2fnWRJ4M5IkV0lo2FqA61uGkNBfE=
|
||||
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||
github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco=
|
||||
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
|
||||
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.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=
|
||||
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.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||
go.uber.org/zap v1.21.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-20220214200702-86341886e292/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=
|
||||
@ -408,8 +277,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
@ -418,13 +285,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
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-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=
|
||||
@ -437,7 +300,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
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=
|
||||
@ -452,35 +314,16 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
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=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
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-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=
|
||||
@ -489,16 +332,12 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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=
|
||||
@ -507,17 +346,13 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-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=
|
||||
@ -528,44 +363,29 @@ 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=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-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=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/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-20210603081109-ebe580a85c40/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 h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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=
|
||||
@ -584,7 +404,6 @@ 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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
@ -609,25 +428,14 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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=
|
||||
@ -645,29 +453,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
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.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=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@ -691,46 +482,12 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
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=
|
||||
@ -743,22 +500,6 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
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=
|
||||
@ -771,6 +512,7 @@ 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=
|
||||
@ -778,18 +520,18 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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.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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -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,12 +1,8 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -15,14 +11,11 @@ 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
|
||||
disableL10n bool
|
||||
cfg *config.Config
|
||||
)
|
||||
|
||||
@ -31,101 +24,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 {
|
||||
for _, h := range history {
|
||||
sort.Slice(h, func(i, j int) bool {
|
||||
return h[i].Code < h[j].Code
|
||||
})
|
||||
}
|
||||
|
||||
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, disableL10n)
|
||||
},
|
||||
}
|
||||
|
||||
@ -136,14 +51,98 @@ func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:f
|
||||
"generate index page",
|
||||
)
|
||||
|
||||
cmd.Flags().BoolVarP(
|
||||
&disableL10n,
|
||||
"disable-l10n", "",
|
||||
false,
|
||||
"disable error pages localization",
|
||||
)
|
||||
|
||||
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, disableL10n bool) error { //nolint:funlen,lll
|
||||
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,
|
||||
L10nDisabled: disableL10n,
|
||||
})
|
||||
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
|
||||
@ -155,52 +154,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)
|
||||
}
|
41
internal/cli/build/index.tpl.html
Normal file
41
internal/cli/build/index.tpl.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<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" />
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root {--bs-light:#212529;--bs-light-rgb:33,37,41;--bs-body-color:#eee}a{color:#91b4e8}a:hover{color:#a2bfec}
|
||||
}
|
||||
</style>
|
||||
</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,37 +25,31 @@ 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()
|
||||
return f.Validate()
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) },
|
||||
RunE: func(*cobra.Command, []string) error { return run(ctx, log, cfg, f) },
|
||||
}
|
||||
|
||||
f.init(cmd.Flags())
|
||||
f.Init(cmd.Flags())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// run current command.
|
||||
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
|
||||
func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
|
||||
oss = breaker.NewOSSignals(ctx) // OS signals listener
|
||||
@ -76,36 +68,13 @@ 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 interface{ Pick() string }
|
||||
|
||||
opt = f.ToOptions()
|
||||
)
|
||||
|
||||
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 {
|
||||
switch opt.Template.Name {
|
||||
case useRandomTemplate:
|
||||
log.Info("A random template will be used")
|
||||
|
||||
@ -116,35 +85,37 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
|
||||
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
|
||||
|
||||
case useRandomTemplateDaily:
|
||||
log.Info("A random template will be used and changed once a day")
|
||||
|
||||
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd
|
||||
|
||||
case useRandomTemplateHourly:
|
||||
log.Info("A random template will be used and changed hourly")
|
||||
|
||||
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour)
|
||||
|
||||
case "":
|
||||
log.Info("The first template (ordered by name) will be used")
|
||||
|
||||
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 t, found := cfg.Template(opt.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: " + opt.Template.Name)
|
||||
}
|
||||
|
||||
if !found {
|
||||
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, f.defaultHTTPCode)
|
||||
if err := server.Register(cfg, picker, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
|
||||
|
||||
@ -153,13 +124,16 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
defer close(errCh)
|
||||
|
||||
log.Info("Server starting",
|
||||
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.String("addr", f.Listen.IP),
|
||||
zap.Uint16("port", f.Listen.Port),
|
||||
zap.String("default error page", opt.Default.PageCode),
|
||||
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
|
||||
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
|
||||
zap.Bool("show request details", opt.ShowDetails),
|
||||
zap.Bool("localization disabled", opt.L10n.Disabled),
|
||||
)
|
||||
|
||||
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
|
||||
if err := server.Start(f.Listen.IP, f.Listen.Port); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(startingErrCh)
|
||||
@ -172,6 +146,12 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
case <-ctx.Done(): // ..or context cancellation
|
||||
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
|
||||
|
||||
if p, ok := picker.(interface{ Close() error }); ok {
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// stop the server using created context above
|
||||
if err := server.Stop(); err != nil {
|
||||
return err
|
||||
|
@ -3,23 +3,30 @@ package serve
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/options"
|
||||
)
|
||||
|
||||
type flags struct {
|
||||
listen struct {
|
||||
ip string
|
||||
port uint16
|
||||
Listen struct {
|
||||
IP string
|
||||
Port uint16
|
||||
}
|
||||
template struct {
|
||||
name string
|
||||
}
|
||||
l10n struct {
|
||||
disabled bool
|
||||
}
|
||||
defaultErrorPage string
|
||||
defaultHTTPCode uint16
|
||||
showDetails bool
|
||||
proxyHTTPHeaders string // comma-separated
|
||||
}
|
||||
|
||||
const (
|
||||
@ -28,33 +35,43 @@ const (
|
||||
templateNameFlagName = "template-name"
|
||||
defaultErrorPageFlagName = "default-error-page"
|
||||
defaultHTTPCodeFlagName = "default-http-code"
|
||||
showDetailsFlagName = "show-details"
|
||||
proxyHTTPHeadersFlagName = "proxy-headers"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
)
|
||||
|
||||
const (
|
||||
useRandomTemplate = "random"
|
||||
useRandomTemplateOnEachRequest = "i-said-random"
|
||||
useRandomTemplateDaily = "random-daily"
|
||||
useRandomTemplateHourly = "random-hourly"
|
||||
)
|
||||
|
||||
func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
func (f *flags) Init(flagSet *pflag.FlagSet) {
|
||||
flagSet.StringVarP(
|
||||
&f.listen.ip,
|
||||
&f.Listen.IP,
|
||||
listenFlagName, "l",
|
||||
"0.0.0.0",
|
||||
fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr),
|
||||
fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr),
|
||||
)
|
||||
flagSet.Uint16VarP(
|
||||
&f.listen.port,
|
||||
&f.Listen.Port,
|
||||
portFlagName, "p",
|
||||
8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
|
||||
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
|
||||
fmt.Sprintf("TCP prt number [$%s]", env.ListenPort),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.template.name,
|
||||
templateNameFlagName, "t",
|
||||
"",
|
||||
fmt.Sprintf(
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request) [$%s]", //nolint:lll
|
||||
useRandomTemplate, useRandomTemplateOnEachRequest, env.TemplateName,
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+
|
||||
"or \"%s/%s\" daily/hourly randomized) [$%s]",
|
||||
useRandomTemplate,
|
||||
useRandomTemplateOnEachRequest,
|
||||
useRandomTemplateDaily,
|
||||
useRandomTemplateHourly,
|
||||
env.TemplateName,
|
||||
),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
@ -69,22 +86,40 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
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),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.proxyHTTPHeaders,
|
||||
proxyHTTPHeadersFlagName, "",
|
||||
"",
|
||||
fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders),
|
||||
)
|
||||
flagSet.BoolVarP(
|
||||
&f.l10n.disabled,
|
||||
disableL10nFlagName, "",
|
||||
false,
|
||||
fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit
|
||||
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
|
||||
switch flag.Name {
|
||||
case listenFlagName:
|
||||
if envVar, exists := env.ListenAddr.Lookup(); exists {
|
||||
f.listen.ip = strings.TrimSpace(envVar)
|
||||
f.Listen.IP = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case portFlagName:
|
||||
if envVar, exists := env.ListenPort.Lookup(); exists {
|
||||
if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd
|
||||
f.listen.port = uint16(p)
|
||||
f.Listen.Port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
|
||||
}
|
||||
@ -108,6 +143,25 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nol
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
case proxyHTTPHeadersFlagName:
|
||||
if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists {
|
||||
f.proxyHTTPHeaders = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case disableL10nFlagName:
|
||||
if envVar, exists := env.DisableL10n.Lookup(); exists {
|
||||
if b, err := strconv.ParseBool(envVar); err == nil {
|
||||
f.l10n.disabled = b
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -115,14 +169,67 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nol
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (f *flags) validate() error {
|
||||
if net.ParseIP(f.listen.ip) == nil {
|
||||
return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip)
|
||||
func (f *flags) Validate() error {
|
||||
if net.ParseIP(f.Listen.IP) == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if strings.ContainsRune(f.proxyHTTPHeaders, ' ') {
|
||||
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without
|
||||
// duplicates).
|
||||
func (f *flags) headersToProxy() []string {
|
||||
var raw = strings.Split(f.proxyHTTPHeaders, ",")
|
||||
|
||||
if len(raw) == 0 {
|
||||
return []string{}
|
||||
} else if len(raw) == 1 {
|
||||
if h := strings.TrimSpace(raw[0]); h != "" {
|
||||
return []string{h}
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
var m = make(map[string]struct{}, len(raw))
|
||||
|
||||
// make unique and ignore empty strings
|
||||
for _, h := range raw {
|
||||
if h = strings.TrimSpace(h); h != "" {
|
||||
if _, ok := m[h]; !ok {
|
||||
m[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map into slice
|
||||
var headers = make([]string, 0, len(m))
|
||||
for h := range m {
|
||||
headers = append(headers, h)
|
||||
}
|
||||
|
||||
// make sort
|
||||
sort.Strings(headers)
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
func (f *flags) ToOptions() (o options.ErrorPage) {
|
||||
o.Default.PageCode = f.defaultErrorPage
|
||||
o.Default.HTTPCode = f.defaultHTTPCode
|
||||
o.L10n.Disabled = f.l10n.disabled
|
||||
o.Template.Name = f.template.name
|
||||
o.ShowDetails = f.showDetails
|
||||
o.ProxyHTTPHeaders = f.headersToProxy()
|
||||
|
||||
return o
|
||||
}
|
||||
|
@ -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:
|
||||
|
3
internal/env/env.go
vendored
3
internal/env/env.go
vendored
@ -12,6 +12,9 @@ const (
|
||||
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
|
||||
ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response)
|
||||
DisableL10n envVariable = "DISABLE_L10N" // disable pages localization
|
||||
)
|
||||
|
||||
// String returns environment variable name in the string representation.
|
||||
|
6
internal/env/env_test.go
vendored
6
internal/env/env_test.go
vendored
@ -14,6 +14,9 @@ func TestConstants(t *testing.T) {
|
||||
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))
|
||||
assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders))
|
||||
assert.Equal(t, "DISABLE_L10N", string(DisableL10n))
|
||||
}
|
||||
|
||||
func TestEnvVariable_Lookup(t *testing.T) {
|
||||
@ -26,6 +29,9 @@ func TestEnvVariable_Lookup(t *testing.T) {
|
||||
{giveEnv: ConfigFilePath},
|
||||
{giveEnv: DefaultErrorPage},
|
||||
{giveEnv: DefaultHTTPCode},
|
||||
{giveEnv: ShowDetails},
|
||||
{giveEnv: ProxyHTTPHeaders},
|
||||
{giveEnv: DisableL10n},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
|
@ -9,17 +9,32 @@ import (
|
||||
)
|
||||
|
||||
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
|
||||
const headersSeparator = ": "
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var (
|
||||
startedAt = time.Now()
|
||||
ua = string(ctx.UserAgent())
|
||||
)
|
||||
var ua = string(ctx.UserAgent())
|
||||
|
||||
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
|
||||
h(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var reqHeaders = make([]string, 0, 24) //nolint:gomnd
|
||||
|
||||
ctx.Request.Header.VisitAll(func(key, value []byte) {
|
||||
reqHeaders = append(reqHeaders, string(key)+headersSeparator+string(value))
|
||||
})
|
||||
|
||||
var startedAt = time.Now()
|
||||
|
||||
h(ctx)
|
||||
|
||||
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
|
||||
return
|
||||
}
|
||||
var respHeaders = make([]string, 0, 16) //nolint:gomnd
|
||||
|
||||
ctx.Response.Header.VisitAll(func(key, value []byte) {
|
||||
respHeaders = append(respHeaders, string(key)+headersSeparator+string(value))
|
||||
})
|
||||
|
||||
log.Info("HTTP request processed",
|
||||
zap.String("useragent", ua),
|
||||
@ -27,8 +42,27 @@ 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)),
|
||||
zap.Strings("request_headers", reqHeaders),
|
||||
zap.Strings("response_headers", respHeaders),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
126
internal/http/core/errorpage.go
Normal file
126
internal/http/core/errorpage.go
Normal file
@ -0,0 +1,126 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/options"
|
||||
"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,gocyclo
|
||||
ctx *fasthttp.RequestCtx,
|
||||
cfg *config.Config,
|
||||
p templatePicker,
|
||||
rdr renderer,
|
||||
pageCode string,
|
||||
httpCode int,
|
||||
opt options.ErrorPage,
|
||||
) {
|
||||
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: opt.ShowDetails,
|
||||
L10nDisabled: opt.L10n.Disabled,
|
||||
}
|
||||
)
|
||||
|
||||
if opt.ShowDetails {
|
||||
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
|
||||
}
|
||||
|
||||
// proxy required HTTP headers from the request to the response
|
||||
for _, headerToProxy := range opt.ProxyHTTPHeaders {
|
||||
if reqHeader := ctx.Request.Header.Peek(headerToProxy); len(reqHeader) > 0 {
|
||||
ctx.Response.Header.SetBytesV(headerToProxy, reqHeader)
|
||||
}
|
||||
}
|
||||
|
||||
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,34 @@
|
||||
package errorpage
|
||||
|
||||
import (
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/http/core"
|
||||
"github.com/tarampampam/error-pages/internal/options"
|
||||
"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, opt options.ErrorPage) 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, opt)
|
||||
} 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,41 +1,49 @@
|
||||
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/options"
|
||||
"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,
|
||||
defaultHTTPCode uint16,
|
||||
) fasthttp.RequestHandler {
|
||||
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
content, err := e.GetPage(p.Pick(), defaultPageCode)
|
||||
pageCode, httpCode := opt.Default.PageCode, int(opt.Default.HTTPCode)
|
||||
|
||||
if err == nil {
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(int(defaultHTTPCode))
|
||||
_, _ = 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, opt)
|
||||
}
|
||||
}
|
||||
|
||||
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,27 @@ 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/options"
|
||||
"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 +36,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 +44,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,33 +60,45 @@ 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,
|
||||
defaultHTTPCode uint16,
|
||||
) {
|
||||
s.router.GET("/", indexHandler.NewHandler(errorsPager, templatePicker, defaultPageCode, defaultHTTPCode))
|
||||
func (s *Server) Register(cfg *config.Config, templatePicker templatePicker, opt options.ErrorPage) 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, opt))
|
||||
s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, opt))
|
||||
|
||||
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")
|
||||
}
|
16
internal/options/errorpage.go
Normal file
16
internal/options/errorpage.go
Normal file
@ -0,0 +1,16 @@
|
||||
package options
|
||||
|
||||
type ErrorPage struct {
|
||||
Default struct {
|
||||
PageCode string // default error page code
|
||||
HTTPCode uint16 // default HTTP response code
|
||||
}
|
||||
L10n struct {
|
||||
Disabled bool // disable error pages localization
|
||||
}
|
||||
Template struct {
|
||||
Name string // template name
|
||||
}
|
||||
ShowDetails bool // show request details in response
|
||||
ProxyHTTPHeaders []string // proxy HTTP request headers list
|
||||
}
|
88
internal/pick/picker.go
Normal file
88
internal/pick/picker.go
Normal file
@ -0,0 +1,88 @@
|
||||
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 {
|
||||
return p.randomizeNext()
|
||||
}
|
||||
|
||||
return p.lastIdx
|
||||
|
||||
case RandomEveryTime:
|
||||
return p.randomizeNext()
|
||||
|
||||
default:
|
||||
panic("picker.NextIndex(): unsupported mode")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *picker) randomizeNext() uint32 {
|
||||
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
|
||||
}
|
||||
|
||||
if p.lastIdx == unsetIdx {
|
||||
p.lastIdx--
|
||||
}
|
||||
|
||||
return p.lastIdx
|
||||
}
|
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,135 @@
|
||||
package pick
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"errors"
|
||||
"sync"
|
||||
"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
|
||||
maxIdx := len(items) - 1
|
||||
|
||||
if mode != First {
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
if maxIdx < 0 {
|
||||
maxIdx = 0
|
||||
}
|
||||
|
||||
return &StringsSlice{
|
||||
items: items,
|
||||
mode: mode,
|
||||
lastUsedIdx: -1,
|
||||
rnd: rnd,
|
||||
}
|
||||
return &StringsSlice{s: items, p: NewPicker(uint32(maxIdx), 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]
|
||||
return s.s[s.p.NextIndex()]
|
||||
}
|
||||
|
||||
case RandomOnce:
|
||||
if s.lastUsedIdx == -1 {
|
||||
s.lastUsedIdx = s.rnd.Intn(len(s.items))
|
||||
type StringsSliceWithInterval struct {
|
||||
s []string
|
||||
p *picker
|
||||
d time.Duration
|
||||
|
||||
idxMu sync.RWMutex
|
||||
idx uint32
|
||||
|
||||
close chan struct{}
|
||||
closedMu sync.RWMutex
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewStringsSliceWithInterval creates new StringsSliceWithInterval.
|
||||
func NewStringsSliceWithInterval(items []string, mode pickMode, interval time.Duration) *StringsSliceWithInterval {
|
||||
maxIdx := len(items) - 1
|
||||
|
||||
if maxIdx < 0 {
|
||||
maxIdx = 0
|
||||
}
|
||||
|
||||
if interval <= time.Duration(0) {
|
||||
panic("NewStringsSliceWithInterval: wrong interval")
|
||||
}
|
||||
|
||||
s := &StringsSliceWithInterval{
|
||||
s: items,
|
||||
p: NewPicker(uint32(maxIdx), mode),
|
||||
d: interval,
|
||||
close: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
s.next()
|
||||
|
||||
go s.rotate()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) rotate() {
|
||||
defer close(s.close)
|
||||
|
||||
timer := time.NewTimer(s.d)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.close:
|
||||
return
|
||||
|
||||
case <-timer.C:
|
||||
s.next()
|
||||
timer.Reset(s.d)
|
||||
}
|
||||
|
||||
return s.items[s.lastUsedIdx]
|
||||
|
||||
case RandomEveryTime:
|
||||
return s.items[s.rnd.Intn(len(s.items))]
|
||||
|
||||
default:
|
||||
panic("pick: unsupported mode")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) next() {
|
||||
idx := s.p.NextIndex()
|
||||
|
||||
s.idxMu.Lock()
|
||||
s.idx = idx
|
||||
s.idxMu.Unlock()
|
||||
}
|
||||
|
||||
// Pick an element from the strings slice.
|
||||
func (s *StringsSliceWithInterval) Pick() string {
|
||||
if s.isClosed() {
|
||||
panic("StringsSliceWithInterval.Pick(): closed")
|
||||
}
|
||||
|
||||
if len(s.s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
s.idxMu.RLock()
|
||||
defer s.idxMu.RUnlock()
|
||||
|
||||
return s.s[s.idx]
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) isClosed() (closed bool) {
|
||||
s.closedMu.RLock()
|
||||
closed = s.closed
|
||||
s.closedMu.RUnlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *StringsSliceWithInterval) Close() error {
|
||||
if s.isClosed() {
|
||||
return errors.New("closed")
|
||||
}
|
||||
|
||||
s.closedMu.Lock()
|
||||
s.closed = true
|
||||
s.closedMu.Unlock()
|
||||
|
||||
s.close <- struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package pick_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -9,94 +8,123 @@ import (
|
||||
"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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewStringsSliceWithInterval_Pick(t *testing.T) {
|
||||
t.Run("first", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.First, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.First, time.Millisecond)
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
})
|
||||
|
||||
t.Run("random once", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomOnce, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
var (
|
||||
p = pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomOnce, time.Millisecond)
|
||||
picked = p.Pick()
|
||||
)
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
assert.Equal(t, picked, p.Pick())
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
})
|
||||
|
||||
t.Run("random every time", func(t *testing.T) {
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{}, pick.RandomEveryTime, time.Millisecond)
|
||||
assert.Equal(t, "", p.Pick())
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
var changed int
|
||||
|
||||
for i := uint8(0); i < 50; i++ {
|
||||
p := pick.NewStringsSliceWithInterval([]string{"foo", "bar", "baz"}, pick.RandomEveryTime, time.Millisecond) //nolint:lll
|
||||
|
||||
one, two := p.Pick(), p.Pick()
|
||||
assert.Equal(t, one, two)
|
||||
|
||||
<-time.After(time.Millisecond * 2)
|
||||
|
||||
three, four := p.Pick(), p.Pick()
|
||||
assert.Equal(t, three, four)
|
||||
|
||||
if one != three {
|
||||
changed++
|
||||
}
|
||||
|
||||
assert.NoError(t, p.Close())
|
||||
assert.Error(t, p.Close())
|
||||
assert.Panics(t, func() { p.Pick() })
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, changed, 25)
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
40
internal/tpl/properties.go
Normal file
40
internal/tpl/properties.go
Normal file
@ -0,0 +1,40 @@
|
||||
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"`
|
||||
L10nDisabled bool
|
||||
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)
|
||||
}
|
218
internal/tpl/render.go
Normal file
218
internal/tpl/render.go
Normal file
@ -0,0 +1,218 @@
|
||||
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 },
|
||||
"l10n_disabled": func() bool { return props.L10nDisabled },
|
||||
"l10n_enabled": func() bool { return !props.L10nDisabled },
|
||||
}
|
||||
|
||||
// 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++ { //nolint:gosimple
|
||||
result[i] = a[i]
|
||||
}
|
||||
|
||||
for i := 0; i < len(b); i++ {
|
||||
result[i+len(a)] = b[i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
130
internal/tpl/render_test.go
Normal file
130
internal/tpl/render_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
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 " ]}}`,
|
||||
},
|
||||
|
||||
"fn l10n_enabled": {
|
||||
giveContent: "{{ if l10n_enabled }}Y{{ else }}N{{ end }}",
|
||||
giveProps: tpl.Properties{L10nDisabled: true},
|
||||
wantContent: "N",
|
||||
},
|
||||
"fn l10n_disabled": {
|
||||
giveContent: "{{ if l10n_disabled }}Y{{ else }}N{{ end }}",
|
||||
giveProps: tpl.Properties{L10nDisabled: true},
|
||||
wantContent: "Y",
|
||||
},
|
||||
} {
|
||||
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"},
|
||||
)
|
||||
}
|
||||
}
|
11
l10n/.eslintrc.json
Normal file
11
l10n/.eslintrc.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
}
|
||||
}
|
727
l10n/l10n.js
Normal file
727
l10n/l10n.js
Normal file
@ -0,0 +1,727 @@
|
||||
Object.defineProperty(window, 'l10n', {
|
||||
value: new function () {
|
||||
// language codes list: <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>
|
||||
const data = { // all keys should be in english (it is default/main locale)
|
||||
'Error': {
|
||||
fr: 'Erreur',
|
||||
ru: 'Ошибка',
|
||||
uk: 'Помилка',
|
||||
pt: 'Erro',
|
||||
nl: 'Fout',
|
||||
de: 'Fehler',
|
||||
es: 'Error',
|
||||
},
|
||||
'Good luck': {
|
||||
fr: 'Bonne chance',
|
||||
ru: 'Удачи',
|
||||
uk: 'Успіхів',
|
||||
pt: 'Boa sorte',
|
||||
nl: 'Veel succes',
|
||||
de: 'Viel Glück',
|
||||
es: 'Buena Suerte',
|
||||
},
|
||||
'UH OH': {
|
||||
fr: 'Oups',
|
||||
ru: 'Ох',
|
||||
uk: 'Ох',
|
||||
pt: 'Ops',
|
||||
nl: 'Oeps',
|
||||
de: 'Hoppla',
|
||||
es: 'Oups',
|
||||
},
|
||||
'Request details': {
|
||||
fr: 'Détails de la requête',
|
||||
ru: 'Детали запроса',
|
||||
uk: 'Деталі запиту',
|
||||
pt: 'Detalhes da solicitação',
|
||||
nl: 'Details van verzoek',
|
||||
de: 'Details der Anfrage',
|
||||
es: 'Detalles de la petición',
|
||||
},
|
||||
'Double-check the URL': {
|
||||
fr: 'Vérifiez l’URL',
|
||||
ru: 'Дважды проверьте URL',
|
||||
uk: 'Двічі перевіряйте URL-адресу',
|
||||
pt: 'Verifique novamente a URL',
|
||||
nl: 'Controleer de URL',
|
||||
de: 'Überprüfen Sie die URL',
|
||||
es: 'Verifique la url',
|
||||
},
|
||||
'Alternatively, go back': {
|
||||
fr: 'Essayer de revenir en arrière',
|
||||
ru: 'Или можете вернуться назад',
|
||||
uk: 'Або ви можете повернутися',
|
||||
pt: "Como alternativa, tente voltar",
|
||||
nl: 'Of ga terug',
|
||||
de: 'Alternativ gehen Sie zurück',
|
||||
es: 'Como alternativa, vuelva atrás',
|
||||
},
|
||||
'Here\'s what might have happened': {
|
||||
fr: 'Voici ce qui aurait pu se passer',
|
||||
ru: 'Из-за чего это могло случиться',
|
||||
uk: 'Що це може статися',
|
||||
pt: 'Aqui está o que pode ter acontecido',
|
||||
nl: 'Wat er gebeurd kan zijn',
|
||||
de: 'Folgendes könnte passiert sein',
|
||||
es: 'Esto es lo que ha podido pasar',
|
||||
},
|
||||
'You may have mistyped the URL': {
|
||||
fr: 'Vous avez peut-être mal tapé l’URL',
|
||||
ru: 'Вы могли ошибиться в URL',
|
||||
uk: 'Ви можете зробити помилку в URL-адресі',
|
||||
pt: 'Você pode ter digitado incorretamente a URL',
|
||||
nl: 'De URL bevat een typefout',
|
||||
de: 'Möglicherweise haben Sie die URL falsch eingegeben',
|
||||
es: 'Igual ha escrito mal la URL',
|
||||
},
|
||||
'The site was moved': {
|
||||
fr: 'Le site a été déplacé',
|
||||
ru: 'Сайт был перемещён',
|
||||
uk: 'Сайт був переміщений',
|
||||
pt: 'O site foi movido',
|
||||
nl: 'De site is verplaatst',
|
||||
de: 'Die Seite wurde verschoben',
|
||||
es: 'El sitio se ha movido',
|
||||
},
|
||||
'It was never here': {
|
||||
fr: 'Il n’a jamais été ici',
|
||||
ru: 'Он никогда не был здесь',
|
||||
uk: 'Він ніколи не був тут',
|
||||
pt: 'Nunca esteve aqui',
|
||||
nl: 'Het was hier nooit',
|
||||
de: 'Es war nie hier',
|
||||
es: 'Nunca ha estado aquí',
|
||||
},
|
||||
'Bad Request': {
|
||||
fr: 'Mauvaise requête',
|
||||
ru: 'Некорректный запрос',
|
||||
uk: 'Неправильний запит',
|
||||
pt: 'Requisição inválida',
|
||||
nl: 'Foutieve anvraag',
|
||||
de: 'Fehlerhafte Anfrage',
|
||||
es: 'Petición inválida'
|
||||
},
|
||||
'The server did not understand the request': {
|
||||
fr: 'Le serveur ne comprend pas la requête',
|
||||
ru: 'Сервер не смог обработать запрос из-за ошибки в нём',
|
||||
uk: 'Сервер не міг обробити запит через помилку в ньому',
|
||||
pt: 'O servidor não entendeu a solicitação',
|
||||
nl: 'De server begreep het verzoek niet',
|
||||
de: 'Der Server hat die Anfrage nicht verstanden',
|
||||
es: 'El servidor no entendió la petición'
|
||||
},
|
||||
'Unauthorized': {
|
||||
fr: 'Non autorisé',
|
||||
ru: 'Запрос не авторизован',
|
||||
uk: 'Несанкціонований доступ',
|
||||
pt: 'Não autorizado',
|
||||
nl: 'Niet geautoriseerd',
|
||||
de: 'Nicht autorisiert',
|
||||
es: 'No autorizado',
|
||||
},
|
||||
'The requested page needs a username and a password': {
|
||||
fr: 'La page demandée nécessite un nom d’utilisateur et un mot de passe',
|
||||
ru: 'Для доступа к странице требуется логин и пароль',
|
||||
uk: 'Щоб отримати доступ до сторінки, потрібний логін та пароль',
|
||||
pt: 'A página solicitada precisa de um nome de usuário e uma senha',
|
||||
nl: 'De pagina heeft een gebruikersnaam en wachtwoord nodig',
|
||||
de: 'Die angeforderte Seite benötigt einen Benutzernamen und ein Passwort',
|
||||
es: 'La página solicitada necesita un usuario y una contraseña',
|
||||
},
|
||||
'Forbidden': {
|
||||
fr: 'Interdit',
|
||||
ru: 'Запрещено',
|
||||
uk: 'Заборонено',
|
||||
pt: 'Proibido',
|
||||
nl: 'Verboden',
|
||||
de: 'Verboten',
|
||||
es: 'Prohibido',
|
||||
},
|
||||
'Access is forbidden to the requested page': {
|
||||
fr: 'Accès interdit à la page demandée',
|
||||
ru: 'Доступ к странице запрещён',
|
||||
uk: 'Доступ до сторінки заборонено',
|
||||
pt: 'É proibido o acesso à página solicitada',
|
||||
nl: 'Toegang tot de pagina is verboden',
|
||||
de: 'Der Zugriff auf die angeforderte Seite ist verboten',
|
||||
es: 'El acceso está prohibido para la página solicitada',
|
||||
},
|
||||
'Not Found': {
|
||||
fr: 'Introuvable',
|
||||
ru: 'Страница не найдена',
|
||||
uk: 'Сторінка не знайдена',
|
||||
pt: 'Não encontrado',
|
||||
nl: 'Niet gevonden',
|
||||
de: 'Nicht gefunden',
|
||||
es: 'No encontrado',
|
||||
},
|
||||
'The server can not find the requested page': {
|
||||
fr: 'Le serveur ne peut trouver la page demandée',
|
||||
ru: 'Сервер не смог найти запрашиваемую страницу',
|
||||
uk: 'Сервер не міг знайти запитану сторінку',
|
||||
pt: 'O servidor não consegue encontrar a página solicitada',
|
||||
nl: 'De server kan de pagina niet vinden',
|
||||
de: 'Der Server kann die angeforderte Seite nicht finden',
|
||||
es: 'El servidor no puede encontrar la página solicitada',
|
||||
},
|
||||
'Method Not Allowed': {
|
||||
fr: 'Méthode Non Autorisée',
|
||||
ru: 'Метод не поддерживается',
|
||||
uk: 'Неприпустимий метод',
|
||||
pt: 'Método não permitido',
|
||||
nl: 'Methode niet toegestaan',
|
||||
de: 'Methode nicht erlaubt',
|
||||
es: 'Método no permitido',
|
||||
},
|
||||
'The method specified in the request is not allowed': {
|
||||
fr: 'La méthode spécifiée dans la requête n’est pas autorisée',
|
||||
ru: 'Указанный в запросе метод не поддерживается',
|
||||
uk: 'Метод, зазначений у запиті, не підтримується',
|
||||
pt: 'O método especificado na solicitação não é permitido',
|
||||
nl: 'De methode in het verzoek is niet toegestaan',
|
||||
de: 'Die in der Anfrage angegebene Methode ist nicht zulässig',
|
||||
es: 'El método especificado en la petición no está permitido'
|
||||
},
|
||||
'Proxy Authentication Required': {
|
||||
fr: 'Authentification proxy requise',
|
||||
ru: 'Нужна аутентификация прокси',
|
||||
uk: 'Потрібна ідентифікація проксі',
|
||||
pt: 'Autenticação de proxy necessária',
|
||||
nl: 'Authenticatie op de proxyserver verplicht',
|
||||
de: 'Proxy-Authentifizierung benötigt',
|
||||
es: 'Autenticación de proxy requerida',
|
||||
},
|
||||
'You must authenticate with a proxy server before this request can be served': {
|
||||
fr: 'Vous devez vous authentifier avec un serveur proxy avant que cette requête puisse être servie',
|
||||
ru: 'Вы должны быть авторизованы на прокси сервере для обработки этого запроса',
|
||||
uk: 'Ви повинні увійти до проксі-сервера для обробки цього запиту',
|
||||
pt: 'Você deve se autenticar com um servidor proxy antes que esta solicitação possa ser atendida',
|
||||
nl: 'Je moet authenticeren bij een proxyserver voordat dit verzoek uitgevoerd kan worden',
|
||||
de: 'Sie müssen sich bei einem Proxy-Server authentifizieren, bevor diese Anfrage bedient werden kann',
|
||||
es: 'Debes autentificarte con un servidor proxy antes de que esta petición pueda ser atendida',
|
||||
},
|
||||
'Request Timeout': {
|
||||
fr: 'Requête expiré',
|
||||
ru: 'Истекло время ожидания',
|
||||
uk: 'Час запиту закінчився',
|
||||
pt: 'Tempo limite de solicitação excedido',
|
||||
nl: 'Aanvraagtijd verstreken',
|
||||
de: 'Zeitüberschreitung der Anforderung',
|
||||
es: 'Tiempo límite de la petición excedido',
|
||||
},
|
||||
'The request took longer than the server was prepared to wait': {
|
||||
fr: 'La requête prend plus de temps que prévu',
|
||||
ru: 'Отправка запроса заняла слишком много времени',
|
||||
uk: 'Надсилання запиту зайняв занадто багато часу',
|
||||
pt: 'A solicitação demorou mais do que o servidor estava preparado para esperar',
|
||||
nl: 'Het verzoek duurde langer dan de server wilde wachten',
|
||||
de: 'Die Anfrage hat länger gedauert, als der Server bereit war zu warten',
|
||||
es: 'La petición esta tardando más de lo que el servidor estaba preparado para esperar',
|
||||
},
|
||||
'Conflict': {
|
||||
fr: 'Conflit',
|
||||
ru: 'Конфликт',
|
||||
uk: 'Конфлікт',
|
||||
pt: 'Conflito',
|
||||
nl: 'Conflict',
|
||||
de: 'Konflikt',
|
||||
es: 'Conflicto',
|
||||
},
|
||||
'The request could not be completed because of a conflict': {
|
||||
fr: 'La requête n’a pas pu être complétée à cause d’un conflit',
|
||||
ru: 'Запрос не может быть обработан из-за конфликта',
|
||||
uk: 'Запит не може бути оброблений через конфлікт',
|
||||
pt: 'A solicitação não pôde ser concluída devido a um conflito',
|
||||
nl: 'Het verzoek kon niet worden verwerkt vanwege een conflict',
|
||||
de: 'Die Anfrage konnte aufgrund eines Konflikts nicht abgeschlossen werden',
|
||||
es: 'La petición no ha podido ser completada por un conflicto',
|
||||
},
|
||||
'Gone': {
|
||||
fr: 'Supprimé',
|
||||
ru: 'Удалено',
|
||||
uk: 'Вилучений',
|
||||
pt: 'Removido',
|
||||
nl: 'Verdwenen',
|
||||
de: 'Verschwunden',
|
||||
es: 'Eliminado',
|
||||
},
|
||||
'The requested page is no longer available': {
|
||||
fr: 'La page demandée n’est plus disponible',
|
||||
ru: 'Запрошенная страница была удалена',
|
||||
uk: 'Запитана сторінка була видалена',
|
||||
pt: 'A página solicitada não está mais disponível',
|
||||
nl: 'De pagina is niet langer beschikbaar',
|
||||
de: 'Die angeforderte Seite ist nicht mehr verfügbar',
|
||||
es: 'La página solicitada no está ya disponible',
|
||||
},
|
||||
'Length Required': {
|
||||
fr: 'Longueur requise',
|
||||
ru: 'Необходима длина',
|
||||
uk: 'Потрібно вказати розмір',
|
||||
pt: 'Content-Length necessário',
|
||||
nl: 'Lengte benodigd',
|
||||
de: 'Länge benötigt',
|
||||
es: 'Longitud requerida',
|
||||
},
|
||||
'The "Content-Length" is not defined. The server will not accept the request without it': {
|
||||
fr: 'Le "Content-Length" n’est pas défini. Le serveur ne prendra pas en compte la requête',
|
||||
ru: 'Заголовок "Content-Length" не был передан. Сервер не может обработать запрос без него',
|
||||
uk: 'Заголовок "Content-Length" не був переданий. Сервер не може обробити запит без нього',
|
||||
pt: 'O "Content-Length" não está definido. O servidor não aceitará a solicitação sem ele',
|
||||
nl: 'De "Content-Length" is niet gespecificeerd. De server accepteert het verzoek niet zonder',
|
||||
de: 'Die "Content-Length" ist nicht definiert. Ohne sie akzeptiert der Server die Anfrage nicht',
|
||||
es: 'El "Content-Legth" no eta definido. Este servidor no aceptará la petición sin él',
|
||||
},
|
||||
'Precondition Failed': {
|
||||
fr: 'Échec de la condition préalable',
|
||||
ru: 'Условие ложно',
|
||||
uk: 'Збій під час обробки попередньої умови',
|
||||
pt: 'Falha na pré-condição',
|
||||
nl: 'Niet voldaan aan vooraf gestelde voorwaarde',
|
||||
de: 'Vorbedingung fehlgeschlagen',
|
||||
es: 'Precondición fallida',
|
||||
},
|
||||
'The pre condition given in the request evaluated to false by the server': {
|
||||
fr: 'La précondition donnée dans la requête a été évaluée comme étant fausse par le serveur',
|
||||
ru: 'Ни одно из условных полей заголовка запроса не было выполнено',
|
||||
uk: 'Жодна з умовних полів заголовка запиту не була виконана',
|
||||
pt: 'A pré-condição dada na solicitação avaliada como falsa pelo servidor',
|
||||
nl: 'De vooraf gestelde voorwaarde is afgewezen door de server',
|
||||
de: 'Die in der Anfrage angegebene Vorbedingung wird vom Server als falsch bewertet',
|
||||
es: 'La precondición ha sido evaluada como negativa para esta petición por el servidor'
|
||||
},
|
||||
'Payload Too Large': {
|
||||
fr: 'Charge trop volumineuse',
|
||||
ru: 'Слишком большой запрос',
|
||||
uk: 'Занадто великий запит',
|
||||
pt: 'Payload muito grande',
|
||||
nl: 'Aanvraag te grood',
|
||||
de: 'Anfrage zu groß',
|
||||
es: 'Carga muy grande',
|
||||
},
|
||||
'The server will not accept the request, because the request entity is too large': {
|
||||
fr: 'Le serveur ne prendra pas en compte la requête, car l’entité de la requête est trop volumineuse',
|
||||
ru: 'Сервер не может обработать запрос, так как он слишком большой',
|
||||
uk: 'Сервер не може обробити запит, оскільки він занадто великий',
|
||||
pt: 'O servidor não aceitará a solicitação porque a entidade da solicitação é muito grande',
|
||||
nl: 'De server accepteert het verzoek niet omdat de aanvraag te groot is',
|
||||
de: 'Der Server akzeptiert die Anfrage nicht, da die Datenmenge zu groß ist',
|
||||
es: 'El servidor no aceptará esta petición, porque la carga es demasiado grande',
|
||||
},
|
||||
'Requested Range Not Satisfiable': {
|
||||
fr: 'Requête non satisfaisante',
|
||||
ru: 'Диапазон не достижим',
|
||||
uk: 'Запитуваний діапазон недосяжний',
|
||||
pt: 'Intervalo Solicitado Não Satisfatório',
|
||||
nl: 'Aangevraagd gedeelte niet opvraagbaar',
|
||||
de: 'Anfrage-Bereich nicht erfüllbar',
|
||||
es: 'Intervalo solicitado no satisfactorio',
|
||||
},
|
||||
'The requested byte range is not available and is out of bounds': {
|
||||
fr: 'Le byte range demandé n’est pas disponible et est hors des limites',
|
||||
ru: 'Запрошенный диапазон данных недоступен или вне допустимых пределов',
|
||||
uk: 'Описаний діапазон даних недоступний або з допустимих меж',
|
||||
pt: 'O intervalo de bytes solicitado não está disponível e está fora dos limites',
|
||||
nl: 'De aangevraagde bytes zijn buiten het limiet',
|
||||
de: 'Der angefragte Teilbereich der Ressource existiert nicht oder ist ungültig',
|
||||
es: 'El intervalo de bytes requerido no está disponible o se encuentra fuera de los límites',
|
||||
},
|
||||
'I\'m a teapot': {
|
||||
fr: 'Je suis une théière',
|
||||
ru: 'Я чайник',
|
||||
uk: 'Я чайник',
|
||||
pt: 'Eu sou um bule',
|
||||
nl: 'Ik ben een theepot',
|
||||
de: 'Ich bin eine Teekanne',
|
||||
es: 'Soy una tetera'
|
||||
},
|
||||
'Attempt to brew coffee with a teapot is not supported': {
|
||||
fr: 'Tenter de préparer du café avec une théière n’est pas pris en charge',
|
||||
ru: 'Попытка заварить кофе в чайнике обречена на фиаско',
|
||||
uk: 'Спроба виварити каву в чайник приречена на фіаско',
|
||||
pt: 'A tentativa de preparar café com um bule não é suportada',
|
||||
nl: 'Koffie maken met een theepot is niet ondersteund',
|
||||
de: 'Der Versuch, Kaffee mit einer Teekanne zuzubereiten, wird nicht unterstützt',
|
||||
es: 'Intentar hacer un café en una tetera no está soportado',
|
||||
},
|
||||
'Too Many Requests': {
|
||||
fr: 'Trop de requêtes',
|
||||
ru: 'Слишком много запросов',
|
||||
uk: 'Занадто багато запитів',
|
||||
pt: 'Excesso de solicitações',
|
||||
nl: 'Te veel requests',
|
||||
de: 'Zu viele Anfragen',
|
||||
es: 'Demasiadas peticiones'
|
||||
},
|
||||
'Too many requests in a given amount of time': {
|
||||
fr: 'Trop de requêtes dans un délai donné',
|
||||
ru: 'Отправлено слишком много запросов за короткое время',
|
||||
uk: 'Надіслано занадто багато запитів на короткий час',
|
||||
pt: 'Excesso de solicitações em um determinado período de tempo',
|
||||
nl: 'Te veel verzoeken binnen een bepaalde tijd',
|
||||
de: 'Der Client hat zu viele Anfragen in einem bestimmten Zeitraum gesendet',
|
||||
es: 'Demasiadas peticiones en un determinado periodo de tiempo',
|
||||
},
|
||||
'Internal Server Error': {
|
||||
fr: 'Erreur interne du serveur',
|
||||
ru: 'Внутренняя ошибка сервера',
|
||||
uk: 'Внутрішня помилка сервера',
|
||||
pt: 'Erro do Servidor Interno',
|
||||
nl: 'Interne serverfout',
|
||||
de: 'Interner Server-Fehler',
|
||||
es: 'Error Interno',
|
||||
},
|
||||
'The server met an unexpected condition': {
|
||||
fr: 'Le serveur a rencontré une condition inattendue',
|
||||
ru: 'Произошло что-то неожиданное на сервере',
|
||||
uk: 'На сервері було щось несподіване',
|
||||
pt: 'O servidor encontrou uma condição inesperada',
|
||||
nl: 'De server ondervond een onverwachte conditie',
|
||||
de: 'Der Server hat einen internen Fehler festgestellt',
|
||||
es: 'El servidor ha encontrado una condición no esperada',
|
||||
},
|
||||
'Bad Gateway': {
|
||||
fr: 'Mauvaise passerelle',
|
||||
ru: 'Ошибка шлюза',
|
||||
uk: 'Помилка шлюзу',
|
||||
pt: 'Gateway inválido',
|
||||
nl: 'Ongeldige Gateway',
|
||||
de: 'Fehlerhafter Gateway',
|
||||
es: 'Puerta de enlace no valida',
|
||||
},
|
||||
'The server received an invalid response from the upstream server': {
|
||||
fr: 'Le serveur a reçu une réponse invalide du serveur distant',
|
||||
ru: 'Сервер получил некорректный ответ от вышестоящего сервера',
|
||||
uk: 'Сервер отримав неправильну відповідь з сервера Upstream',
|
||||
pt: 'O servidor recebeu uma resposta inválida do servidor upstream',
|
||||
nl: 'De server ontving een ongeldig antwoord van een bovenliggende server',
|
||||
de: 'Der Server hat eine ungültige Antwort vom Upstream-Server erhalten',
|
||||
es: 'El servidor ha recibido una respuesta no válida del servidor de origen'
|
||||
},
|
||||
'Service Unavailable': {
|
||||
fr: 'Service indisponible',
|
||||
ru: 'Сервис недоступен',
|
||||
uk: 'Сервіс недоступний',
|
||||
pt: 'Serviço não disponível',
|
||||
nl: 'Dienst niet beschikbaar',
|
||||
de: 'Dienst nicht verfügbar',
|
||||
es: 'Servicio no disponible',
|
||||
},
|
||||
'The server is temporarily overloading or down': {
|
||||
fr: 'Le serveur est temporairement en surcharge ou indisponible',
|
||||
ru: 'Сервер временно не может обрабатывать запросы по техническим причинам',
|
||||
uk: 'Сервер тимчасово не може обробляти запити з технічних причин',
|
||||
pt: 'O servidor está temporariamente sobrecarregado ou inativo',
|
||||
nl: 'De server is tijdelijk overbelast of niet bereikbaar',
|
||||
de: 'Der Server ist vorübergehend überlastet oder ausgefallen',
|
||||
es: 'El servidor está temporalmente sobrecargado o inactivo',
|
||||
},
|
||||
'Gateway Timeout': {
|
||||
fr: 'Expiration Passerelle',
|
||||
ru: 'Шлюз не отвечает',
|
||||
uk: 'Шлюз не відповідає',
|
||||
pt: 'Tempo limite do gateway excedido',
|
||||
nl: 'Gateway Verlopen',
|
||||
de: 'Gateway Zeitüberschreitung',
|
||||
es: 'Tiempo límite de puerta de enlace excedido',
|
||||
},
|
||||
'The gateway has timed out': {
|
||||
fr: 'Le temps d’attente de la passerelle est dépassé',
|
||||
ru: 'Сервер не дождался ответа от вышестоящего сервера',
|
||||
uk: 'Сервер не чекав відповіді від сервера Upstream',
|
||||
pt: 'O gateway esgotou o tempo limite',
|
||||
nl: 'De verbinding naar de bovenliggende server is verlopen',
|
||||
de: 'Das Zeitlimit für den Verbindungsaufbau mit dem Upstream-Server ist abgelaufen',
|
||||
es: 'La puerta de enlace ha sobrepasado el tiempo límite',
|
||||
},
|
||||
'HTTP Version Not Supported': {
|
||||
fr: 'Version HTTP non prise en charge',
|
||||
ru: 'Версия HTTP не поддерживается',
|
||||
uk: 'Версія НТТР не підтримується',
|
||||
pt: 'Versão HTTP não suportada',
|
||||
nl: 'HTTP-versie wordt niet ondersteunt',
|
||||
de: 'HTTP-Version wird nicht unterstützt',
|
||||
es: 'Versión de HTTP no soportada',
|
||||
},
|
||||
'The server does not support the "http protocol" version': {
|
||||
fr: 'Le serveur ne supporte pas la version du protocole HTTP',
|
||||
ru: 'Сервер не поддерживает запрошенную версию HTTP протокола',
|
||||
uk: 'Сервер не підтримує запитану версію HTTP-протоколу',
|
||||
pt: 'O servidor não suporta a versão do protocolo HTTP',
|
||||
nl: 'De server ondersteunt deze HTTP-versie niet',
|
||||
de: 'Der Server unterstützt die HTTP-Protokoll-Version nicht',
|
||||
es: 'El servidor no soporta la versión del protocolo HTTP',
|
||||
},
|
||||
|
||||
'Host': {
|
||||
fr: 'Hôte',
|
||||
ru: 'Хост',
|
||||
uk: 'Хост',
|
||||
pt: 'Hospedeiro',
|
||||
nl: 'Host',
|
||||
de: 'Host',
|
||||
es: 'Host',
|
||||
},
|
||||
'Original URI': {
|
||||
fr: 'URI d’origine',
|
||||
ru: 'Исходный URI',
|
||||
uk: 'Вихідний URI',
|
||||
pt: 'URI original',
|
||||
nl: 'Originele URI',
|
||||
de: 'Originale URI',
|
||||
es: 'URI original',
|
||||
},
|
||||
'Forwarded for': {
|
||||
fr: 'Transmis pour',
|
||||
ru: 'Перенаправлен',
|
||||
uk: 'Перенаправлений',
|
||||
pt: 'Encaminhado para',
|
||||
nl: 'Doorgestuurd voor',
|
||||
de: 'Weitergeleitet für',
|
||||
es: 'Remitido por',
|
||||
},
|
||||
'Namespace': {
|
||||
fr: 'Espace de noms',
|
||||
ru: 'Пространство имён',
|
||||
uk: 'Простір імен',
|
||||
pt: 'Namespace',
|
||||
nl: 'Elementnaam',
|
||||
de: 'Namensraum',
|
||||
es: 'Namespace',
|
||||
},
|
||||
'Ingress name': {
|
||||
fr: 'Nom ingress',
|
||||
ru: 'Имя Ingress',
|
||||
uk: 'Ім\'я Ingress',
|
||||
pt: 'Nome Ingress',
|
||||
nl: 'Ingress naam',
|
||||
de: 'Ingress Name',
|
||||
es: 'Nombre Ingress',
|
||||
},
|
||||
'Service name': {
|
||||
fr: 'Nom du service',
|
||||
ru: 'Имя сервиса',
|
||||
uk: 'Ім\'я сервісу',
|
||||
pt: 'Nome do Serviço',
|
||||
nl: 'Service naam',
|
||||
de: 'Service Name',
|
||||
es: 'Nombre del servicio'
|
||||
},
|
||||
'Service port': {
|
||||
fr: 'Port du service',
|
||||
ru: 'Порт сервиса',
|
||||
uk: 'Порт сервісу',
|
||||
pt: 'Porta do serviço',
|
||||
nl: 'Service poort',
|
||||
de: 'Service Port',
|
||||
es: 'Puerto del servicio',
|
||||
},
|
||||
'Request ID': {
|
||||
fr: 'Identifiant de la requête',
|
||||
ru: 'ID запроса',
|
||||
uk: 'ID запиту',
|
||||
pt: 'ID da solicitação',
|
||||
nl: 'ID van het verzoek',
|
||||
de: 'Anfrage ID',
|
||||
es: 'ID de la petición',
|
||||
},
|
||||
'Timestamp': {
|
||||
fr: 'Horodatage',
|
||||
ru: 'Временная метка',
|
||||
uk: 'Тимчасова мітка',
|
||||
pt: 'Timestamp',
|
||||
nl: 'Tijdstempel',
|
||||
de: 'Zeitstempel',
|
||||
es: 'Timestamp',
|
||||
},
|
||||
|
||||
'client-side error': {
|
||||
fr: 'Erreur Client',
|
||||
ru: 'ошибка на стороне клиента',
|
||||
uk: 'помилка на стороні клієнта',
|
||||
pt: 'erro do lado do cliente',
|
||||
nl: 'fout aan de gebruikerskant',
|
||||
de: 'Clientseitiger Fehler',
|
||||
es: 'Error del lado del cliente',
|
||||
},
|
||||
'server-side error': {
|
||||
fr: 'Erreur Serveur',
|
||||
ru: 'ошибка на стороне сервера',
|
||||
uk: 'помилка на стороні сервера',
|
||||
pt: 'erro do lado do servidor',
|
||||
nl: 'fout aan de serverkant',
|
||||
de: 'Serverseitiger Fehler',
|
||||
es: 'Error del lado del servidor',
|
||||
},
|
||||
|
||||
'Your Client': {
|
||||
fr: 'Votre Client',
|
||||
ru: 'Ваш Браузер',
|
||||
uk: 'Ваш Браузер',
|
||||
pt: 'Seu Cliente',
|
||||
nl: 'Jouw Client',
|
||||
de: 'Ihr Client',
|
||||
es: 'Tu cliente',
|
||||
},
|
||||
'Network': {
|
||||
fr: 'Réseau',
|
||||
ru: 'Сеть',
|
||||
uk: 'Сіть',
|
||||
pt: 'Rede',
|
||||
nl: 'Netwerk',
|
||||
de: 'Netzwerk',
|
||||
es: 'Red',
|
||||
},
|
||||
'Web Server': {
|
||||
fr: 'Serveur Web',
|
||||
ru: 'Web Сервер',
|
||||
uk: 'Web Сервер',
|
||||
pt: 'Servidor web',
|
||||
nl: 'Web Server',
|
||||
de: 'Webserver',
|
||||
es: 'Servidor web',
|
||||
},
|
||||
'What happened?': {
|
||||
fr: 'Que s’est-il passé ?',
|
||||
ru: 'Что произошло?',
|
||||
uk: 'Що сталося?',
|
||||
pt: 'O que aconteceu?',
|
||||
nl: 'Wat is er gebeurd?',
|
||||
de: 'Was ist passiert?',
|
||||
es: '¿Que ha pasado?',
|
||||
},
|
||||
'What can i do?': {
|
||||
fr: 'Que puis-je faire ?',
|
||||
ru: 'Что можно сделать?',
|
||||
uk: 'Що можна зробити?',
|
||||
pt: 'O que eu posso fazer?',
|
||||
nl: 'Wat kan ik doen?',
|
||||
de: 'Was kann ich machen?',
|
||||
es: '¿Que puedo hacer?',
|
||||
},
|
||||
'Please try again in a few minutes': {
|
||||
fr: 'Veuillez réessayer dans quelques minutes',
|
||||
ru: 'Пожалуйста, попробуйте повторить запрос ещё раз чуть позже',
|
||||
uk: 'Будь ласка, спробуйте повторити запит ще раз трохи пізніше',
|
||||
pt: 'Por favor, tente novamente em alguns minutos',
|
||||
nl: 'Probeer het alstublieft opnieuw over een paar minuten',
|
||||
de: 'Bitte versuchen Sie es in ein paar Minuten erneut',
|
||||
es: 'Por favor, intente nuevamente en unos minutos',
|
||||
},
|
||||
'Working': {
|
||||
fr: 'Opérationnel',
|
||||
ru: 'Работает',
|
||||
uk: 'Працює',
|
||||
pt: 'Funcionando',
|
||||
nl: 'Functioneel',
|
||||
de: 'Funktioniert',
|
||||
es: 'Trabajando',
|
||||
},
|
||||
'Unknown': {
|
||||
fr: 'Inconnu',
|
||||
ru: 'Неизвестно',
|
||||
uk: 'Невідомо',
|
||||
pt: 'Desconhecido',
|
||||
nl: 'Onbekend',
|
||||
de: 'Unbekannt',
|
||||
es: 'Desconocido',
|
||||
},
|
||||
'Please try to change the request method, headers, payload, or URL': {
|
||||
fr: 'Veuillez essayer de changer la méthode de requête, les en-têtes, le contenu ou l’URL',
|
||||
ru: 'Пожалуйста, попробуйте изменить метод запроса, заголовки, его содержимое или URL',
|
||||
uk: 'Будь ласка, спробуйте змінити метод запиту, заголовки, його вміст або URL-адресу',
|
||||
pt: 'Tente alterar o método de solicitação, cabeçalhos, payload ou URL',
|
||||
nl: 'Probeer het opnieuw met een andere methode, headers, payload of URL',
|
||||
de: 'Bitte versuchen Sie, die Anfragemethode, Header, Payload oder URL zu ändern',
|
||||
es: 'Por favor intente cambiar el método de la petición, cabeceras, carga o URL'
|
||||
},
|
||||
'Please check your authorization data': {
|
||||
fr: 'Veuillez vérifier vos données d’autorisation',
|
||||
ru: 'Пожалуйста, проверьте данные авторизации',
|
||||
uk: 'Будь ласка, перевірте дані авторизації',
|
||||
pt: 'Verifique seus dados de autorização',
|
||||
nl: 'Controleer de authenticatiegegevens',
|
||||
de: 'Bitte überprüfen Sie Ihre Zugangsdaten',
|
||||
es: 'Verifique sus datos de autorización',
|
||||
},
|
||||
'Please double-check the URL and try again': {
|
||||
fr: 'Veuillez vérifier l’URL et réessayer',
|
||||
ru: 'Пожалуйста, дважды проверьте URL и попробуйте снова',
|
||||
uk: 'Будь ласка, двічі перевірте URL-адресу і спробуйте знову',
|
||||
pt: 'Verifique novamente o URL e tente novamente',
|
||||
nl: 'Controleer de URL en probeer het opnieuw',
|
||||
de: 'Bitte überprüfen Sie die URL und versuchen Sie es erneut',
|
||||
es: 'Verifique de nuevo la URL y vuelva a probar',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
* @return {string}
|
||||
*/
|
||||
const serializeToken = function (token) {
|
||||
return token.toLowerCase().replaceAll(/[^a-z0-9]/g, '');
|
||||
};
|
||||
|
||||
// normalize the data keys
|
||||
for (const key in data) {
|
||||
Object.defineProperty(data, serializeToken(key), Object.getOwnPropertyDescriptor(data, key));
|
||||
delete data[key];
|
||||
}
|
||||
|
||||
// detect browser locale (take only 2 first symbols)
|
||||
let activeLocale = navigator.language.substring(0, 2).toLowerCase();
|
||||
|
||||
/**
|
||||
* @param {string} locale
|
||||
*/
|
||||
this.setLocale = function (locale) {
|
||||
activeLocale = locale.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
* @param {string|undefined?} def
|
||||
*/
|
||||
this.translate = function (token, def) {
|
||||
const t = serializeToken(token);
|
||||
|
||||
if (activeLocale === 'en' && Object.prototype.hasOwnProperty.call(data, t)) {
|
||||
return token
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, t) && Object.prototype.hasOwnProperty.call(data[t], activeLocale)) {
|
||||
return data[t][activeLocale];
|
||||
}
|
||||
|
||||
return def;
|
||||
};
|
||||
|
||||
/**
|
||||
* Localize all elements with HTML attribute `data-l10n`.
|
||||
*/
|
||||
this.localizeDocument = function () {
|
||||
const dataAttributeName = 'data-l10n';
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[' + dataAttributeName + ']'), ($el) => {
|
||||
const attr = $el.getAttribute(dataAttributeName).trim(),
|
||||
token = attr.length > 0 ? attr : $el.innerText.trim(),
|
||||
localized = this.translate(token, undefined);
|
||||
|
||||
if (attr.length === 0) {
|
||||
$el.setAttribute(dataAttributeName, token);
|
||||
}
|
||||
|
||||
if (localized !== undefined) {
|
||||
$el.innerText = localized;
|
||||
} else {
|
||||
console.debug(`Unsupported l10n token detected: "${token}" (locale "${activeLocale}")`, $el);
|
||||
}
|
||||
});
|
||||
};
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
window.l10n.localizeDocument();
|
20
l10n/readme.md
Normal file
20
l10n/readme.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 🔤 Localization
|
||||
|
||||
[](https://www.jsdelivr.com/package/gh/tarampampam/error-pages)
|
||||
|
||||
This directory contains file [l10n.js](l10n.js) for the error pages localization. The working logic is very simple - pages load this script using [jsdelivr.com](https://www.jsdelivr.com/) as a CDN for [versioned content from the GitHub repository](https://www.jsdelivr.com/features#gh), and it translates tag content with the special HTML attribute `data-l10n`.
|
||||
|
||||
By default, pages markup contains strings in English (`en` locale). If you want to localize the error pages on the different locales, you should:
|
||||
|
||||
- Find your locale name on [this page](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (column `639-1`)
|
||||
- Make a fork of this repository
|
||||
- Edit file [l10n.js](l10n.js) in `data` section (append new localized strings) using locale name from the step 1
|
||||
- Make a PR with your changes
|
||||
|
||||
## 👍 Translators
|
||||
|
||||
- 🇫🇷 French by [@jvin042](https://github.com/jvin042)
|
||||
- 🇵🇹 Portuguese by [@fabtrompet](https://github.com/fabtrompet)
|
||||
- 🇳🇱 Dutch by [@SchoNie](https://github.com/SchoNie)
|
||||
- 🇩🇪 German by [@mschoeffmann](https://github.com/mschoeffmann)
|
||||
- 🇪🇸 Spanish by [@Runig006](https://github.com/Runig006)
|
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"
|
||||
```
|
244
templates/app-down.html
Normal file
244
templates/app-down.html
Normal file
@ -0,0 +1,244 @@
|
||||
<!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>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--color-bg-primary:#fff;--color-bg-secondary:#eef6fa;--color-bg-sign:#fff;--color-text-primary:#333;--color-text-secondary:#777;--color-img-details:#f62f37;--color-img-primary:#7990a1;--color-img-secondary:#00baff;--font-size-small:13px;--font-size-normal:16px;--font-size-large:45px}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root{--color-bg-primary:#222526;--color-bg-secondary:#292e2f;--color-bg-sign:#262828;--color-text-primary:#fff;--color-text-secondary:#999;--color-img-details:#c72d34;--color-img-primary:#adacac;--color-img-secondary:#86d3ff}
|
||||
}
|
||||
body,html{background-color:var(--color-bg-primary);color:var(--color-text-primary);font-family:Roboto,Helvetica,sans-serif;font-size:0;margin:0;padding:0;height:100vh;overflow-x:hidden}
|
||||
body{align-items:center;display:flex;justify-content:center;height:100vh}
|
||||
main{width:100%;max-width:1024px;padding:0 40px;display:flex;justify-content:space-between}
|
||||
.content,.picture{box-sizing:border-box}
|
||||
.content{display:flex;flex-direction:column;flex-shrink:0;justify-content:space-around;width:45%;z-index:1}
|
||||
a,p,ul li{font-size:var(--font-size-normal)}
|
||||
.title{line-height:1.2;font-size:var(--font-size-large);margin:0 0 30px;width:130%}
|
||||
.subtitle{display:flex;flex-direction:column;justify-content:center;margin:16px 0}
|
||||
ul{padding:0;list-style:none;line-height:24px}
|
||||
ul li::before{content:'•';padding-right:7px;color:var(--color-img-secondary)}
|
||||
/* {{ if show_details }} */
|
||||
.details{margin:0 0 16px 0}
|
||||
.details p{font-size:var(--font-size-small)}
|
||||
.details ul{line-height:0}
|
||||
.details ul li{padding-top:calc(var(--font-size-small) * 1.5)}
|
||||
.details ul li:first-child{padding-top:calc(var(--font-size-small) * .6)}
|
||||
.details code,.details span,.details ul li::before{font-size:var(--font-size-small);font-weight:400}
|
||||
.details code{padding-left:7px}
|
||||
/* {{ end }} */
|
||||
a{text-decoration:none;color:var(--color-img-secondary)}
|
||||
.hidden{display:none}
|
||||
.picture{display:flex;align-items:center;justify-content:center;width:55%;user-select:none;z-index:0}
|
||||
.picture svg{width:100%}
|
||||
.picture svg .st10,.picture svg .st11,.picture svg .st12,.picture svg .st13,.picture svg .st14,.picture svg .st15,.picture svg .st16,.picture svg .st17,.picture svg .st3,.picture svg .st6,.picture svg .st9{stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10}
|
||||
.picture svg .st0{fill:var(--color-bg-primary)}
|
||||
.picture svg .st1{fill:url(#svg-background-gradient)}
|
||||
.picture svg .st2{fill:var(--color-bg-secondary)}
|
||||
.picture svg .st3{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:3.5}
|
||||
.picture svg .st4{fill:var(--color-img-secondary)}
|
||||
.picture svg .st5{fill:none;stroke:var(--color-img-secondary);stroke-width:4;stroke-linejoin:round;stroke-miterlimit:10}
|
||||
.picture svg .st6{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:3}
|
||||
.picture svg .st7{fill:var(--color-img-primary)}
|
||||
.picture svg .st8{fill:none;stroke:var(--color-img-primary);stroke-width:2.5;stroke-linecap:round;stroke-miterlimit:10}
|
||||
.picture svg .st9{fill:none;stroke:var(--color-img-primary);stroke-width:3}
|
||||
.picture svg .st10{fill:none;stroke:var(--color-img-primary);stroke-width:3.5}
|
||||
.picture svg .st11{fill:none;stroke:var(--color-img-secondary);stroke-width:4}
|
||||
.picture svg .st12{fill:var(--color-bg-primary);stroke:var(--color-img-primary);stroke-width:4}
|
||||
.picture svg .st13{fill:none;stroke:var(--color-img-primary);stroke-width:4}
|
||||
.picture svg .st14{fill:none;stroke:var(--color-img-secondary);stroke-width:4.5}
|
||||
.picture svg .st15{fill:none;stroke:var(--color-img-secondary);stroke-width:5}
|
||||
.picture svg .st16{fill:none;stroke:var(--color-img-primary);stroke-width:5}
|
||||
.picture svg .st17{fill:var(--color-bg-primary);stroke:var(--color-img-details);stroke-width:3.5}
|
||||
.picture svg .st19{fill:none;stroke:var(--color-img-details);stroke-width:2.5;stroke-linecap:round;stroke-miterlimit:10}
|
||||
.picture svg .error-code{font:bold 40px sans-serif;fill:var(--color-img-details)}
|
||||
@media (max-width:1024px){
|
||||
:root{--font-size-small:11px;--font-size-normal:14px;--font-size-large:35px}
|
||||
main{display:block;position:relative;padding-top:40px}
|
||||
.content,.picture{width:100%}
|
||||
.content{position:relative;margin:0 auto;z-index:1}
|
||||
.title{width:100%}
|
||||
.picture{position:absolute;top:0;left:0;z-index:0;opacity:.2;width:100%;height:100%;padding:0}
|
||||
.picture svg{max-width:70%}
|
||||
}
|
||||
@media (max-width:600px){
|
||||
.picture svg{max-width:100%}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="content">
|
||||
<h2 class="title" data-l10n>{{ message }}</h2>
|
||||
<p data-l10n>{{ description }}</p>
|
||||
<div class="subtitle if-not-found hidden">
|
||||
<p><span data-l10n>Here's what might have happened</span>:</p>
|
||||
<ul>
|
||||
<li data-l10n>You may have mistyped the URL</li>
|
||||
<li data-l10n>The site was moved</li>
|
||||
<li data-l10n>It was never here</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="if-maybe-wrong-uri"><span data-l10n>Double-check the URL</span>. <a class="go-back hidden" data-l10n>Alternatively, go back</a></p>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<p><span data-l10n>Request details</span>:</p>
|
||||
<ul>
|
||||
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
|
||||
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
|
||||
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
|
||||
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
|
||||
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
|
||||
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
|
||||
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
|
||||
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
|
||||
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="picture">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 480" x="0px" y="0px" xml:space="preserve">
|
||||
<rect y="0" class="st0" width="600" height="480"></rect>
|
||||
<radialgradient id="svg-background-gradient" cx="328.1394" cy="306.3561" r="219.5134" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:var(--color-bg-secondary)"></stop>
|
||||
<stop offset="0.5002" style="stop-color:var(--color-bg-secondary)"></stop>
|
||||
<stop offset="1" style="stop-color:var(--color-bg-primary)"></stop>
|
||||
</radialgradient>
|
||||
<rect x="95.2" y="35.7" class="st1" width="460" height="271.4"></rect>
|
||||
<ellipse class="st2" cx="289.7" cy="352.3" rx="69.5" ry="13.9"></ellipse>
|
||||
<ellipse class="st2" cx="180.5" cy="396.3" rx="51.2" ry="9.5"></ellipse>
|
||||
<ellipse class="st2" cx="381.3" cy="418.3" rx="40.8" ry="6.4"></ellipse>
|
||||
<path class="st3" d="M551.1,285.8H527c-2.3,0-4.1-1.8-4.1-4.1v-30c0-2.3,1.8-4.1,4.1-4.1h24.1c2.3,0,4.1,1.8,4.1,4.1v30 C555.2,284,553.4,285.8,551.1,285.8z"></path>
|
||||
<circle class="st3" cx="539.1" cy="266.7" r="10.3"></circle>
|
||||
<path class="st4" d="M265.6,343.3c-5,0-9,4-9,9h18C274.6,347.3,270.6,343.3,265.6,343.3z"></path>
|
||||
<line class="st5" x1="272.7" y1="328.1" x2="272.7" y2="352.3"></line>
|
||||
<path class="st4" d="M307,343.3c-5,0-9,4-9,9h18C316,347.3,311.9,343.3,307,343.3z"></path>
|
||||
<line class="st5" x1="314.1" y1="328.1" x2="314.1" y2="352.3"></line>
|
||||
<path class="st6" d="M380.7,422.6l-37.6-6.4c-1.5-0.3-2.5-1.5-2.2-2.9l4.6-26.8c0.2-1.4,1.6-2.2,3-2l37.6,6.4 c1.5,0.3,2.5,1.5,2.2,2.9l-4.6,26.8C383.6,422,382.2,422.9,380.7,422.6z"></path>
|
||||
<path class="st6" d="M344.6,391.5l0.8-4.5c0.3-1.7,1.6-2.8,3.1-2.5l37.6,6.4c1.5,0.3,2.4,1.7,2.1,3.4l-0.8,4.5L344.6,391.5z"></path>
|
||||
<circle class="st7" cx="349" cy="388.4" r="1"></circle>
|
||||
<circle class="st7" cx="353.1" cy="389.1" r="1"></circle>
|
||||
<circle class="st7" cx="357.1" cy="389.8" r="1"></circle>
|
||||
<line class="st8" x1="360.4" y1="402.8" x2="367.4" y2="412.7"></line>
|
||||
<line class="st8" x1="368.8" y1="404.3" x2="359" y2="411.2"></line>
|
||||
<path class="st6" d="M166.4,401.4l-36.6-10.8c-1.5-0.4-2.3-1.8-1.9-3.1l7.7-26.1c0.4-1.3,1.8-2,3.3-1.6l36.6,10.8 c1.5,0.4,2.3,1.8,1.9,3.1l-7.7,26.1C169.3,401.1,167.9,401.8,166.4,401.4z"></path>
|
||||
<path class="st6" d="M134.2,366.2l1.3-4.4c0.5-1.6,2-2.6,3.4-2.1l36.6,10.8c1.5,0.4,2.2,2,1.7,3.6l-1.3,4.4L134.2,366.2z"></path>
|
||||
<circle class="st7" cx="138.9" cy="363.7" r="1"></circle>
|
||||
<circle class="st7" cx="142.9" cy="364.8" r="1"></circle>
|
||||
<circle class="st7" cx="146.9" cy="366" r="1"></circle>
|
||||
<path class="st6" d="M220.9,399.3l-38-3.9c-1.5-0.2-2.5-1.3-2.4-2.7l2.8-27.1c0.1-1.4,1.4-2.3,2.9-2.2l38,3.9 c1.5,0.2,2.5,1.3,2.4,2.7l-2.8,27.1C223.6,398.5,222.4,399.5,220.9,399.3z"></path>
|
||||
<path class="st6" d="M188.6,400.9l-38.1,2.8c-1.5,0.1-2.7-0.9-2.8-2.3l-2-27.1c-0.1-1.4,1-2.6,2.5-2.7l38.1-2.8 c1.5-0.1,2.7,0.9,2.8,2.3l2,27.1C191.2,399.6,190.1,400.8,188.6,400.9z"></path>
|
||||
<path class="st9" d="M146.1,379.4l-0.3-4.5c-0.1-1.7,0.9-3.1,2.4-3.2l38.1-2.8c1.5-0.1,2.8,1.1,2.9,2.8l0.3,4.5L146.1,379.4z"></path>
|
||||
<circle class="st7" cx="149.6" cy="375.3" r="1"></circle>
|
||||
<circle class="st7" cx="153.7" cy="375" r="1"></circle>
|
||||
<circle class="st7" cx="157.8" cy="374.7" r="1"></circle>
|
||||
<line class="st8" x1="164.1" y1="386.6" x2="173.3" y2="394.4"></line>
|
||||
<line class="st8" x1="172.7" y1="385.9" x2="164.8" y2="395.1"></line>
|
||||
<path class="st10" d="M539.1,267.8c0,96.1-51.7,97.6-67.6,98.6c-28.1,1.8-76.3-14.4-63-25.6c13.3-11.2,53.8-10.3,59.3-4.3 c4,4.3,6.1,16.6-49.9,15.8c-29.4-0.4-51-8.4-60.8-32.1"></path>
|
||||
<path class="st11" d="M184.1,262.5c17.8,9,28.4-2.4,28.4-2.4"></path>
|
||||
<ellipse class="st0" cx="289.7" cy="170.7" rx="77.1" ry="21.7"></ellipse>
|
||||
<path class="st12" d="M366.8,308.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8V170.7c0,12.1,34.5,21.8,77.1,21.8 c42.6,0,77.1-9.8,77.1-21.8V308.7z"></path>
|
||||
<path class="st13" d="M212.6,170.7c0-12.1,34.5-21.8,77.1-21.8c42.6,0,77.1,9.8,77.1,21.8"></path>
|
||||
<path class="st13" d="M366.8,216.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8"></path>
|
||||
<path class="st13" d="M366.8,262.7c0,12.1-34.5,21.8-77.1,21.8c-42.6,0-77.1-9.8-77.1-21.8"></path>
|
||||
<path class="st11" d="M384.2,279.8c-6.2-18.9-25.1-18.7-25.1-18.7"></path>
|
||||
<path class="st14" d="M378,288.7c0,0,0-6.3,5.6-8.8c0,0,1.6,0.5,3.3,1.3"></path>
|
||||
<path class="st15" d="M384.2,279.8"></path>
|
||||
<circle class="st4" cx="319" cy="254.8" r="4.2"></circle>
|
||||
<circle class="st4" cx="257.2" cy="255.4" r="4.2"></circle>
|
||||
<line class="st16" x1="182.4" y1="284.4" x2="179" y2="229.2"></line>
|
||||
<polygon class="st17" points="191.3,144 153.6,146.3 128.7,174.8 131,212.7 159.3,238 196.9,235.6 221.8,207.2 219.5,169.2" style="fill:var(--color-bg-sign)"></polygon>
|
||||
<text class="error-code" x="125" y="220" transform="rotate(-5)">{{ code }}</text>
|
||||
<line class="st14" x1="183.2" y1="255.9" x2="175.9" y2="258.8"></line>
|
||||
<line class="st14" x1="184.7" y1="260.4" x2="175.8" y2="263"></line>
|
||||
<line class="st14" x1="185.4" y1="265.4" x2="176.9" y2="267.2"></line>
|
||||
<ellipse class="st11" cx="287.7" cy="269" rx="4.4" ry="6.7"></ellipse>
|
||||
<path class="st6" d="M405.5,316l-37.8,5.5c-1.5,0.2-2.8-0.7-3-2.1l-3.9-26.9c-0.2-1.4,0.8-2.6,2.3-2.8l37.8-5.5 c1.5-0.2,2.8,0.7,3,2.1l3.9,26.9C407.9,314.5,407,315.7,405.5,316z"></path>
|
||||
<path class="st6" d="M361.5,297.6l-0.7-4.5c-0.2-1.7,0.7-3.1,2.2-3.4l37.8-5.5c1.5-0.2,2.8,0.9,3.1,2.6l0.7,4.5L361.5,297.6z"></path>
|
||||
<circle class="st7" cx="364.7" cy="293.3" r="1"></circle>
|
||||
<circle class="st7" cx="368.8" cy="292.7" r="1"></circle>
|
||||
<circle class="st7" cx="372.9" cy="292.1" r="1"></circle>
|
||||
<line class="st19" x1="380" y1="303.4" x2="389.7" y2="310.6"></line>
|
||||
<line class="st19" x1="388.5" y1="302.2" x2="381.3" y2="311.9"></line>
|
||||
<path class="st6" d="M204.8,355.2l-28.4,25.5c-1.1,1-2.7,1-3.6-0.1l-18.2-20.3c-0.9-1-0.8-2.6,0.3-3.6l28.4-25.5 c1.1-1,2.7-1,3.6,0.1l18.2,20.3C206.1,352.6,205.9,354.2,204.8,355.2z"></path>
|
||||
<path class="st9" d="M158,364.1l-3-3.4c-1.1-1.3-1.1-3,0-4l28.4-25.5c1.1-1,2.9-0.8,4,0.5l3,3.4L158,364.1z"></path>
|
||||
<circle class="st7" cx="158.3" cy="358.7" r="1"></circle>
|
||||
<circle class="st7" cx="161.3" cy="356" r="1"></circle>
|
||||
<circle class="st7" cx="164.4" cy="353.2" r="1"></circle>
|
||||
<line class="st8" x1="176.7" y1="358.8" x2="188.7" y2="359.4"></line>
|
||||
<line class="st8" x1="183" y1="353.1" x2="182.4" y2="365.1"></line>
|
||||
<path class="st6" d="M219.9,344l14.8,35.2c0.6,1.4,0,2.9-1.2,3.4l-25.1,10.5c-1.3,0.5-2.7-0.1-3.3-1.5l-14.8-35.2 c-0.6-1.4,0-2.9,1.2-3.4l25.1-10.5C217.8,341.9,219.3,342.6,219.9,344z"></path>
|
||||
<path class="st9" d="M213,391.1l-4.2,1.8c-1.6,0.7-3.2,0.1-3.8-1.3l-14.8-35.2c-0.6-1.4,0.2-3,1.7-3.6l4.2-1.8L213,391.1z"></path>
|
||||
<circle class="st7" cx="208" cy="389.1" r="1"></circle>
|
||||
<circle class="st7" cx="206.4" cy="385.3" r="1"></circle>
|
||||
<circle class="st7" cx="204.8" cy="381.5" r="1"></circle>
|
||||
<line class="st8" x1="214.1" y1="371.7" x2="218.6" y2="360.6"></line>
|
||||
<line class="st8" x1="210.8" y1="363.9" x2="221.9" y2="368.4"></line>
|
||||
<path class="st14" d="M394.1,287.1c-0.7-1.6-3.9-4.5-7.2-5.9"></path>
|
||||
<path class="st6" d="M419.7,413.7l-37.8,5.2c-1.5,0.2-2.8-0.7-3-2.1l-3.7-27c-0.2-1.4,0.8-2.6,2.3-2.8l37.8-5.2 c1.5-0.2,2.8,0.7,3,2.1l3.7,27C422.2,412.2,421.2,413.5,419.7,413.7z"></path>
|
||||
<path class="st6" d="M375.9,394.8l-0.6-4.5c-0.2-1.7,0.7-3.1,2.2-3.3l37.8-5.2c1.5-0.2,2.8,0.9,3.1,2.6l0.6,4.5L375.9,394.8z"></path>
|
||||
<circle class="st7" cx="379.2" cy="390.6" r="1"></circle>
|
||||
<circle class="st7" cx="383.3" cy="390" r="1"></circle>
|
||||
<circle class="st7" cx="387.4" cy="389.5" r="1"></circle>
|
||||
<line class="st8" x1="394.4" y1="400.9" x2="404" y2="408.2"></line>
|
||||
<line class="st8" x1="402.9" y1="399.7" x2="395.6" y2="409.4"></line>
|
||||
<polygon class="st17" points="361,62.2 346.5,104.9 364.7,107.8 347.6,141.8 382,99.7 363.5,93.5 385,63.8 "></polygon>
|
||||
<polygon class="st17" points="396.5,101.6 374.8,122.8 384.1,130.2 363.6,145.4 396.4,130.6 388,121.2 409.5,109.9 "></polygon>
|
||||
<line class="st14" x1="384.7" y1="281.7" x2="386" y2="290.6"></line>
|
||||
</svg>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('if-not-found'), function ($el) {
|
||||
$el.style.display = "{{ code }}" === "404" ? 'block' : 'none';
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('if-maybe-wrong-uri'), function ($el) {
|
||||
$el.style.display = ["401", "403", "404", "418", "505"].includes("{{ code }}") ? 'block' : 'none';
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('go-back'), function ($el) {
|
||||
if (document.referrer !== '' || history.length > 1) {
|
||||
$el.setAttribute('href', '#back-to-the-future');
|
||||
|
||||
$el.addEventListener('click', function (event) {
|
||||
history.back();
|
||||
event.preventDefault();
|
||||
|
||||
return false;
|
||||
}, false);
|
||||
|
||||
$el.style.display = 'inline-block'; // show the element
|
||||
} else {
|
||||
$el.style.display = 'none'; // hide the element
|
||||
}
|
||||
});
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
</html>
|
@ -7,14 +7,16 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<title>{{ message }}</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
color: #aaa;
|
||||
overflow: hidden;
|
||||
color: #ddd;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.centered {
|
||||
@ -28,13 +30,91 @@
|
||||
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 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" data-l10n>Host</td>
|
||||
<td class="value">{{ host }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if original_uri }}<tr>
|
||||
<td class="name" data-l10n>Original URI</td>
|
||||
<td class="value">{{ original_uri }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if forwarded_for }}<tr>
|
||||
<td class="name" data-l10n>Forwarded for</td>
|
||||
<td class="value">{{ forwarded_for }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if namespace }}<tr>
|
||||
<td class="name" data-l10n>Namespace</td>
|
||||
<td class="value">{{ namespace }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if ingress_name }}<tr>
|
||||
<td class="name" data-l10n>Ingress name</td>
|
||||
<td class="value">{{ ingress_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_name }}<tr>
|
||||
<td class="name" data-l10n>Service name</td>
|
||||
<td class="value">{{ service_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_port }}<tr>
|
||||
<td class="name" data-l10n>Service port</td>
|
||||
<td class="value">{{ service_port }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if request_id }}<tr>
|
||||
<td class="name" data-l10n>Request ID</td>
|
||||
<td class="value">{{ request_id }}</td>
|
||||
</tr>{{ end -}}
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
|
272
templates/connection.html
Normal file
272
templates/connection.html
Normal file
@ -0,0 +1,272 @@
|
||||
<!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"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<title>{{ code }} | {{ message }}</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@500&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
/** Idea author: https://github.com/186526/CloudflareCustomErrorPage */
|
||||
:root{--color-bg-primary:#fff;--color-text-primary:#000;--color-text-secondary:#575958;--font-size-primary:56px;--font-size-secondary:20px;--ui-card-color-bg:#f2f2f2;--color-text-ok:#137333;--color-bg-ok:#e6f4ea;--color-text-error:#c5221f;--color-bg-error:#fce8e6;--color-text-warning:#b05a00;--color-bg-warning:#fef7e0;--icon-size:48px}
|
||||
@media (prefers-color-scheme:dark){
|
||||
:root{--color-bg-primary:#111;--color-text-primary:rgba(255, 255, 255, 0.86);--color-text-secondary:rgba(255, 255, 255, 0.4);--ui-card-color-bg:rgba(40, 40, 40, 0.73);--color-bg-ok:#07220f;--color-bg-error:#270501;--color-bg-warning:#392605}
|
||||
}
|
||||
body{margin:2rem 2rem;font-family:'Red Hat Display',Ubuntu,Roboto,'Noto Sans SC',sans-serif;color:var(--color-text-primary);background-color:var(--color-bg-primary)}
|
||||
header{margin-left:1rem}
|
||||
header .error-code{font-size:var(--font-size-primary);line-height:var(--font-size-primary);font-family:'Fira Mono',Ubuntu,monospace;font-weight:400;margin:0 0 0 10px}
|
||||
header .error-description{font-family:Ubuntu,Roboto,'Noto Sans SC',sans-serif;font-size:var(--font-size-secondary);color:var(--color-text-secondary);margin:0 0 0 10px}
|
||||
code{font-family:'Fira Mono',monospace}
|
||||
.status{margin-top:2.5rem;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:center;align-items:center}
|
||||
.card{background-color:var(--ui-card-color-bg);padding:2rem;margin:1rem 1rem;min-height:3rem;border-radius:9px;flex-grow:1}
|
||||
.arrows svg{fill:var(--color-text-secondary)}
|
||||
.icon svg{width:var(--icon-size);height:auto;fill:var(--color-text-primary)}
|
||||
.card.ok{background-color:var(--color-bg-ok)}.card.ok .status-text{color:var(--color-text-ok)}.card.ok svg{fill:var(--color-text-ok)}
|
||||
.card.error{background-color:var(--color-bg-error)}.card.error .status-text{color:var(--color-text-error)}.card.error svg{fill:var(--color-text-error)}
|
||||
.card.warning{background-color:var(--color-bg-warning)}.card.warning .status-text{color:var(--color-text-warning)}.card.warning svg{fill:var(--color-text-warning)}
|
||||
.card main{font-size:calc(var(--font-size-secondary) + .1rem)}
|
||||
.card .status-text,.reason p{margin:0;font-family:Ubuntu,Roboto,Noto Sans SC,sans-serif}
|
||||
.reason p{line-height:125%}
|
||||
a{text-decoration:none;color:#1967d2}
|
||||
.reason{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline}
|
||||
.reason>*{display:block;margin:1rem;flex-grow:1;max-width:40%}
|
||||
.reason h2{font-size:calc(var(--font-size-secondary) + .2rem);margin:0 0 .6em 0;font-weight:550}
|
||||
footer{margin:1rem;color:var(--color-text-secondary);font-size:0}
|
||||
/* {{ if show_details }} */
|
||||
footer .details{margin-top:20px}
|
||||
footer .details ul{padding:0}
|
||||
footer .details code,footer .details span{font-size:calc(var(--font-size-secondary) - .6rem)}
|
||||
footer .details code{padding-left:7px}
|
||||
/* {{ end }} */
|
||||
@media screen and (max-width:820px){
|
||||
.arrows{display:none}
|
||||
}
|
||||
@media screen and (max-width:480px){
|
||||
.reason>*{max-width:100%}
|
||||
/* {{ if show_details }} */
|
||||
footer .details code,footer .details span{font-size:calc(var(--font-size-secondary) - .3rem)}
|
||||
/* {{ end }} */
|
||||
}
|
||||
@media screen and (min-width:768px){
|
||||
body{margin:8% 10%}
|
||||
header>*{display:inline-block;margin-left:1%}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1 class="error-code">{{ code }}</h1>
|
||||
<p class="error-description">{{ message }}</p>
|
||||
</header>
|
||||
<div class="status">
|
||||
<div class="card warning" id="client-status-card">
|
||||
<i class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm0 14H5V8h14v10z"/>
|
||||
</svg>
|
||||
</i>
|
||||
<main data-l10n>Your Client</main>
|
||||
<p class="status-text" data-l10n>Unknown</p>
|
||||
</div>
|
||||
|
||||
<div class="arrows">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" fill="#000000">
|
||||
<defs>
|
||||
<symbol id="arrows-horizontal" viewBox="0 0 24 24">
|
||||
<rect fill="none" height="24" width="24" x="0"/>
|
||||
<polygon points="7.41,13.41 6,12 2,16 6,20 7.41,18.59 5.83,17 21,17 21,15 5.83,15"/>
|
||||
<polygon points="16.59,10.59 18,12 22,8 18,4 16.59,5.41 18.17,7 3,7 3,9 18.17,9"/>
|
||||
</symbol>
|
||||
</defs>
|
||||
<use href="#arrows-horizontal"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="card ok" id="network-status-card">
|
||||
<i class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6m0-2C9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96C18.67 6.59 15.64 4 12 4z"/>
|
||||
</svg>
|
||||
</i>
|
||||
<main data-l10n>Network</main>
|
||||
<p class="status-text" data-l10n>Working</p>
|
||||
</div>
|
||||
|
||||
<div class="arrows">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" fill="#000000">
|
||||
<use href="#arrows-horizontal" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="card warning" id="server-status-card">
|
||||
<i class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"/>
|
||||
<path d="M19 15v4H5v-4h14m1-2H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1zM7 18.5c-.82 0-1.5-.67-1.5-1.5s.68-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zM19 5v4H5V5h14m1-2H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zM7 8.5c-.82 0-1.5-.67-1.5-1.5S6.18 5.5 7 5.5s1.5.68 1.5 1.5S7.83 8.5 7 8.5z"/>
|
||||
</svg>
|
||||
</i>
|
||||
<main data-l10n>Web Server</main>
|
||||
<p class="status-text" data-l10n>Unknown</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reason">
|
||||
<div class="what-happened">
|
||||
<h2 data-l10n>What happened?</h2>
|
||||
<p class="description" data-l10n>{{ description }}</p>
|
||||
</div>
|
||||
<div class="what-can-i-do">
|
||||
<h2 data-l10n>What can I do?</h2>
|
||||
<p class="description" data-l10n>Please try again in a few minutes</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<footer>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<ul>
|
||||
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
|
||||
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
|
||||
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
|
||||
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
|
||||
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
|
||||
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
|
||||
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
|
||||
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
|
||||
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
</footer>
|
||||
<script>
|
||||
const errorCode = parseInt(`{{ code }}`, 10);
|
||||
|
||||
if (typeof errorCode !== 'undefined' && !isNaN(errorCode)) {
|
||||
/**
|
||||
* @param {HTMLElement} $card
|
||||
* @param { {isOk?: boolean, isWarning?: boolean, isError?: boolean} } state
|
||||
* @param {string} statusText
|
||||
*/
|
||||
const setCardState = function ($card, state, statusText) {
|
||||
const okClass = 'ok', warnClass = 'warning', errClass = 'error',
|
||||
$statusText = $card.querySelectorAll('.status-text');
|
||||
|
||||
switch (true) {
|
||||
case state.isOk === true:
|
||||
$card.classList.remove(errClass, warnClass);
|
||||
$card.classList.add(okClass);
|
||||
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
|
||||
break;
|
||||
|
||||
case state.isWarning === true:
|
||||
$card.classList.remove(okClass, errClass);
|
||||
$card.classList.add(warnClass);
|
||||
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
|
||||
break;
|
||||
|
||||
case state.isError === true:
|
||||
$card.classList.remove(okClass, warnClass);
|
||||
$card.classList.add(errClass);
|
||||
$statusText.forEach(($statusText) => $statusText.innerText = statusText);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param { {whatHappened?: string, whatToDo?: string} } reasons
|
||||
*/
|
||||
const setReasons = function (reasons) {
|
||||
const descSelector = '.description';
|
||||
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('what-happened'), ($el) => {
|
||||
if (typeof reasons.whatHappened === 'string' && reasons.whatHappened.length > 0) {
|
||||
Array.prototype.forEach.call($el.querySelectorAll(descSelector), ($desc) => $desc.innerText = reasons.whatHappened);
|
||||
} else {
|
||||
$el.remove();
|
||||
}
|
||||
});
|
||||
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('what-can-i-do'), ($el) => {
|
||||
if (typeof reasons.whatToDo === 'string' && reasons.whatToDo.length > 0) {
|
||||
Array.prototype.forEach.call($el.querySelectorAll(descSelector), ($desc) => $desc.innerText = reasons.whatToDo);
|
||||
} else {
|
||||
$el.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
const setErrorDescription = function (text) {
|
||||
Array.prototype.forEach.call(document.getElementsByClassName('error-description'), function ($el) {
|
||||
$el.innerHTML = text;
|
||||
});
|
||||
};
|
||||
|
||||
const message = `{{ message }}`.trim(), cards = {
|
||||
$client: document.getElementById('client-status-card'),
|
||||
$network: document.getElementById('network-status-card'),
|
||||
$server: document.getElementById('server-status-card'),
|
||||
};
|
||||
|
||||
let whatToDo = 'Please try again in a few minutes';
|
||||
|
||||
switch (true) {
|
||||
case errorCode >= 400 && errorCode <= 499:
|
||||
switch (errorCode) {
|
||||
case 400: case 405: case 411: case 413: whatToDo = 'Please try to change the request method, headers, payload, or URL'; break;
|
||||
case 401: case 403: case 407: whatToDo = 'Please check your authorization data'; break;
|
||||
case 404: whatToDo = 'Please double-check the URL and try again'; break;
|
||||
case 409: case 410: case 418: whatToDo = '¯\\_(ツ)_/¯'; break;
|
||||
}
|
||||
|
||||
setErrorDescription(`<span data-l10n>${message}</span> (<span data-l10n>client-side error</span>)`);
|
||||
setCardState(cards.$client, {isError: true}, message)
|
||||
setCardState(cards.$network, {isOk: true}, 'Working')
|
||||
setCardState(cards.$server, {isOk: true}, 'Working')
|
||||
break;
|
||||
|
||||
case errorCode >= 500 && errorCode <= 599:
|
||||
setErrorDescription(`<span data-l10n>${message}</span> (<span data-l10n>server-side error</span>)`);
|
||||
setCardState(cards.$client, {isOk: true}, 'Working')
|
||||
setCardState(cards.$network, {isOk: true}, 'Working')
|
||||
setCardState(cards.$server, {isError: true}, message)
|
||||
break;
|
||||
|
||||
default:
|
||||
setErrorDescription(message);
|
||||
setCardState(cards.$client, {isWarning: true}, 'Unknown')
|
||||
setCardState(cards.$network, {isOk: true}, 'Working')
|
||||
setCardState(cards.$server, {isWarning: true}, 'Unknown')
|
||||
break;
|
||||
}
|
||||
|
||||
setReasons({whatHappened: `{{ description }}`.trim(), whatToDo: whatToDo.trim()});
|
||||
} else {
|
||||
console.warn('Cannot parse the error code:', errorCode);
|
||||
}
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
</html>
|
@ -9,41 +9,106 @@
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>{{ code }}: {{ message }}</title>
|
||||
<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" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" 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" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="127.433px" height="132.743px" viewBox="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 class="shadow" xmlns="http://www.w3.org/2000/svg" x="61px" y="20px" width="122.436px" height="39.744px" viewBox="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><span data-l10n>Error</span> {{ code }}</h3>
|
||||
<p class="description" data-l10n>{{ description }}</p>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<table>
|
||||
{{- if host }}<tr>
|
||||
<td class="name" data-l10n>Host</td>
|
||||
<td class="value">{{ host }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if original_uri }}<tr>
|
||||
<td class="name" data-l10n>Original URI</td>
|
||||
<td class="value">{{ original_uri }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if forwarded_for }}<tr>
|
||||
<td class="name" data-l10n>Forwarded for</td>
|
||||
<td class="value">{{ forwarded_for }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if namespace }}<tr>
|
||||
<td class="name" data-l10n>Namespace</td>
|
||||
<td class="value">{{ namespace }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if ingress_name }}<tr>
|
||||
<td class="name" data-l10n>Ingress name</td>
|
||||
<td class="value">{{ ingress_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_name }}<tr>
|
||||
<td class="name" data-l10n>Service name</td>
|
||||
<td class="value">{{ service_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_port }}<tr>
|
||||
<td class="name" data-l10n>Service port</td>
|
||||
<td class="value">{{ service_port }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if request_id }}<tr>
|
||||
<td class="name" data-l10n>Request ID</td>
|
||||
<td class="value">{{ request_id }}</td>
|
||||
</tr>{{ end -}}
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
|
@ -9,8 +9,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>{{ message }}</title>
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/** Idea author: https://codepen.io/robinselmer */
|
||||
html, 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,15 +121,59 @@
|
||||
.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>
|
||||
<div class="overlay"></div>
|
||||
<div class="terminal">
|
||||
<h1>Error <span class="error_code">{{ code }}</span></h1>
|
||||
<p class="output">{{ description }}.</p>
|
||||
<p class="output">Good luck.</p>
|
||||
<h1><span data-l10n>Error</span> <span class="error_code">{{ code }}</span></h1>
|
||||
<p class="output" data-l10n>{{ description }}.</p>
|
||||
<p class="output"><span data-l10n>Good luck</span>.</p>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
{{- if host }}<p class="output small"><span data-l10n>Host</span>: <code>{{ host }}</code></p>{{ end -}}
|
||||
{{- if original_uri }}<p class="output small"><span data-l10n>Original URI</span>: <code>{{ original_uri }}</code></p>{{ end -}}
|
||||
{{- if forwarded_for }}<p class="output small"><span data-l10n>Forwarded for</span>: <code>{{ forwarded_for }}</code></p>{{ end -}}
|
||||
{{- if namespace }}<p class="output small"><span data-l10n>Namespace</span>: <code>{{ namespace }}</code></p>{{ end -}}
|
||||
{{- if ingress_name }}<p class="output small"><span data-l10n>Ingress name</span>: <code>{{ ingress_name }}</code></p>{{ end -}}
|
||||
{{- if service_name }}<p class="output small"><span data-l10n>Service name</span>: <code>{{ service_name }}</code></p>{{ end -}}
|
||||
{{- if service_port }}<p class="output small"><span data-l10n>Service port</span>: <code>{{ service_port }}</code></p>{{ end -}}
|
||||
{{- if request_id }}<p class="output small"><span data-l10n>Request ID</span>: <code>{{ request_id }}</code></p>{{ end -}}
|
||||
<p class="output small"><span data-l10n>Timestamp</span>: <code>{{ now.Unix }}</code></p>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<script>
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
|
@ -12,23 +12,87 @@
|
||||
<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" data-l10n>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<table>
|
||||
{{- if host }}<tr>
|
||||
<td class="name" data-l10n>Host</td>
|
||||
<td class="value">{{ host }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if original_uri }}<tr>
|
||||
<td class="name" data-l10n>Original URI</td>
|
||||
<td class="value">{{ original_uri }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if forwarded_for }}<tr>
|
||||
<td class="name" data-l10n>Forwarded for</td>
|
||||
<td class="value">{{ forwarded_for }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if namespace }}<tr>
|
||||
<td class="name" data-l10n>Namespace</td>
|
||||
<td class="value">{{ namespace }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if ingress_name }}<tr>
|
||||
<td class="name" data-l10n>Ingress name</td>
|
||||
<td class="value">{{ ingress_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_name }}<tr>
|
||||
<td class="name" data-l10n>Service name</td>
|
||||
<td class="value">{{ service_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_port }}<tr>
|
||||
<td class="name" data-l10n>Service port</td>
|
||||
<td class="value">{{ service_port }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if request_id }}<tr>
|
||||
<td class="name" data-l10n>Request ID</td>
|
||||
<td class="value">{{ request_id }}</td>
|
||||
</tr>{{ end -}}
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
|
@ -12,23 +12,89 @@
|
||||
<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}
|
||||
:root {--color-bg-primary:#fff;--color-text-primary:#636b6f;--color-text-secondary:#777}
|
||||
@media (prefers-color-scheme: dark) {:root {--color-bg-primary:#222526;--color-text-primary:#fff;--color-text-secondary:#999}}
|
||||
html,body {background-color:var(--color-bg-primary);color:var(--color-text-primary);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:var(--color-text-secondary)}
|
||||
.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" data-l10n>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<table>
|
||||
{{- if host }}<tr>
|
||||
<td class="name" data-l10n>Host</td>
|
||||
<td class="value">{{ host }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if original_uri }}<tr>
|
||||
<td class="name" data-l10n>Original URI</td>
|
||||
<td class="value">{{ original_uri }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if forwarded_for }}<tr>
|
||||
<td class="name" data-l10n>Forwarded for</td>
|
||||
<td class="value">{{ forwarded_for }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if namespace }}<tr>
|
||||
<td class="name" data-l10n>Namespace</td>
|
||||
<td class="value">{{ namespace }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if ingress_name }}<tr>
|
||||
<td class="name" data-l10n>Ingress name</td>
|
||||
<td class="value">{{ ingress_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_name }}<tr>
|
||||
<td class="name" data-l10n>Service name</td>
|
||||
<td class="value">{{ service_name }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if service_port }}<tr>
|
||||
<td class="name" data-l10n>Service port</td>
|
||||
<td class="value">{{ service_port }}</td>
|
||||
</tr>{{ end -}}
|
||||
{{- if request_id }}<tr>
|
||||
<td class="name" data-l10n>Request ID</td>
|
||||
<td class="value">{{ request_id }}</td>
|
||||
</tr>{{ end -}}
|
||||
<tr>
|
||||
<td class="name" data-l10n>Timestamp</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
|
398
templates/lost-in-space.html
Normal file
398
templates/lost-in-space.html
Normal file
@ -0,0 +1,398 @@
|
||||
<!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>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans" rel="stylesheet">
|
||||
<style>
|
||||
/** Codepen: https://codepen.io/kdbkapsere/pen/oNXLbqQ */
|
||||
|
||||
:root {
|
||||
--color-bg-primary: #fff;
|
||||
--color-text-primary: #0e0620;
|
||||
|
||||
--color-ui-bg-primary: #0e0620;
|
||||
--color-ui-bg-inverted: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-bg-primary: #212121;
|
||||
--color-text-primary: #fafafa;
|
||||
|
||||
--color-ui-bg-primary: #fafafa;
|
||||
--color-ui-bg-inverted: #212121;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-primary);
|
||||
color: var(--color-text-primary);
|
||||
font-family: 'Nunito Sans', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 1140px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.picture, .content {
|
||||
box-sizing: border-box;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
svg .dark {
|
||||
stroke: var(--color-ui-bg-primary);
|
||||
}
|
||||
|
||||
svg .fill-dark {
|
||||
fill: var(--color-ui-bg-primary);
|
||||
}
|
||||
|
||||
svg .fill-light {
|
||||
fill: var(--color-ui-bg-inverted);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 150px;
|
||||
margin: 15px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* {{ if show_details }} */
|
||||
.details {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.details li span {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.details li code {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
padding-left: 7px;
|
||||
}
|
||||
/* {{ end }} */
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.picture, .content {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.picture svg {
|
||||
max-width: 60%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main>
|
||||
<div class="picture">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
|
||||
<g>
|
||||
<defs>
|
||||
<clipPath id="GlassClip">
|
||||
<path
|
||||
d="M380.857,346.164c-1.247,4.651-4.668,8.421-9.196,10.06c-9.332,3.377-26.2,7.817-42.301,3.5 s-28.485-16.599-34.877-24.192c-3.101-3.684-4.177-8.66-2.93-13.311l7.453-27.798c0.756-2.82,3.181-4.868,6.088-5.13 c6.755-0.61,20.546-0.608,41.785,5.087s33.181,12.591,38.725,16.498c2.387,1.682,3.461,4.668,2.705,7.488L380.857,346.164z"/>
|
||||
</clipPath>
|
||||
<clipPath id="cordClip">
|
||||
<rect width="800" height="600"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="planet">
|
||||
<circle fill="none" stroke-width="3" stroke-miterlimit="10" cx="572.859" cy="108.803"
|
||||
r="90.788" class="dark" />
|
||||
<circle id="craterBig" fill="none" stroke-width="3" stroke-miterlimit="10" cx="548.891"
|
||||
cy="62.319" r="13.074" class="dark"/>
|
||||
<circle id="craterSmall" fill="none" stroke-width="3" stroke-miterlimit="10" cx="591.743"
|
||||
cy="158.918" r="7.989" class="dark"/>
|
||||
<path id="ring" fill="none" stroke-width="3" stroke-linecap="round" stroke-miterlimit="10"
|
||||
class="dark"
|
||||
d="M476.562,101.461c-30.404,2.164-49.691,4.221-49.691,8.007c0,6.853,63.166,12.408,141.085,12.408s141.085-5.555,141.085-12.408c0-3.378-15.347-4.988-40.243-7.225"/>
|
||||
<path id="ringShadow" opacity="0.5" fill="none" class="dark" stroke-width="3"
|
||||
stroke-linecap="round" stroke-miterlimit="10"
|
||||
d="M483.985,127.43c23.462,1.531,52.515,2.436,83.972,2.436c36.069,0,68.978-1.19,93.922-3.149"/>
|
||||
</g>
|
||||
<g id="stars">
|
||||
<g id="starsBig">
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="518.07" y1="245.375" x2="518.07" y2="266.581"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="508.129" y1="255.978" x2="528.01" y2="255.978"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="154.55" y1="231.391" x2="154.55" y2="252.598"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="144.609" y1="241.995" x2="164.49" y2="241.995"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="320.135" y1="132.746" x2="320.135" y2="153.952"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="310.194" y1="143.349" x2="330.075" y2="143.349"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="200.67" y1="483.11" x2="200.67" y2="504.316"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="210.611" y1="493.713" x2="190.73" y2="493.713"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="starsSmall">
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="432.173" y1="380.52" x2="432.173" y2="391.83"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="426.871" y1="386.175" x2="437.474" y2="386.175"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="489.555" y1="299.765" x2="489.555" y2="308.124"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="485.636" y1="303.945" x2="493.473" y2="303.945"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="231.468" y1="291.009" x2="231.468" y2="299.369"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="227.55" y1="295.189" x2="235.387" y2="295.189"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="244.032" y1="547.539" x2="244.032" y2="555.898"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="247.95" y1="551.719" x2="240.113" y2="551.719"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="186.359" y1="406.967" x2="186.359" y2="415.326"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="190.277" y1="411.146" x2="182.44" y2="411.146"/>
|
||||
</g>
|
||||
<g>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="480.296" y1="406.967" x2="480.296" y2="415.326"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" x1="484.215" y1="411.146" x2="476.378" y2="411.146"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="circlesBig">
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="588.977" cy="255.978" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="450.066" cy="320.259" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="168.303" cy="353.753" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="429.522" cy="201.185" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="200.67" cy="176.313" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="133.343" cy="477.014" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="283.521" cy="568.033" r="7.952"/>
|
||||
<circle fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-miterlimit="10" cx="413.618" cy="482.387" r="7.952"/>
|
||||
</g>
|
||||
<g id="circlesSmall">
|
||||
<circle class="fill-dark" cx="549.879" cy="296.402" r="2.651"/>
|
||||
<circle class="fill-dark" cx="253.29" cy="229.24" r="2.651"/>
|
||||
<circle class="fill-dark" cx="434.824" cy="263.931" r="2.651"/>
|
||||
<circle class="fill-dark" cx="183.708" cy="544.176" r="2.651"/>
|
||||
<circle class="fill-dark" cx="382.515" cy="530.923" r="2.651"/>
|
||||
<circle class="fill-dark" cx="130.693" cy="305.608" r="2.651"/>
|
||||
<circle class="fill-dark" cx="480.296" cy="477.014" r="2.651"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="spaceman" clip-path="url(cordClip)">
|
||||
<path id="cord" fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M273.813,410.969c0,0-54.527,39.501-115.34,38.218c-2.28-0.048-4.926-0.241-7.841-0.548c-68.038-7.178-134.288-43.963-167.33-103.87c-0.908-1.646-1.793-3.3-2.654-4.964c-18.395-35.511-37.259-83.385-32.075-118.817"/>
|
||||
<path id="backpack" class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M338.164,454.689l-64.726-17.353c-11.086-2.972-17.664-14.369-14.692-25.455l15.694-58.537c3.889-14.504,18.799-23.11,33.303-19.221l52.349,14.035c14.504,3.889,23.11,18.799,19.221,33.303l-15.694,58.537C360.647,451.083,349.251,457.661,338.164,454.689z"/>
|
||||
<g id="antenna">
|
||||
<line class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10" x1="323.396" y1="236.625"
|
||||
x2="295.285" y2="353.753"/>
|
||||
<circle class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10" cx="323.666" cy="235.617"
|
||||
r="6.375"/>
|
||||
</g>
|
||||
<g id="armR">
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M360.633,363.039c1.352,1.061,4.91,5.056,5.824,6.634l27.874,47.634c3.855,6.649,1.59,15.164-5.059,19.02l0,0c-6.649,3.855-15.164,1.59-19.02-5.059l-5.603-9.663"/>
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M388.762,434.677c5.234-3.039,7.731-8.966,6.678-14.594c2.344,1.343,4.383,3.289,5.837,5.793c4.411,7.596,1.829,17.33-5.767,21.741c-7.596,4.411-17.33,1.829-21.741-5.767c-1.754-3.021-2.817-5.818-2.484-9.046C375.625,437.355,383.087,437.973,388.762,434.677z"/>
|
||||
</g>
|
||||
<g id="armL">
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M301.301,347.66c-1.702,0.242-5.91,1.627-7.492,2.536l-47.965,27.301c-6.664,3.829-8.963,12.335-5.134,18.999h0c3.829,6.664,12.335,8.963,18.999,5.134l9.685-5.564"/>
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M241.978,395.324c-3.012-5.25-2.209-11.631,1.518-15.977c-2.701-0.009-5.44,0.656-7.952,2.096c-7.619,4.371-10.253,14.09-5.883,21.71c4.371,7.619,14.09,10.253,21.709,5.883c3.03-1.738,5.35-3.628,6.676-6.59C252.013,404.214,245.243,401.017,241.978,395.324z"/>
|
||||
</g>
|
||||
<g id="body">
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M353.351,365.387c-7.948,1.263-16.249,0.929-24.48-1.278c-8.232-2.207-15.586-6.07-21.836-11.14c-17.004,4.207-31.269,17.289-36.128,35.411l-1.374,5.123c-7.112,26.525,8.617,53.791,35.13,60.899l0,0c26.513,7.108,53.771-8.632,60.883-35.158l1.374-5.123C371.778,395.999,365.971,377.536,353.351,365.387z"/>
|
||||
<path fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M269.678,394.912L269.678,394.912c26.3,20.643,59.654,29.585,93.106,25.724l2.419-0.114"/>
|
||||
</g>
|
||||
<g id="legs">
|
||||
<g id="legR">
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M312.957,456.734l-14.315,53.395c-1.896,7.07,2.299,14.338,9.37,16.234l0,0c7.07,1.896,14.338-2.299,16.234-9.37l17.838-66.534C333.451,455.886,323.526,457.387,312.957,456.734z"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10" x1="304.883" y1="486.849"
|
||||
x2="330.487" y2="493.713"/>
|
||||
</g>
|
||||
<g id="legL">
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M296.315,452.273L282,505.667c-1.896,7.07-9.164,11.265-16.234,9.37l0,0c-7.07-1.896-11.265-9.164-9.37-16.234l17.838-66.534C278.993,441.286,286.836,447.55,296.315,452.273z"/>
|
||||
<line fill="none" class="dark" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10" x1="262.638" y1="475.522"
|
||||
x2="288.241" y2="482.387"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="head">
|
||||
<ellipse transform="matrix(0.259 -0.9659 0.9659 0.259 -51.5445 563.2371)"
|
||||
class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10" cx="341.295" cy="315.211"
|
||||
rx="61.961" ry="60.305"/>
|
||||
<path id="headStripe" fill="none" class="dark" stroke-width="3"
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M330.868,261.338c-7.929,1.72-15.381,5.246-21.799,10.246"/>
|
||||
<path class="dark fill-light" stroke-width="3" stroke-linecap="round"
|
||||
stroke-linejoin="round" stroke-miterlimit="10"
|
||||
d="M380.857,346.164c-1.247,4.651-4.668,8.421-9.196,10.06c-9.332,3.377-26.2,7.817-42.301,3.5s-28.485-16.599-34.877-24.192c-3.101-3.684-4.177-8.66-2.93-13.311l7.453-27.798c0.756-2.82,3.181-4.868,6.088-5.13c6.755-0.61,20.546-0.608,41.785,5.087s33.181,12.591,38.725,16.498c2.387,1.682,3.461,4.668,2.705,7.488L380.857,346.164z"/>
|
||||
<g clip-path="url(#GlassClip)">
|
||||
<polygon id="glassShine" fill="none" class="dark" stroke-width="3"
|
||||
stroke-miterlimit="10"
|
||||
points="278.436,375.599 383.003,264.076 364.393,251.618 264.807,364.928 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>{{ code }}</h1>
|
||||
<h2><span data-l10n>UH OH</span>! <span data-l10n>{{ message }}</span></h2>
|
||||
<p data-l10n>{{ description }}</p>
|
||||
|
||||
{{ if show_details }}
|
||||
<ul class="details">
|
||||
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
|
||||
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
|
||||
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
|
||||
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
|
||||
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
|
||||
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
|
||||
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
|
||||
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
|
||||
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"
|
||||
integrity="sha512-H6cPm97FAsgIKmlBA4s774vqoN24V5gSQL4yBTDOY2su2DeXZVhQPxFK4P6GPdnZqM9fg1G3cMv5wD7e6cFLZQ=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script>
|
||||
/** @var {object} gsap is a library for building high-performance animations */
|
||||
|
||||
if (typeof gsap === 'object') {
|
||||
gsap.set("svg", {visibility: "visible"});
|
||||
gsap.to("#headStripe", {y: 0.5, rotation: 1, yoyo: true, repeat: -1, ease: "sine.inOut", duration: 1});
|
||||
gsap.to("#spaceman", {y: 0.5, rotation: 1, yoyo: true, repeat: -1, ease: "sine.inOut", duration: 1});
|
||||
gsap.to("#craterSmall", {x: -3, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut"});
|
||||
gsap.to("#craterBig", {x: 3, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut"});
|
||||
gsap.to("#planet", {rotation: -2, yoyo: true, repeat: -1, duration: 1, ease: "sine.inOut", transformOrigin: "50% 50%"});
|
||||
gsap.to("#starsBig g", {rotation: "random(-30,30)", transformOrigin: "50% 50%", yoyo: true, repeat: -1, ease: "sine.inOut"});
|
||||
gsap.fromTo("#starsSmall g", {scale: 0, transformOrigin: "50% 50%"}, {scale: 1, transformOrigin: "50% 50%", yoyo: true, repeat: -1, stagger: 0.1});
|
||||
gsap.to("#circlesSmall circle", {y: -4, yoyo: true, duration: 1, ease: "sine.inOut", repeat: -1});
|
||||
gsap.to("#circlesBig circle", {y: -2, yoyo: true, duration: 1, ease: "sine.inOut", repeat: -1});
|
||||
gsap.set("#glassShine", {x: -68});
|
||||
gsap.to("#glassShine", {x: 80, duration: 2, rotation: -30, ease: "expo.inOut", transformOrigin: "50% 50%", repeat: -1, repeatDelay: 8, delay: 2});
|
||||
} else {
|
||||
console.warn('gsap library is not initialized (network error?)')
|
||||
}
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
</html>
|
281
templates/matrix.html
Normal file
281
templates/matrix.html
Normal file
@ -0,0 +1,281 @@
|
||||
<!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"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>{{ message }} ({{ code }})</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root{--matrix-glyph-size:15px;--matrix-glyph-font-size:15px;--matrix-glyph-front-color:rgba(255, 255, 255, 0.8);--matrix-glyph-tail-color:#0f0;--matrix-overlay-color:rgba(0, 0, 0, 0.12)}
|
||||
body,html{margin:0;padding:0;background-color:#000;height:100vh}
|
||||
#matrix{display:block;position:fixed;width:100vw;height:100vh}
|
||||
.container{align-items:center;display:flex;justify-content:center;position:absolute;top:0;left:0;width:100vw;height:100vh;z-index:1}
|
||||
.container .message{background-color:rgba(0,0,0,.85);border:2px solid var(--matrix-glyph-tail-color);padding:15px 20px;margin:0 20px;font-family:Inconsolata,Helvetica,sans-serif;text-align:center;font-size:0;color:var(--matrix-glyph-tail-color);text-shadow:1px 0 2px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);box-shadow:1px 0 5px var(--matrix-glyph-tail-color),-1px 0 2px var(--matrix-glyph-tail-color);max-width:640px}
|
||||
.container .message h1{margin:0;font-size:52px}
|
||||
.container .message p{margin:.3em 0 0 0;font-size:17px;color:var(--matrix-glyph-front-color)}
|
||||
/* {{ if show_details }} */
|
||||
.container .details{margin-top:20px}
|
||||
.container .details ul{padding:0}
|
||||
.container .details code,.container .details span{font-size:11px}
|
||||
.container .details code{padding-left:7px}
|
||||
/* {{ end }} */
|
||||
.hidden {display:none}
|
||||
@media screen and (max-width:820px){
|
||||
:root{--matrix-glyph-size:10px;--matrix-glyph-font-size:10px}
|
||||
.container .message h1{font-size:38px}
|
||||
.container .message p{font-size:13px}
|
||||
/* {{ if show_details }} */
|
||||
.container .details code,.container .details span{font-size:11px}
|
||||
/* {{ end }} */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<ul id="matrix-words" class="hidden">
|
||||
<li>{{ code }}</li>
|
||||
<li>{{ message }}</li>
|
||||
<li>{{ description }}</li>
|
||||
<li>{{ code }} {{ message }}</li>
|
||||
</ul>
|
||||
|
||||
<div class="message">
|
||||
<h1>{{ code }}: <span data-l10n>{{ message }}</span></h1>
|
||||
<p data-l10n>{{ description }}</p>
|
||||
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<ul>
|
||||
{{- if host }}<li><span><span data-l10n>Host</span>:</span> <code>{{ host }}</code></li>{{ end -}}
|
||||
{{- if original_uri }}<li><span><span data-l10n>Original URI</span>:</span> <code>{{ original_uri }}</code></li>{{ end -}}
|
||||
{{- if forwarded_for }}<li><span><span data-l10n>Forwarded for</span>:</span> <code>{{ forwarded_for }}</code></li>{{ end -}}
|
||||
{{- if namespace }}<li><span><span data-l10n>Namespace</span>:</span> <code>{{ namespace }}</code></li>{{ end -}}
|
||||
{{- if ingress_name }}<li><span><span data-l10n>Ingress name</span>:</span> <code>{{ ingress_name }}</code></li>{{ end -}}
|
||||
{{- if service_name }}<li><span><span data-l10n>Service name</span>:</span> <code>{{ service_name }}</code></li>{{ end -}}
|
||||
{{- if service_port }}<li><span><span data-l10n>Service port</span>:</span> <code>{{ service_port }}</code></li>{{ end -}}
|
||||
{{- if request_id }}<li><span><span data-l10n>Request ID</span>:</span> <code>{{ request_id }}</code></li>{{ end -}}
|
||||
<li><span><span data-l10n>Timestamp</span>:</span> <code>{{ now.Unix }}</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="matrix"></canvas>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {HTMLCanvasElement} $canvas
|
||||
* @constructor
|
||||
*/
|
||||
const Matrix = function ($canvas) {
|
||||
const symbols = 'ラドクリフマラソンわたしワタシんょンョたばこタバコとうきょうトウキョウ '.split('');
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
const getRandomSymbol = function () {
|
||||
return symbols[Math.floor(Math.random() * symbols.length)];
|
||||
}
|
||||
|
||||
const ctx = $canvas.getContext('2d');
|
||||
ctx.globalCompositeOperation = 'lighter'; // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
|
||||
|
||||
this.redrawInterval = 90; // fade oud speed
|
||||
this.glyphSize = 0;
|
||||
this.rowsCapacity = 0;
|
||||
this.columnsCapacity = 0;
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.init = () => {
|
||||
$canvas.width = $canvas.clientWidth;
|
||||
$canvas.height = $canvas.clientHeight;
|
||||
|
||||
this.glyphSize = parseInt(getComputedStyle($canvas).getPropertyValue('--matrix-glyph-size'), 10);
|
||||
this.rowsCapacity = Math.ceil($canvas.clientHeight / this.glyphSize);
|
||||
this.columnsCapacity = Math.ceil($canvas.clientWidth / this.glyphSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} symbol
|
||||
* @param {number} row
|
||||
* @param {number} column
|
||||
* @param {string} color
|
||||
*/
|
||||
const drawSymbol = (symbol, row, column, color) => {
|
||||
if (row > this.rowsCapacity || column > this.columnsCapacity) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.font = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-font-size') + ' monospace';
|
||||
|
||||
if (symbol.length > 1) {
|
||||
symbol = symbol.charAt(0); // only one char is allowed
|
||||
}
|
||||
|
||||
let xOffset = 0, charCode = symbol.charCodeAt(0);
|
||||
|
||||
if (charCode >= 33 && charCode <= 126) { // is ascii
|
||||
xOffset = this.glyphSize / 5;
|
||||
}
|
||||
|
||||
ctx.fillText(symbol, (column * this.glyphSize) + xOffset, row * this.glyphSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} column
|
||||
* @param {number} speed Lowest = fastest, largest = slowest
|
||||
* @param {string?} text
|
||||
* @param {number?} offset
|
||||
*/
|
||||
const drawLine = (column, speed, text, offset) => {
|
||||
let cursor = 0;
|
||||
|
||||
const tailColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-tail-color'),
|
||||
frontColor = getComputedStyle($canvas).getPropertyValue('--matrix-glyph-front-color');
|
||||
|
||||
const handler = window.setInterval(() => {
|
||||
if (column > this.columnsCapacity) {
|
||||
return window.clearInterval(handler);
|
||||
}
|
||||
|
||||
if (cursor <= this.rowsCapacity) {
|
||||
let symbol = getRandomSymbol();
|
||||
|
||||
if (typeof text === 'string' && typeof offset === 'number') {
|
||||
if (cursor >= offset && text.length >= cursor - offset) {
|
||||
symbol = text.charAt(cursor - offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof symbol === 'string' && symbol !== ' ') {
|
||||
const prev = cursor;
|
||||
|
||||
window.setTimeout(() => { // redraw with a green color
|
||||
drawSymbol(symbol, prev, column, tailColor);
|
||||
}, speed / 1.3);
|
||||
|
||||
drawSymbol(symbol, cursor, column, frontColor); // white color first
|
||||
}
|
||||
|
||||
cursor++;
|
||||
} else {
|
||||
window.clearInterval(handler);
|
||||
}
|
||||
}, speed);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.redraw = () => {
|
||||
ctx.fillStyle = getComputedStyle($canvas).getPropertyValue('--matrix-overlay-color');
|
||||
ctx.fillRect(0, 0, $canvas.clientWidth, $canvas.clientHeight);
|
||||
};
|
||||
|
||||
let redrawIntervalHandler = undefined, dropsIntervalHandler = undefined;
|
||||
|
||||
/**
|
||||
* @param {HTMLUListElement?} $linesList
|
||||
*/
|
||||
this.run = ($linesList) => {
|
||||
if (redrawIntervalHandler === undefined) {
|
||||
redrawIntervalHandler = window.setInterval(this.redraw, this.redrawInterval);
|
||||
}
|
||||
|
||||
if (dropsIntervalHandler === undefined) {
|
||||
const fn = () => {
|
||||
const randomColumn = Math.floor(Math.random() * (this.columnsCapacity + 1)),
|
||||
minSpeed = 200, maxSpeed = 50,
|
||||
randomSpeed = Math.floor(Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed;
|
||||
|
||||
const list = [];
|
||||
let line = undefined, offset = undefined;
|
||||
|
||||
if ($linesList !== undefined) {
|
||||
Array.prototype.forEach.call($linesList.querySelectorAll('li'), $li => {
|
||||
const text = $li.innerText.trim();
|
||||
|
||||
if (text.length > 0) {
|
||||
list.push(text);
|
||||
}
|
||||
});
|
||||
|
||||
if (list.length > 0 && Math.random() > 0.4) {
|
||||
line = list[Math.floor(Math.random() * list.length)];
|
||||
offset = Math.floor(Math.random() * line.length);
|
||||
|
||||
if (offset <= 5) {
|
||||
offset *= 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawLine(randomColumn, randomSpeed, line, offset);
|
||||
|
||||
if (dropsIntervalHandler !== undefined) {
|
||||
window.clearInterval(dropsIntervalHandler);
|
||||
dropsIntervalHandler = undefined;
|
||||
}
|
||||
|
||||
dropsIntervalHandler = window.setInterval(fn, ((minSpeed + maxSpeed) / 2 * this.rowsCapacity) / this.columnsCapacity / 0.5);
|
||||
};
|
||||
|
||||
fn();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {void}
|
||||
*/
|
||||
this.stop = () => {
|
||||
if (redrawIntervalHandler !== undefined) {
|
||||
window.clearInterval(redrawIntervalHandler);
|
||||
redrawIntervalHandler = undefined;
|
||||
}
|
||||
|
||||
if (dropsIntervalHandler !== undefined) {
|
||||
window.clearInterval(dropsIntervalHandler);
|
||||
dropsIntervalHandler = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof ResizeObserver === 'function') {
|
||||
(new ResizeObserver(this.init)).observe($canvas);
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
};
|
||||
|
||||
(new Matrix(document.getElementById('matrix'))).run(document.getElementById('matrix-words'));
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
</html>
|
@ -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>
|
||||
@ -119,7 +130,7 @@
|
||||
<div class="container-center">
|
||||
<div>
|
||||
<h1>{{ code }}</h1>
|
||||
<h2>{{ description }}</h2>
|
||||
<h2 data-l10n>{{ description }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -163,6 +174,17 @@
|
||||
window.addEventListener('beforeunload', function (/** @param BeforeUnloadEvent event */ event) {
|
||||
window.clearInterval(flickerInterval);
|
||||
});
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
|
39
templates/readme.md
Normal file
39
templates/readme.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Templates
|
||||
|
||||
Creating templates is a very simple operation, even for those who know nothing at all about [Go Template](https://pkg.go.dev/text/template). All you should know is:
|
||||
|
||||
- The template should be one page. Without additional `css` or `js` files (but you can load them from the CDN or another GitHub repositories using [jsdelivr.com](https://www.jsdelivr.com/), for example)
|
||||
- Don't forget to include `<meta name="robots" content="noindex, nofollow" />` tag in the header
|
||||
- You can use a special "placeholders" (wrapped in `{{` and `}}`) for the rendering error code, message and others (see details below)
|
||||
|
||||
## Supported signatures
|
||||
|
||||
### Error page & request data
|
||||
|
||||
| Signature | Description | Example |
|
||||
|--------------------------------------|---------------------------------------------------------------|----------------------------------------------|
|
||||
| `{{ code }}` | Error page code | `404` |
|
||||
| `{{ message }}` | Error code message | `Not found` |
|
||||
| `{{ description }}` | Error code description | `The server can not find the requested page` |
|
||||
| `{{ original_uri }}` | `X-Original-URI` header value | `/foo1/bar2` |
|
||||
| `{{ namespace }}` | `X-Namespace` header value | `foo` |
|
||||
| `{{ ingress_name }}` | `X-Ingress-Name` header value | `bar` |
|
||||
| `{{ service_name }}` | `X-Service-Name` header value | `baz` |
|
||||
| `{{ service_port }}` | `X-Service-Port` header value | `8080` |
|
||||
| `{{ request_id }}` | `X-Request-ID` header value | `12AB34CD56EF78` |
|
||||
| `{{ forwarded_for }}` | `X-Forwarded-For` header value | `203.0.113.195, 70.41.3.18` |
|
||||
| `{{ host }}` | `Host` header value | `example.com` |
|
||||
| `{{ now.Unix }}` | Current timestamp (e.g. in Unix format) | `1643621927` |
|
||||
| `{{ hostname }}` | OS hostname | `ab12cd34ef56` |
|
||||
| `{{ version }}` | Application version | `2.5.0` |
|
||||
| `{{ if show_details }}...{{ end }}` | Logical operator (server started with "show details" option?) | |
|
||||
| `{{ if hide_details }}...{{ end }}` | Same as above, but inverted | |
|
||||
| `{{ if l10n_enabled }}...{{ end }}` | Logical operator (l10n is enabled?) | |
|
||||
| `{{ if l10n_disabled }}...{{ end }}` | Same as above, but inverted | |
|
||||
|
||||
### Modifiers
|
||||
|
||||
| Signature | Description | Example |
|
||||
|------------------------------------|--------------------------------|-------------------------------------|
|
||||
| <code>{{ ... | json }}</code> | Convert value into json-string | <code>{{ code | json }}</code> |
|
||||
| <code>{{ ... | int }}</code> | Convert value into integer | <code>{{ code | int }}</code> |
|
@ -15,61 +15,216 @@
|
||||
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>
|
||||
<div id="error_text">
|
||||
<span class="source">{{ code }}: <span data-l10n>{{ message }}</span></span>
|
||||
<span class="target"></span>
|
||||
</div>
|
||||
{{ if show_details }}
|
||||
<div class="hidden" id="details">
|
||||
<table>
|
||||
{{- if host }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Host</span>:</td>
|
||||
<td class="value">{{ host }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if original_uri }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Original URI</span>:</td>
|
||||
<td class="value">{{ original_uri }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if forwarded_for }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Forwarded for</span>:</td>
|
||||
<td class="value">{{ forwarded_for }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if namespace }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Namespace</span>:</td>
|
||||
<td class="value">{{ namespace }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if ingress_name }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Ingress name</span>:</td>
|
||||
<td class="value">{{ ingress_name }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if service_name }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Service name</span>:</td>
|
||||
<td class="value">{{ service_name }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if service_port }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Service port</span>:</td>
|
||||
<td class="value">{{ service_port }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
{{- if request_id }}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Request ID</span>:</td>
|
||||
<td class="value">{{ request_id }}</td>
|
||||
</tr>
|
||||
{{ end -}}
|
||||
<tr>
|
||||
<td class="name"><span data-l10n>Timestamp</span>:</td>
|
||||
<td class="value">{{ now.Unix }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const $errorText = document.getElementById('error_text'),
|
||||
text = $errorText.innerText,
|
||||
characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=+<>,./?[{()}]!@#$%^&*~`\|'.split('');
|
||||
let progress = 0;
|
||||
/**
|
||||
* @param {HTMLElement} $el
|
||||
*/
|
||||
const Shuffle = function ($el) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-=+<>,./?[{()}]!@#$%^&*~`\|'.split(''),
|
||||
$source = $el.querySelector('.source'), $target = $el.querySelector('.target');
|
||||
|
||||
const scrambleInterval = window.setInterval(function () {
|
||||
let newText = text;
|
||||
let cursor = 0, scrambleInterval = undefined, cursorDelayInterval = undefined, cursorInterval = undefined;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (i >= progress) {
|
||||
newText = newText.substr(0, i) +
|
||||
characters[Math.round(Math.random() * (characters.length - 1))] +
|
||||
newText.substr(i + 1);
|
||||
/**
|
||||
* @param {Number} len
|
||||
* @return {string}
|
||||
*/
|
||||
const getRandomizedString = function (len) {
|
||||
let s = '';
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
s += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
}
|
||||
|
||||
$errorText.innerText = newText;
|
||||
}, 800 / 60);
|
||||
return s;
|
||||
};
|
||||
|
||||
window.setTimeout(function () {
|
||||
let revealInterval = window.setInterval(function () {
|
||||
if (progress < text.length) {
|
||||
progress++;
|
||||
} else {
|
||||
window.clearInterval(revealInterval);
|
||||
this.start = function () {
|
||||
$source.style.display = 'none';
|
||||
$target.style.display = 'block';
|
||||
|
||||
scrambleInterval = window.setInterval(() => {
|
||||
if (cursor <= $source.innerText.length) {
|
||||
$target.innerText = $source.innerText.substring(0, cursor) + getRandomizedString($source.innerText.length - cursor);
|
||||
}
|
||||
}, 450 / 30);
|
||||
|
||||
cursorDelayInterval = window.setTimeout(() => {
|
||||
cursorInterval = window.setInterval(() => {
|
||||
if (cursor > $source.innerText.length - 1) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
cursor++;
|
||||
}, 70);
|
||||
}, 350);
|
||||
};
|
||||
|
||||
this.stop = function () {
|
||||
$source.style.display = 'block';
|
||||
$target.style.display = 'none';
|
||||
$target.innerText = '';
|
||||
cursor = 0;
|
||||
|
||||
if (scrambleInterval !== undefined) {
|
||||
window.clearInterval(scrambleInterval);
|
||||
scrambleInterval = undefined;
|
||||
}
|
||||
}, 70);
|
||||
}, 350);
|
||||
|
||||
if (cursorInterval !== undefined) {
|
||||
window.clearInterval(cursorInterval);
|
||||
cursorInterval = undefined;
|
||||
}
|
||||
|
||||
if (cursorDelayInterval !== undefined) {
|
||||
window.clearInterval(cursorDelayInterval);
|
||||
cursorDelayInterval = undefined;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
(new Shuffle(document.getElementById('error_text'))).start();
|
||||
|
||||
// {{ if show_details }}
|
||||
window.setTimeout(function () {
|
||||
document.getElementById('details').classList.remove('hidden');
|
||||
}, 550);
|
||||
// {{ end }}
|
||||
|
||||
// {{ if l10n_enabled }}
|
||||
if (navigator.language.substring(0, 2).toLowerCase() !== 'en') {
|
||||
((s, p) => { // localize the page (details here - https://github.com/tarampampam/error-pages/tree/master/l10n)
|
||||
s.src = 'https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js'; // '../l10n/l10n.js';
|
||||
s.async = s.defer = true;
|
||||
s.addEventListener('load', () => p.removeChild(s));
|
||||
p.appendChild(s);
|
||||
})(document.createElement('script'), document.body);
|
||||
}
|
||||
// {{ end }}
|
||||
</script>
|
||||
</body>
|
||||
<!--
|
||||
|
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"
|
13
test/hurl/proxy_headers.hurl
Normal file
13
test/hurl/proxy_headers.hurl
Normal file
@ -0,0 +1,13 @@
|
||||
GET http://{{ host }}:{{ port }}/502.html
|
||||
X-Foo: foo
|
||||
bar: BAR
|
||||
Baz_blah: baz Baz
|
||||
NonEx: skip
|
||||
|
||||
HTTP/* 200
|
||||
|
||||
[Asserts]
|
||||
header "X-Foo" == "foo"
|
||||
header "Bar" == "BAR"
|
||||
header "Baz_blah" == "baz Baz"
|
||||
header "NonEx" not exists
|
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